Skip to content

fix(linux/xdgportal): Properly support multiple screens by exposing pipewire streams as separate displays#4931

Open
Kishi85 wants to merge 8 commits intoLizardByte:masterfrom
Kishi85:xdgportalgrab-better-multi-monitor-support
Open

fix(linux/xdgportal): Properly support multiple screens by exposing pipewire streams as separate displays#4931
Kishi85 wants to merge 8 commits intoLizardByte:masterfrom
Kishi85:xdgportalgrab-better-multi-monitor-support

Conversation

@Kishi85
Copy link
Copy Markdown
Contributor

@Kishi85 Kishi85 commented Mar 31, 2026

Description

This PR adds proper multi-monitor support to Sunshine's XDG portal grab by exposing all streams of a capture portal as separate screens (utilizing the multiple capture mode of the screencast portal).

Things this PR does:

  • Improve logging done by portalgrab.cpp for easier debugging (could be moved to a separate PR if desired)
  • Change screencast source selection mode to multiple
  • Expose separate displays for each screen stream provided by the screencast portal (ordering streams consistently by screen position)
  • Fix (more or less defunct) session caching and properly invalidate the cache only when screens are added
  • Fix the (currently broken) keyboard shortcut for the switch_display_event
  • Use a better way to handle stopping the stream via XDG notification icon (as doing it based on matching the display resolution will break display switching)
  • Adds a few more fail safes based on newly added stream_connected indicator which is determined by stream events.

Known issues:

  • Can segfault when trying to switch displays really quickly (by mashing the screen switch shortcut multiple times within a second) and even then it's not fully reproducible. This would have been happening even before this PR when just having a single stream but was masked due to the shortcut handling not being properly implemented in the capture logic (missing return on !push_captured_image_cb). This is a separate issue concerning other capturemethods as well (tested with KMS), see Coredump with SIGSEGV when switching displays too quickly #4943

Screenshot

Issues Fixed or Closed

Roadmap Issues

Type of Change

  • feat: New feature (non-breaking change which adds functionality)
  • fix: Bug fix (non-breaking change which fixes an issue)
  • docs: Documentation only changes
  • style: Changes that do not affect the meaning of the code (white-space, formatting, missing semicolons, etc.)
  • refactor: Code change that neither fixes a bug nor adds a feature
  • perf: Code change that improves performance
  • test: Adding missing tests or correcting existing tests
  • build: Changes that affect the build system or external dependencies
  • ci: Changes to CI configuration files and scripts
  • chore: Other changes that don't modify src or test files
  • revert: Reverts a previous commit
  • BREAKING CHANGE: Introduces a breaking change (can be combined with any type above)

Checklist

  • Code follows the style guidelines of this project
  • Code has been self-reviewed
  • Code has been commented, particularly in hard-to-understand areas
  • Code docstring/documentation-blocks for new or existing methods/components have been added or updated
  • Unit tests have been added or updated for any new or modified functionality

AI Usage

  • None: No AI tools were used in creating this PR
  • Light: AI provided minor assistance (formatting, simple suggestions)
  • Moderate: AI helped with code generation or debugging specific parts
  • Heavy: AI generated most or all of the code changes

@Kishi85 Kishi85 changed the title WIP: xdgportalgrab: Expose multiple streams as separate displays for better multi monitor support with client-side display switching fix(linux/xdgportal): WIP: Expose multiple streams as separate displays for better multi monitor support with client-side display switching Mar 31, 2026
@Kishi85 Kishi85 force-pushed the xdgportalgrab-better-multi-monitor-support branch 2 times, most recently from 1f4fa32 to 58efcab Compare March 31, 2026 18:44
@Kishi85
Copy link
Copy Markdown
Contributor Author

Kishi85 commented Mar 31, 2026

@psyke83 Sorry to ping you here but can you think of a reason why the keyboard shortcut to switch displays would not work with portalgrab?

I've been trying to do so after enumerating them properly for display_names (current state of this PR) but the switch_display_event from video.cpp does not seem to be firing or handled when using portalgrab and I can't figure what I'm missing.

@psyke83
Copy link
Copy Markdown
Contributor

psyke83 commented Mar 31, 2026

@psyke83 Sorry to ping you here but can you think of a reason why the keyboard shortcut to switch displays would not work with portalgrab?

I've been trying to do so after enumerating them properly for display_names (current state of this PR) but the switch_display_event from video.cpp does not seem to be firing or handled when using portalgrab and I can't figure what I'm missing.

If you add some debug to video.cpp you'll see that your current code is hitting the switch_display_event->peek() case when the combination is first pressed, but the peek event then repeatedly triggers. The artificial reinit is not completing successfully for some reason.

@psyke83
Copy link
Copy Markdown
Contributor

psyke83 commented Mar 31, 2026

Look:

case platf::capture_e::timeout:
if (!push_captured_image_cb(std::move(img_out), false)) {
return platf::capture_e::ok;
}
break;
case platf::capture_e::ok:
if (!push_captured_image_cb(std::move(img_out), true)) {
return platf::capture_e::ok;
}
break;

It seems that portalgrab is not checking the push_captured_image_cb callback the same way in the capture loop. The same logic may need to be added. I will look at this further later when I have time, if you don't figure it out.

@Kishi85
Copy link
Copy Markdown
Contributor Author

Kishi85 commented Apr 1, 2026

Look:

case platf::capture_e::timeout:
if (!push_captured_image_cb(std::move(img_out), false)) {
return platf::capture_e::ok;
}
break;
case platf::capture_e::ok:
if (!push_captured_image_cb(std::move(img_out), true)) {
return platf::capture_e::ok;
}
break;

It seems that portalgrab is not checking the push_captured_image_cb callback the same way in the capture loop. The same logic may need to be added. I will look at this further later when I have time, if you don't figure it out.

That is exactly what was missing to get the keyboard shortcut to work. Thanks!

@Kishi85 Kishi85 force-pushed the xdgportalgrab-better-multi-monitor-support branch 4 times, most recently from 5fa6e6f to 6d7942e Compare April 1, 2026 08:21
@Kishi85
Copy link
Copy Markdown
Contributor Author

Kishi85 commented Apr 1, 2026

I've got the basic functionality working now and can switch multiple displays using keyboard shortcuts.

The main issue right now is when the same display currently actively streaming is requested to be switched to again (e.g. currently streaming display 0 and trying to switch to it via shortcut) then the stream will disconnect due to https://github.com/Kishi85/Sunshine/blob/6d7942e0c82aeedfca16b77735f0fc6cf716a4ec/src/platform/linux/portalgrab.cpp#L1294-L1309

which is causing the stream to disconnect with Warning: PipeWire stream stopped by user. in the logs.

Problem here is that this is necessary to detect if the user stopped the stream via the XDG notification (according to @psyke83 notes in #4914 (comment)).

Possible solutions that need more research:

  • Have the check detect if it is currently handling a switch_display_event. I've already had to extend it by adding the current stream position values to the mix so we can discern switching different screens without disconnecting.

  • Figure out another way to detect the user disconnecting the portal manually and change that part of the stream logic.

  • Update video.cpp to not reinit if switching to the same display currently active and just keep streaming (might have side-effects I can't assess).

@Kishi85 Kishi85 force-pushed the xdgportalgrab-better-multi-monitor-support branch from 6d7942e to 3a22dff Compare April 1, 2026 09:09
@Kishi85
Copy link
Copy Markdown
Contributor Author

Kishi85 commented Apr 1, 2026

The main issue right now is when the same display currently actively streaming is requested to be switched to again (e.g. currently streaming display 0 and trying to switch to it via shortcut) then the stream will disconnect due to https://github.com/Kishi85/Sunshine/blob/6d7942e0c82aeedfca16b77735f0fc6cf716a4ec/src/platform/linux/portalgrab.cpp#L1294-L1309

Changing the check to also include currently active switch_display_events directly works (not sure if this is the best way to do it):

auto switch_display_event = mail::man->event<int>(mail::switch_display);
if (previous_width.load() == width && previous_height.load() == height && previous_pos_x.load() == pos_x && previous_pos_y.load() == pos_y && switch_display_event->peek()) {

Next issue is when changing displays via shortcut too quickly there seems to be a possible race condition which can happen that freezes Sunshine hard until it is restarted and then it is exiting with: Fatal: 10 seconds passed, yet Sunshine's still running: Forcing shutdown or Fatal: Hang detected! Session failed to terminate in 10 seconds. when the session is closed on the client-side.

@Kishi85 Kishi85 force-pushed the xdgportalgrab-better-multi-monitor-support branch 2 times, most recently from e097695 to 79773f8 Compare April 1, 2026 10:15
@Kishi85
Copy link
Copy Markdown
Contributor Author

Kishi85 commented Apr 1, 2026

The main issue right now is when the same display currently actively streaming is requested to be switched to again (e.g. currently streaming display 0 and trying to switch to it via shortcut) then the stream will disconnect due to https://github.com/Kishi85/Sunshine/blob/6d7942e0c82aeedfca16b77735f0fc6cf716a4ec/src/platform/linux/portalgrab.cpp#L1294-L1309

Changing the check to also include currently active switch_display_events directly works (not sure if this is the best way to do it):

auto switch_display_event = mail::man->event<int>(mail::switch_display);
if (previous_width.load() == width && previous_height.load() == height && previous_pos_x.load() == pos_x && previous_pos_y.load() == pos_y && switch_display_event->peek()) {

Scratch that solution as I've had a typo (missing inversion) there and didn't realize that the event is already popped when the display gets reset by video.cpp and the portal re-initialzies. Back to square one on that.

Next issue is when changing displays via shortcut too quickly there seems to be a possible race condition which can happen that freezes Sunshine hard until it is restarted and then it is exiting with: Fatal: 10 seconds passed, yet Sunshine's still running: Forcing shutdown or Fatal: Hang detected! Session failed to terminate in 10 seconds. when the session is closed on the client-side.

Sunshine hanging when switching too quickly is still an issue.

UPDATE: This could be related to chosen encoder, having tried vulkan and vaapi yields different results (hang vs. SEGV).
After running with a debugger I'm seeing the SEGV in https://github.com/FFmpeg/FFmpeg/blob/15504610b0dc12c56e5e9f94ff06c873382368f5/libavcodec/hw_base_encode.c#L508 related to ctx being invalid/missing. Could we have an issue with switching from one pipewire_stream to another upon calling portal_t->init() multiple times? Simply adding a lock mutex for the whole init function does not seem to do the trick. It's likely somewhere in the capture or encoder logic.

UPDATE 2: More or less consistently reproducible when switching screens back and forth without waiting for the stream to update to the new screen. Does not seem to trigger if waiting for the stream to update to the new screen after switching plus a second or two more.

@Kishi85 Kishi85 force-pushed the xdgportalgrab-better-multi-monitor-support branch 2 times, most recently from 36c8067 to 073e73d Compare April 1, 2026 14:14
@psyke83
Copy link
Copy Markdown
Contributor

psyke83 commented Apr 1, 2026

The crashing (whether hang or segfault) may be due to the pipewire loop still being active during fake reinit.

See this block here:

if (stream_stopped.load()) {
BOOST_LOG(warning) << "PipeWire stream stopped by user."sv;
capture_running.store(false);
stream_stopped.store(false);
previous_height.store(0);
previous_width.store(0);
pipewire.frame_cv().notify_all();
return platf::capture_e::error;

And here:

case platf::capture_e::interrupted:
capture_running.store(false);
stream_stopped.store(false);
previous_height.store(0);
previous_width.store(0);
pipewire.frame_cv().notify_all();
return status;

You may also need to set these same variables when you intend to signal an artificial reinit (i.e., when push_captured_image_cb returns false) to ensure the encoder fully stops.

Regarding the race condition that causes a hang, one of the changes in #4875 resolves a hang after disconnect that happens when there are no screen updates happening. Since the on_process callback doesn't fire, the capture loop gets stuck. I worked around that by checking the pull_free_image_cb result during timeout, so you may want to see if you need to do a similar check elsewhere (or perhaps find a better fix).

@ReenigneArcher ReenigneArcher added this to the xdg portal grab milestone Apr 1, 2026
@Kishi85
Copy link
Copy Markdown
Contributor Author

Kishi85 commented Apr 2, 2026

I'll stop force-pushing most things im doing right now and will be squashing the whole thing in the end after we've got all the kinks ironed out. A few things I've done and learned now:

  • 9146245 Adds a prefix to all log messages from portalgrab.cpp so messages can be easily identified in the log (could do that as a separate PR as well if desired)
  • Added a lot more logging on what goes on with pipewire_t and the session cache. Things learned from that:
    • When video.cpp switches displays it calls reset_display() which in term resets the shared_ptr to the current display implicitly destroying pipewire_t and then reinitialzing a new display from scratch.
    • pipewire_t::cleanup_stream() always invalidates the session cache. Since it is called by the destructor of pipewire_t the session cache is never actually used? (c432cb6 fixes this without any noticable changes in behaviour)
  • 20af084 Temporarily disables the session ending logic via XDG notification icon
    • If you try to access an ended session pipewire returns a specific error Pipewire Error, id:2 seq:8 message: no target node available (causing a green screen on the connected client) that could be used to better track a user ending the session via the xdg notification.

With all these changes I seemingly have managed to get a stable reproducer for the crash (SEGV) by switching to the same display that is currently streaming. This is working on a single monitor setup as well, allowed by the implementation in video.cpp and should just fully reset the display (+pipewire stream) without segfaulting. The crash only occurs when switching to the same display. Switching to another display and then back to the first one does not seem to crash (as easily) unless you're switching very quickly but that might just be resulting in a switch to the same display internally in the end.

I'm wondering if this is related to the whole session caching (which is currently more or less defunct on master anyway) and will try to remove the whole session cache to see what happens when this is using a fresh portal every time a new display is constructed. video.cpp already ensures that only one capture is active (due to other capture methods limitations) so multiple XDG notifications should never be an issue in the first place Without session cache the client is just getting a greenscreen.

UPDATE: Testing this again and again leads to the same result: Segfault is only happening when trying to switch to the same screen that is already streaming even though the display reset/initialization looks the same in the log as when switching from one screen to another. It is properly reusing the session cache now and connecting to the same pipewire_node for each display. Could this be some quirk when repeatedly streaming the same node from pipewire? Switching to a different node and then back to the first one does not seem to trigger this issue.

@Kishi85 Kishi85 force-pushed the xdgportalgrab-better-multi-monitor-support branch 2 times, most recently from ed38479 to 12c20b7 Compare April 2, 2026 14:28
@Kishi85
Copy link
Copy Markdown
Contributor Author

Kishi85 commented Apr 2, 2026

After a lot of trial and error I've managed to cut the hangs when switching display down to only when very quickly, repeatedly switching (to the same? haven't tried others) display. You have to basically mash the shortcut to get a segfault. My theory for that is that when switching really quickly something in the capture logic combined with pipewire just cannot keep up (some thread sync issue?).

This is still an issue but I'm wondering if it can be mitigated in video.cpp as it seems unreasonable to allow queuing another switch_display_event while the previous one's reset_display has not finished:

Sunshine/src/video.cpp

Lines 1478 to 1484 in b172a98

// Process any pending display switch with the new list of displays
if (switch_display_event->peek()) {
display_p = std::clamp(*switch_display_event->pop(), 0, (int) display_names.size() - 1);
}
// reset_display() will sleep between retries
reset_display(disp, encoder.platform_formats->dev_type, display_names[display_p], capture_ctxs.front().config);

All other issues I've had with this are basically ironed out now:

  • Hanging issue for switching to the same display is (almost fully) solved.
  • XDG notification stream end is implemented via the associated pipewire error and not by matching screen properties
  • The session cache is now properly invalidated only when a display_name that is not in the list is requested. This can happen as display_names enumerate directly from a separate portal instance (so that is always the current state). As display_names are just position and resolution values as "x,y,w,h" portal->init matches pipewire_streams based on those. Also we do not have to invalidate the session cache for removed portals as the related streams will just stay in there unused.

Things still todo before I'm likely to remove the draft status:

  • Tone down logging to debug level for most of the newly added messages.

@Kishi85 Kishi85 force-pushed the xdgportalgrab-better-multi-monitor-support branch 3 times, most recently from aa2ff1f to b346dd1 Compare April 3, 2026 07:14
@Kishi85
Copy link
Copy Markdown
Contributor Author

Kishi85 commented Apr 3, 2026

Squashed, rebased and description+title updated. This is ready for review.

There's still one known issue (that was there before this PR just masked, see description for details) but maybe someone reviewing this has an idea on how to fix or work around that.

@Kishi85 Kishi85 marked this pull request as ready for review April 3, 2026 07:19
@Kishi85
Copy link
Copy Markdown
Contributor Author

Kishi85 commented Apr 5, 2026

Apologies, I won't have time to look at the code right now, but I did a quick test on Plasma 6.6.3 and found some issues.

  • In single display mode, resolutions switching is broken by this PR; seems to always trigger a green (vulkan) or black (vaapi) screen that requires a service restart to recover from.

I've managed to fix this issue by invalidating the session cache during portal_display_names enumeration (should live values using a new portal differ from the cache it will be invalidated and re-initialized during next portal_display).

  • In multi display mode with display 2 set to the left and (primary) display 1 to the right, changing the resolution of display 1 causes it to switch to display 2 on reinit.

Added a correlation of stream position/resolution to the monitors name by matching to wl::monitors() and if that provides a non-empty result then this value (prefixed with 'n') is used as the display name and later matched to. Should this fail then the old logic will be used and the displayname will be prefixed with a 'p' to clearly distinguish between both modes when matching the name back to a stream. The streams will still be sorted by position to have a consistent order.

Repeatedly testing both solutions has not shown any errors on my end.

@Kishi85 Kishi85 force-pushed the xdgportalgrab-better-multi-monitor-support branch 4 times, most recently from 4ba63a8 to 96bdcf1 Compare April 5, 2026 18:20
@psyke83
Copy link
Copy Markdown
Contributor

psyke83 commented Apr 6, 2026

Some notes on the latest PR code:

I'm testing on latest Arch, Plasma 6.6.3, running headless (DP-2 left, DP-1 set to primary on the right) via an EDID dump extracted from a HDMI dummy dongle inserted into the initramfs and using these kernel parameters to expose them to two DisplayPort outputs on my RX 6600:

drm.edid_firmware=DP-1:edid/HDMI4KHDR.edid,DP-2:edid/HDMI4KHDR.edid video=DP-1:e video=DP-2:e
  • Monitor switch is working great via the hotkeys.
  • The green/black screen is solved on resolution change, but...
  • When multiple screens are enabled, the stream is still starting on the wrong monitor (DP-2) and changing resolution of DP-1 always causes it to switch back to DP-2.
  • I don't think the monitor name is getting picked up on my configuration:
Apr 06 01:22:17 archlinux sunshine[14754]: [2026-04-06 01:22:17.880]: Info: [portalgrab] Loaded portal restore token from disk
Apr 06 01:22:17 archlinux sunshine[14754]: [2026-04-06 01:22:17.909]: Info: [portalgrab] Found stream for display: '' position: 0x0 resolution: 1536x864
Apr 06 01:22:17 archlinux sunshine[14754]: [2026-04-06 01:22:17.909]: Info: [portalgrab] Found stream for display: '' position: 1536x0 resolution: 1536x864
Apr 06 01:22:17 archlinux sunshine[14754]: [2026-04-06 01:22:17.910]: Info: [portalgrab] Loaded portal restore token from disk
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.028]: Info: [portalgrab] Requested frame rate [60/1, approx. 60 fps]
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.040]: Info: [portalgrab] Using first available stream as no matching stream was found for: ''
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.040]: Info: [portalgrab] Streaming display '' at position: 0x0 resolution: 1536x864
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.042]: Info: [portalgrab] Connected to pipewire version 1.6.2
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.045]: Info: [portalgrab] Video format: 12
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.045]: Info: [portalgrab] Size: 1920x1080
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.045]: Info: [portalgrab] Framerate (from compositor): 0/1 (variable rate capture)
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.045]: Info: [portalgrab] using DMA-BUF buffers
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.045]: Info: [portalgrab] Video format: 12
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.045]: Info: [portalgrab] Size: 1920x1080
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.045]: Info: [portalgrab] Framerate (from compositor): 0/1 (variable rate capture)
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.045]: Info: [portalgrab] using DMA-BUF buffers
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.052]: Info: [portalgrab] Using negotiated resolution 1920x1080
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.117]: Info: [portalgrab] Requested frame rate [60/1, approx. 60 fps]
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.126]: Info: [portalgrab] Using first available stream as no matching stream was found for: ''
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.126]: Info: [portalgrab] Streaming display '' at position: 0x0 resolution: 1536x864
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.128]: Info: [portalgrab] Connected to pipewire version 1.6.2
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.133]: Info: [portalgrab] Video format: 12
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.133]: Info: [portalgrab] Size: 1920x1080
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.133]: Info: [portalgrab] Framerate (from compositor): 0/1 (variable rate capture)
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.133]: Info: [portalgrab] using DMA-BUF buffers
Apr 06 01:22:18 archlinux sunshine[14754]: [2026-04-06 01:22:18.138]: Info: [portalgrab] Using negotiated resolution 1920x1080
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.070]: Info: [portalgrab] Requested frame rate [60/1, approx. 60 fps]
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.081]: Info: [portalgrab] Using first available stream as no matching stream was found for: ''
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.081]: Info: [portalgrab] Streaming display '' at position: 0x0 resolution: 1536x864
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.084]: Info: [portalgrab] Connected to pipewire version 1.6.2
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.088]: Info: [portalgrab] Video format: 12
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.088]: Info: [portalgrab] Size: 1920x1080
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.088]: Info: [portalgrab] Framerate (from compositor): 0/1 (variable rate capture)
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.088]: Info: [portalgrab] using DMA-BUF buffers
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.093]: Info: [portalgrab] Using negotiated resolution 1920x1080
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.158]: Info: [portalgrab] Requested frame rate [60/1, approx. 60 fps]
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.167]: Info: [portalgrab] Using first available stream as no matching stream was found for: ''
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.167]: Info: [portalgrab] Streaming display '' at position: 0x0 resolution: 1536x864
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.169]: Info: [portalgrab] Connected to pipewire version 1.6.2
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.174]: Info: [portalgrab] Video format: 12
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.174]: Info: [portalgrab] Size: 1920x1080
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.174]: Info: [portalgrab] Framerate (from compositor): 0/1 (variable rate capture)
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.174]: Info: [portalgrab] using DMA-BUF buffers
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.178]: Info: [portalgrab] Using negotiated resolution 1920x1080
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.256]: Info: [portalgrab] Loaded portal restore token from disk
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.278]: Info: [portalgrab] Found stream for display: '' position: 0x0 resolution: 1536x864
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.278]: Info: [portalgrab] Found stream for display: '' position: 1536x0 resolution: 1536x864
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.278]: Info: [portalgrab] Requested frame rate [60fps]
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.291]: Info: [portalgrab] Streaming display '' at position: 0x0 resolution: 1536x864
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.294]: Info: [portalgrab] Connected to pipewire version 1.6.2
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.298]: Info: [portalgrab] Video format: 12
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.298]: Info: [portalgrab] Size: 1920x1080
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.298]: Info: [portalgrab] Framerate (from compositor): 0/1 (variable rate capture)
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.298]: Info: [portalgrab] using DMA-BUF buffers
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.303]: Info: [portalgrab] Using negotiated resolution 1920x1080

Have you looked at the xdg_listener entries? The xdg_name seems to pick up stable connector names that can be used:

Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.277]: Info: [wayland] Name: DP-1
Apr 06 01:22:27 archlinux sunshine[14754]: [2026-04-06 01:22:27.277]: Info: [wayland] Name: DP-2

For context, the monitor name is also detected via wayland.cpp, but the string it prints is actually the description and not the name:

Apr 06 01:13:48 archlinux sunshine[10168]: [2026-04-06 01:13:48.463]: Info: [wayland] Found monitor: Hisense Electric Co., Ltd. HDMI4KHDR
Apr 06 01:13:48 archlinux sunshine[10168]: [2026-04-06 01:13:48.490]: Info: [wayland] Found monitor: Hisense Electric Co., Ltd. HDMI4KHDR

Edit: I see that name and description are considered deprecated, so wl_output should by used instead. But it seems that something is not working correctly with the monitor name detection implemented in portalgrab for my configuration.

@Kishi85
Copy link
Copy Markdown
Contributor Author

Kishi85 commented Apr 6, 2026

Edit: I see that name and description are considered deprecated, so wl_output should by used instead. But it seems that something is not working correctly with the monitor name detection implemented in portalgrab for my configuration.

From your logs it seems that the correlation with wl::monitors() does not work as you're getting no matching names back:

Apr 06 01:22:17 archlinux sunshine[14754]: [2026-04-06 01:22:17.909]: Info: [portalgrab] Found stream for display: '' position: 0x0 resolution: 1536x864
Apr 06 01:22:17 archlinux sunshine[14754]: [2026-04-06 01:22:17.909]: Info: [portalgrab] Found stream for display: '' position: 1536x0 resolution: 1536x864

Then it's falling back to position/resolution matching and the already known behaviour. Could you check if in the wayland output just before those lines the viewport information is matching to the screen position/resolution information you see for the pipewire streams found?

For comparison here's a working example (with bold/italic highlights) using my config (also Arch-based but CachyOS with Plasma 6.6.3, Fake-EDID is on HDMI-A-1 of my RX9070XT and taken from https://github.com/linuxhw/EDID, DP-1/2 are real monitors with DP-1 mirrored to HDMI-A-1 therefore not in the list):

[2026-04-06 07:28:40.719]: Info: [wayland] Offset: 1920x0
[2026-04-06 07:28:40.719]: Info: [wayland] Logical size: 1920x1080
[2026-04-06 07:28:40.719]: Info: [wayland] Name: DP-2
[2026-04-06 07:28:40.719]: Info: [wayland] Found monitor: Samsung Electric Company LS24AG30x
[2026-04-06 07:28:40.719]: Info: [wayland] Offset: 0x0
[2026-04-06 07:28:40.719]: Info: [wayland] Logical size: 1920x1080
[2026-04-06 07:28:40.719]: Info: [wayland] Name: HDMI-A-1
[2026-04-06 07:28:40.719]: Info: [wayland] Found monitor: BNQ BenQ XL2411Z
[2026-04-06 07:28:40.720]: Info: [portalgrab] Found stream for display: 'HDMI-A-1' position: 0x0 resolution: 1920x1080
[2026-04-06 07:28:40.720]: Info: [portalgrab] Found stream for display: 'DP-2' position: 1920x0 resolution: 1920x1080

The name correlation code (see https://github.com/Kishi85/Sunshine/blob/96bdcf1d1fc893aab4e95415b7402f9f1c2be3b9/src/platform/linux/portalgrab.cpp#L585-L609) is just reusing data from the same code that kmsgrab is during it's monitor correlation:

std::vector<std::unique_ptr<monitor_t>> monitors(const char *display_name) {

Correlation is done during start_portal_session so the both information sets (wl::monitors() and listed portal streams) are sure to match as close as possible. It's using the (connector) name of monitor_t (stored as part of pipewire_streaminfo_t) as it is a unique identifier per system to match streams to later with a fallback to position/resolution matching in case the name is empty.

@psyke83 It's interesting to see that the correlation does not work on your setup and I'm curious to see if matching up wayland log output to the streams found can help identify the issue here.

@Kishi85 Kishi85 force-pushed the xdgportalgrab-better-multi-monitor-support branch from c51bf18 to 61d9138 Compare April 6, 2026 07:15
@psyke83
Copy link
Copy Markdown
Contributor

psyke83 commented Apr 6, 2026

I'll provide some more detailed logs when I have access to the host pc later today.

Regarding the default monitor selection, KDE has a dbus call you can use to get the active output display:

qdbus org.kde.KWin /KWin org.kde.KWin.activeOutputName

I can't verify right now, but some Googling shows a similar method for GNOME: https://wiki.gnome.org/Initiatives(2f)Wayland(2f)Gaps(2f)DisplayConfig.html (there's a primary bool in the properties array).

I'm not saying that you have to implement these (as it seems quite annoying to tailor detection to specific compositors), but I'm not aware of any generic methods to get the primary (or even just currently active) screen on Wayland.

@psyke83
Copy link
Copy Markdown
Contributor

psyke83 commented Apr 6, 2026

Perhaps we shouldn't worry about the primary monitor, but instead try to determine the active monitor from the pointer position provided by spa_meta_cursor metadata? From that we may be able to determine the position data and which screen it is currently active on.

@Kishi85
Copy link
Copy Markdown
Contributor Author

Kishi85 commented Apr 6, 2026

I'll provide some more detailed logs when I have access to the host pc later today.

Regarding the default monitor selection, KDE has a dbus call you can use to get the active output display:

qdbus org.kde.KWin /KWin org.kde.KWin.activeOutputName

I can't verify right now, but some Googling shows a similar method for GNOME: https://wiki.gnome.org/Initiatives(2f)Wayland(2f)Gaps(2f)DisplayConfig.html (there's a primary bool in the properties array).

It's not just a bool if you're using more than 2 screens, then there's a whole screen priority list of monitors that you can modify (at least in KDE display settings) and the first in the list is considered primary.

I'm not saying that you have to implement these (as it seems quite annoying to tailor detection to specific compositors), but I'm not aware of any generic methods to get the primary (or even just currently active) screen on Wayland.

I agree that using screen priorites would be good way to sort streams but is only feasible if we can leverage wayland.h/cpp to get those priorities from a standardized interface. I'll have a look into it when I find some time.

But even if possible that should be done in a separate PR. It's big enough as it is now and I'd like to fix any remaining issues first and get this PR merged before improving on functionality further.

@Kishi85
Copy link
Copy Markdown
Contributor Author

Kishi85 commented Apr 6, 2026

Perhaps we shouldn't worry about the primary monitor, but instead try to determine the active monitor from the pointer position provided by spa_meta_cursor metadata? From that we may be able to determine the position data and which screen it is currently active on.

Not sure why you would want to add pointer correlation here?

The main problem is that video.cpp is doing the switching based on the display_name selected by index based on the F-key you press from the display_names vector. In case of a capture re-init due to resolution change it'll try to find the last selected display_name from the new display_names vector and if that fails it falls back with the first element of said vector. To make this work on resolution change we need to use a resolution/position independent display_name like the (connector) name from wl::monitors().
That also means you have to do the correlation for all entries/streams that portal_display_names can return every time the portal session is started. Otherwise the video.cpp will not be able to find the last display_name and switch to it again causing the fallback.

EDIT: To clarify what this PR does is that it provides all portal_display_names for video.cpp so that can call portal_display with the name it intends to switch to and initialize the correct stream for what is requested. The whole logic as to what is switched to is done by video.cpp. It also ensures things don't break by destroying the old display before it even enumerates display_names again and then initializing a new display based on what shortcut is pressed. Similar things happen for a capture::re-init due to resolution change. See video.cpp for details.

Kishi85 added 3 commits April 6, 2026 13:49
…iffer from dbus during display names retrieval

Fix capture issues (black- or greenscreen) when changing resolution due problems
when doing session cache invalidation during portal display init. The pipewire
stream is working properly when session cache check and invalidation are done
during portal_display_names while discovering available displays, so do it there.
Also make sure pipewire_fd opened by dbus_t are closed by it.
… usage

pipewire_screenstream_t -> pipewire_streaminfo_t as it is only
containing the metadata necessary to identify the stream

streams_to_display_names -> portal_streams_to_display_names so
it can be easily identified as portal related
@Kishi85 Kishi85 force-pushed the xdgportalgrab-better-multi-monitor-support branch from 61d9138 to 5aec74c Compare April 6, 2026 11:51
@Kishi85
Copy link
Copy Markdown
Contributor Author

Kishi85 commented Apr 6, 2026

I've taken the time to cleanup the commits a bit and update a few commit messages with more detail to make them easier to understand. I can do another squash if necessary before this gets merged and/or put 69c05b1 into a separate PR.

@psyke83
Copy link
Copy Markdown
Contributor

psyke83 commented Apr 6, 2026

Here's a full log taken from the latest PR changes:

[2026-04-06 16:37:57.387]: Info: Package Publisher: LizardByte
[2026-04-06 16:37:57.387]: Info: Publisher Website: https://app.lizardbyte.dev
[2026-04-06 16:37:57.387]: Info: Get support: https://app.lizardbyte.dev/support
[2026-04-06 16:37:57.387]: Info: config: 'vaapi_strict_rc_buffer' = enabled
[2026-04-06 16:37:57.387]: Info: config: 'min_threads' = 2
[2026-04-06 16:37:57.387]: Info: config: 'system_tray' = disabled
[2026-04-06 16:37:57.387]: Info: config: 'vk_rc_mode' = 4
[2026-04-06 16:37:57.387]: Info: config: 'minimum_fps_target' = 1
[2026-04-06 16:37:57.387]: Info: config: 'encoder' = vulkan
[2026-04-06 16:37:57.387]: Info: config: 'capture' = portal
[2026-04-06 16:37:57.388]: Info: [portalgrab] Loaded portal restore token from disk
[2026-04-06 16:37:57.408]: Info: [wayland] Found display [wayland-0]
[2026-04-06 16:37:57.410]: Info: [wayland] Found interface: zxdg_output_manager_v1(31) version 3
[2026-04-06 16:37:57.410]: Info: [wayland] Found interface: zwp_linux_dmabuf_v1(57) version 5
[2026-04-06 16:37:57.410]: Info: [wayland] Found interface: wl_output(65) version 4
[2026-04-06 16:37:57.410]: Info: [wayland] Found interface: wl_output(66) version 4
[2026-04-06 16:37:57.410]: Info: [wayland] Resolution: 1920x1080
[2026-04-06 16:37:57.410]: Info: [wayland] Resolution: 1920x1080
[2026-04-06 16:37:57.410]: Info: [wayland] Offset: 1536x0
[2026-04-06 16:37:57.410]: Info: [wayland] Logical size: 1536x864
[2026-04-06 16:37:57.410]: Info: [wayland] Name: DP-1
[2026-04-06 16:37:57.410]: Info: [wayland] Found monitor: Hisense Electric Co., Ltd. HDMI4KHDR
[2026-04-06 16:37:57.410]: Info: [wayland] Offset: 0x0
[2026-04-06 16:37:57.410]: Info: [wayland] Logical size: 1536x864
[2026-04-06 16:37:57.410]: Info: [wayland] Name: DP-2
[2026-04-06 16:37:57.410]: Info: [wayland] Found monitor: Hisense Electric Co., Ltd. HDMI4KHDR
[2026-04-06 16:37:57.411]: Info: [portalgrab] Found stream for display: '' position: 0x0 resolution: 1536x864
[2026-04-06 16:37:57.411]: Info: [portalgrab] Found stream for display: '' position: 1536x0 resolution: 1536x864
[2026-04-06 16:37:57.412]: Info: [portalgrab] Loaded portal restore token from disk
[2026-04-06 16:37:57.428]: Info: [wayland] Found display [wayland-0]
[2026-04-06 16:37:57.429]: Info: [wayland] Found interface: zxdg_output_manager_v1(31) version 3
[2026-04-06 16:37:57.429]: Info: [wayland] Found interface: zwp_linux_dmabuf_v1(57) version 5
[2026-04-06 16:37:57.429]: Info: [wayland] Found interface: wl_output(65) version 4
[2026-04-06 16:37:57.429]: Info: [wayland] Found interface: wl_output(66) version 4
[2026-04-06 16:37:57.429]: Info: [wayland] Resolution: 1920x1080
[2026-04-06 16:37:57.429]: Info: [wayland] Resolution: 1920x1080
[2026-04-06 16:37:57.429]: Info: [wayland] Offset: 1536x0
[2026-04-06 16:37:57.429]: Info: [wayland] Logical size: 1536x864
[2026-04-06 16:37:57.429]: Info: [wayland] Name: DP-1
[2026-04-06 16:37:57.429]: Info: [wayland] Found monitor: Hisense Electric Co., Ltd. HDMI4KHDR
[2026-04-06 16:37:57.429]: Info: [wayland] Offset: 0x0
[2026-04-06 16:37:57.429]: Info: [wayland] Logical size: 1536x864
[2026-04-06 16:37:57.429]: Info: [wayland] Name: DP-2
[2026-04-06 16:37:57.429]: Info: [wayland] Found monitor: Hisense Electric Co., Ltd. HDMI4KHDR
[2026-04-06 16:37:57.526]: Info: Trying encoder [vulkan]
[2026-04-06 16:37:57.526]: Info: Screencasting with XDG portal
[2026-04-06 16:37:57.526]: Info: [portalgrab] Requested frame rate [60/1, approx. 60 fps]
[2026-04-06 16:37:57.526]: Info: [wayland] Found display [wayland-0]
[2026-04-06 16:37:57.538]: Info: [portalgrab] Using first available stream as no matching stream was found for: ''
[2026-04-06 16:37:57.538]: Info: [portalgrab] Streaming display '' at position: 0x0 resolution: 1536x864
[2026-04-06 16:37:57.539]: Info: [portalgrab] Connected to pipewire version 1.6.2
[2026-04-06 16:37:57.542]: Info: [portalgrab] Video format: 12
[2026-04-06 16:37:57.542]: Info: [portalgrab] Size: 1920x1080
[2026-04-06 16:37:57.542]: Info: [portalgrab] Framerate (from compositor): 0/1 (variable rate capture)
[2026-04-06 16:37:57.542]: Info: [portalgrab] using DMA-BUF buffers
[2026-04-06 16:37:57.542]: Info: [portalgrab] Video format: 12
[2026-04-06 16:37:57.542]: Info: [portalgrab] Size: 1920x1080
[2026-04-06 16:37:57.542]: Info: [portalgrab] Framerate (from compositor): 0/1 (variable rate capture)
[2026-04-06 16:37:57.542]: Info: [portalgrab] using DMA-BUF buffers
[2026-04-06 16:37:57.549]: Info: [portalgrab] Using negotiated resolution 1920x1080
[2026-04-06 16:37:57.549]: Info: Creating encoder [h264_vulkan]
[2026-04-06 16:37:57.549]: Info: Color coding: SDR (Rec. 601)
[2026-04-06 16:37:57.549]: Info: Color depth: 8-bit
[2026-04-06 16:37:57.549]: Info: Color range: JPEG
[2026-04-06 16:37:57.569]: Info: Streaming bitrate is 1000000
[2026-04-06 16:37:57.569]: Info: Vulkan encode using GPU: AMD Radeon RX 6600 (RADV NAVI23)
[2026-04-06 16:37:57.588]: Info: Creating encoder [hevc_vulkan]
[2026-04-06 16:37:57.588]: Info: Color coding: SDR (Rec. 601)
[2026-04-06 16:37:57.588]: Info: Color depth: 8-bit
[2026-04-06 16:37:57.588]: Info: Color range: JPEG
[2026-04-06 16:37:57.603]: Info: Streaming bitrate is 1000000
[2026-04-06 16:37:57.605]: Info: Vulkan encode using GPU: AMD Radeon RX 6600 (RADV NAVI23)
[2026-04-06 16:37:57.622]: Info: Creating encoder [av1_vulkan]
[2026-04-06 16:37:57.622]: Info: Color coding: SDR (Rec. 601)
[2026-04-06 16:37:57.622]: Info: Color depth: 8-bit
[2026-04-06 16:37:57.622]: Info: Color range: JPEG
[2026-04-06 16:37:57.636]: Info: Streaming bitrate is 1000000
[2026-04-06 16:37:57.636]: Error: [av1_vulkan @ 0x564cc3ff6640] Device does not support encoding av1!
[2026-04-06 16:37:57.642]: Error: Could not open codec [av1_vulkan]: Function not implemented
[2026-04-06 16:37:57.642]: Info: Screencasting with XDG portal
[2026-04-06 16:37:57.642]: Info: [portalgrab] Requested frame rate [60/1, approx. 60 fps]
[2026-04-06 16:37:57.642]: Info: [wayland] Found display [wayland-0]
[2026-04-06 16:37:57.652]: Info: [portalgrab] Using first available stream as no matching stream was found for: ''
[2026-04-06 16:37:57.652]: Info: [portalgrab] Streaming display '' at position: 0x0 resolution: 1536x864
[2026-04-06 16:37:57.654]: Info: [portalgrab] Connected to pipewire version 1.6.2
[2026-04-06 16:37:57.657]: Info: [portalgrab] Video format: 12
[2026-04-06 16:37:57.657]: Info: [portalgrab] Size: 1920x1080
[2026-04-06 16:37:57.657]: Info: [portalgrab] Framerate (from compositor): 0/1 (variable rate capture)
[2026-04-06 16:37:57.657]: Info: [portalgrab] using DMA-BUF buffers
[2026-04-06 16:37:57.663]: Info: [portalgrab] Using negotiated resolution 1920x1080
[2026-04-06 16:37:57.663]: Info: Creating encoder [hevc_vulkan]
[2026-04-06 16:37:57.663]: Info: Color coding: SDR (Rec. 709)
[2026-04-06 16:37:57.663]: Info: Color depth: 10-bit
[2026-04-06 16:37:57.663]: Info: Color range: JPEG
[2026-04-06 16:37:57.679]: Info: Streaming bitrate is 1000000
[2026-04-06 16:37:57.681]: Info: Vulkan encode using GPU: AMD Radeon RX 6600 (RADV NAVI23)
[2026-04-06 16:37:57.690]: Info: // Testing for available encoders, this may generate errors. You can safely ignore those errors. //
[2026-04-06 16:37:57.690]: Info: 
[2026-04-06 16:37:57.690]: Info: // Ignore any errors mentioned above, they are not relevant. //
[2026-04-06 16:37:57.690]: Info: 
[2026-04-06 16:37:57.690]: Info: Found H.264 encoder: h264_vulkan [vulkan]
[2026-04-06 16:37:57.690]: Info: Found HEVC encoder: hevc_vulkan [vulkan]
[2026-04-06 16:37:57.690]: Info: No main thread features enabled, skipping event loop
[2026-04-06 16:37:57.691]: Error: Failed to create client: Daemon not running
[2026-04-06 16:37:57.691]: Info: Configuration UI available at [https://localhost:47990]
[2026-04-06 16:38:05.683]: Info: Trying encoder [vulkan]
[2026-04-06 16:38:05.683]: Info: Screencasting with XDG portal
[2026-04-06 16:38:05.683]: Info: [portalgrab] Requested frame rate [60/1, approx. 60 fps]
[2026-04-06 16:38:05.683]: Info: [wayland] Found display [wayland-0]
[2026-04-06 16:38:05.694]: Info: [portalgrab] Using first available stream as no matching stream was found for: ''
[2026-04-06 16:38:05.694]: Info: [portalgrab] Streaming display '' at position: 0x0 resolution: 1536x864
[2026-04-06 16:38:05.695]: Info: [portalgrab] Connected to pipewire version 1.6.2
[2026-04-06 16:38:05.699]: Info: [portalgrab] Video format: 12
[2026-04-06 16:38:05.699]: Info: [portalgrab] Size: 1920x1080
[2026-04-06 16:38:05.699]: Info: [portalgrab] Framerate (from compositor): 0/1 (variable rate capture)
[2026-04-06 16:38:05.699]: Info: [portalgrab] using DMA-BUF buffers
[2026-04-06 16:38:05.705]: Info: [portalgrab] Using negotiated resolution 1920x1080
[2026-04-06 16:38:05.705]: Info: Creating encoder [h264_vulkan]
[2026-04-06 16:38:05.705]: Info: Color coding: SDR (Rec. 601)
[2026-04-06 16:38:05.705]: Info: Color depth: 8-bit
[2026-04-06 16:38:05.705]: Info: Color range: JPEG
[2026-04-06 16:38:05.720]: Info: Streaming bitrate is 1000000
[2026-04-06 16:38:05.721]: Info: Vulkan encode using GPU: AMD Radeon RX 6600 (RADV NAVI23)
[2026-04-06 16:38:05.739]: Info: Creating encoder [hevc_vulkan]
[2026-04-06 16:38:05.739]: Info: Color coding: SDR (Rec. 601)
[2026-04-06 16:38:05.739]: Info: Color depth: 8-bit
[2026-04-06 16:38:05.739]: Info: Color range: JPEG
[2026-04-06 16:38:05.755]: Info: Streaming bitrate is 1000000
[2026-04-06 16:38:05.756]: Info: Vulkan encode using GPU: AMD Radeon RX 6600 (RADV NAVI23)
[2026-04-06 16:38:05.772]: Info: Creating encoder [av1_vulkan]
[2026-04-06 16:38:05.772]: Info: Color coding: SDR (Rec. 601)
[2026-04-06 16:38:05.772]: Info: Color depth: 8-bit
[2026-04-06 16:38:05.772]: Info: Color range: JPEG
[2026-04-06 16:38:05.786]: Info: Streaming bitrate is 1000000
[2026-04-06 16:38:05.787]: Error: [av1_vulkan @ 0x7fd764207000] Device does not support encoding av1!
[2026-04-06 16:38:05.792]: Error: Could not open codec [av1_vulkan]: Function not implemented
[2026-04-06 16:38:05.793]: Info: Screencasting with XDG portal
[2026-04-06 16:38:05.793]: Info: [portalgrab] Requested frame rate [60/1, approx. 60 fps]
[2026-04-06 16:38:05.793]: Info: [wayland] Found display [wayland-0]
[2026-04-06 16:38:05.803]: Info: [portalgrab] Using first available stream as no matching stream was found for: ''
[2026-04-06 16:38:05.803]: Info: [portalgrab] Streaming display '' at position: 0x0 resolution: 1536x864
[2026-04-06 16:38:05.805]: Info: [portalgrab] Connected to pipewire version 1.6.2
[2026-04-06 16:38:05.808]: Info: [portalgrab] Video format: 12
[2026-04-06 16:38:05.808]: Info: [portalgrab] Size: 1920x1080
[2026-04-06 16:38:05.808]: Info: [portalgrab] Framerate (from compositor): 0/1 (variable rate capture)
[2026-04-06 16:38:05.808]: Info: [portalgrab] using DMA-BUF buffers
[2026-04-06 16:38:05.815]: Info: [portalgrab] Using negotiated resolution 1920x1080
[2026-04-06 16:38:05.815]: Info: Creating encoder [hevc_vulkan]
[2026-04-06 16:38:05.815]: Info: Color coding: SDR (Rec. 709)
[2026-04-06 16:38:05.815]: Info: Color depth: 10-bit
[2026-04-06 16:38:05.815]: Info: Color range: JPEG
[2026-04-06 16:38:05.829]: Info: Streaming bitrate is 1000000
[2026-04-06 16:38:05.830]: Info: Vulkan encode using GPU: AMD Radeon RX 6600 (RADV NAVI23)
[2026-04-06 16:38:05.851]: Info: // Testing for available encoders, this may generate errors. You can safely ignore those errors. //
[2026-04-06 16:38:05.851]: Info: 
[2026-04-06 16:38:05.851]: Info: // Ignore any errors mentioned above, they are not relevant. //
[2026-04-06 16:38:05.851]: Info: 
[2026-04-06 16:38:05.851]: Info: Found H.264 encoder: h264_vulkan [vulkan]
[2026-04-06 16:38:05.851]: Info: Found HEVC encoder: hevc_vulkan [vulkan]
[2026-04-06 16:38:05.851]: Info: Executing [Desktop]
[2026-04-06 16:38:05.892]: Info: New streaming session started [active sessions: 1]
[2026-04-06 16:38:05.903]: Info: CLIENT CONNECTED
[2026-04-06 16:38:05.905]: Info: [portalgrab] Loaded portal restore token from disk
[2026-04-06 16:38:05.924]: Info: [wayland] Found display [wayland-0]
[2026-04-06 16:38:05.926]: Info: [wayland] Found interface: zxdg_output_manager_v1(31) version 3
[2026-04-06 16:38:05.926]: Info: [wayland] Found interface: zwp_linux_dmabuf_v1(57) version 5
[2026-04-06 16:38:05.926]: Info: [wayland] Found interface: wl_output(65) version 4
[2026-04-06 16:38:05.926]: Info: [wayland] Found interface: wl_output(66) version 4
[2026-04-06 16:38:05.926]: Info: [wayland] Resolution: 1920x1080
[2026-04-06 16:38:05.926]: Info: [wayland] Resolution: 1920x1080
[2026-04-06 16:38:05.926]: Info: [wayland] Offset: 1536x0
[2026-04-06 16:38:05.926]: Info: [wayland] Logical size: 1536x864
[2026-04-06 16:38:05.926]: Info: [wayland] Name: DP-1
[2026-04-06 16:38:05.926]: Info: [wayland] Found monitor: Hisense Electric Co., Ltd. HDMI4KHDR
[2026-04-06 16:38:05.926]: Info: [wayland] Offset: 0x0
[2026-04-06 16:38:05.926]: Info: [wayland] Logical size: 1536x864
[2026-04-06 16:38:05.926]: Info: [wayland] Name: DP-2
[2026-04-06 16:38:05.926]: Info: [wayland] Found monitor: Hisense Electric Co., Ltd. HDMI4KHDR
[2026-04-06 16:38:05.927]: Info: [portalgrab] Found stream for display: '' position: 0x0 resolution: 1536x864
[2026-04-06 16:38:05.927]: Info: [portalgrab] Found stream for display: '' position: 1536x0 resolution: 1536x864
[2026-04-06 16:38:05.927]: Info: Screencasting with XDG portal
[2026-04-06 16:38:05.927]: Info: [portalgrab] Requested frame rate [60fps]
[2026-04-06 16:38:05.927]: Info: [wayland] Found display [wayland-0]
[2026-04-06 16:38:05.938]: Info: [portalgrab] Streaming display '' at position: 0x0 resolution: 1536x864
[2026-04-06 16:38:05.939]: Info: [portalgrab] Connected to pipewire version 1.6.2
[2026-04-06 16:38:05.943]: Info: [portalgrab] Video format: 12
[2026-04-06 16:38:05.943]: Info: [portalgrab] Size: 1920x1080
[2026-04-06 16:38:05.943]: Info: [portalgrab] Framerate (from compositor): 0/1 (variable rate capture)
[2026-04-06 16:38:05.943]: Info: [portalgrab] using DMA-BUF buffers
[2026-04-06 16:38:05.949]: Info: [portalgrab] Using negotiated resolution 1920x1080
[2026-04-06 16:38:05.949]: Info: Creating encoder [hevc_vulkan]
[2026-04-06 16:38:05.949]: Info: Color coding: SDR (Rec. 601)
[2026-04-06 16:38:05.949]: Info: Color depth: 8-bit
[2026-04-06 16:38:05.949]: Info: Color range: MPEG
[2026-04-06 16:38:05.966]: Info: Streaming bitrate is 59788000
[2026-04-06 16:38:05.968]: Info: Vulkan encode using GPU: AMD Radeon RX 6600 (RADV NAVI23)
[2026-04-06 16:38:05.968]: Info: Minimum FPS target set to ~0.5fps (2000ms)
[2026-04-06 16:38:06.386]: Info: Setting default sink to: [sink-sunshine-stereo]
[2026-04-06 16:38:06.386]: Info: Found default monitor by name: sink-sunshine-stereo.monitor
[2026-04-06 16:38:06.402]: Info: Opus initialized: 48 kHz, 2 channels, 512 kbps (total), LOWDELAY

My comments regarding the pointer location was aimed at figuring out which monitor is to be selected on first connect and persisted on resolution change. Perhaps that won't be needed when the display names are detected correctly? As-is, my setup is still not detecting the name correctly, defaults to the wrong monitor (DP-2), and resolution change on DP-1 still causes it to switch to DP-2.

Kishi85 added 2 commits April 6, 2026 19:27
…hat as display_name

To ensure display_name is independent of postion/resolution so swichting
to the same display on resolution change based re-init works properly.
On failing correlation fall back to position/resolution matching for a
stream to have at least basic display switching working.

To have uniquely distinguishable display_names prefix them with 'n' for
monitor_name matching and 'p' for position/resolution matching.
…pipewire_streaminfo_t

Reduce complexity in other code parts and have the display_name generation/matching done next to each other for better maintainability.
@Kishi85 Kishi85 force-pushed the xdgportalgrab-better-multi-monitor-support branch from 5aec74c to be75524 Compare April 6, 2026 17:27
@Kishi85
Copy link
Copy Markdown
Contributor Author

Kishi85 commented Apr 6, 2026

The wl::monitors() correlation wasn't using logical_width/logical_height to match against so scaled displays wouldn't work correctly but after seeing @psyke83's first comment I've started looking and in the meantime we both came up with the same solution. So this is fixed now as well.

The only remaining feature that would be nice to have is to do stream sorting additionally based on screen priorities (mainly to have the stream start on the primary screen) but that'll need changes to wayland.cpp/h to expose the necessary information (mainly the preferred/primary indicator). I'll try to implement this but might move it to a follow-up PR if it's a larger change as this PR is already doing quite a lot of things (although all are related to and necessary to make this feature work properly).

UPDATE: From looking at other capture methods it looks like those have similar sorting issues (e.g. wlgrab takes the displays in the order that wl::monitors() provides them, which seems to be sorted alphabetically by wl_name). There also does not seem to be a unified Wayland interface for screen priorities/primary monitor (at least I've not found one so far). Adding sorting based on additional parameters is trivial but we have to get those parameters somehow and that's IMHO something for a follow-up PR as it will be a bit more involved.

@psyke83
Copy link
Copy Markdown
Contributor

psyke83 commented Apr 6, 2026

I agree with your assessment that primary/active detection is out of scope. I don't think Wayland has a direct method to probe the primary monitor, hence the suggestion to query the pipewire cursor position. But if it is feasible to do generically via Wayland, it belongs in a separate PR as it that could also be leveraged by kmsgrab. Until then, users can just arrange their displays so that the primary is leftmost.

I'm seeing an issue with the xdg stop event. When stopping the stream via the xdg notification icon, it can't resume the session. Log is here; happens on KDE and GNOME.

@Kishi85
Copy link
Copy Markdown
Contributor Author

Kishi85 commented Apr 6, 2026

I'm seeing an issue with the xdg stop event. When stopping the stream via the xdg notification icon, it can't resume the session. Log is here; happens on KDE and GNOME.

This is a tricky one and I might have an idea on how to handle this. I'll try to explain the issue here, maybe you have an idea:

  • The way of handling this was to check if current stream resolution and previous stream resolution match but that would break things here as video.cpp allows you to switch to the same screen (basically doing a re-init of the portal_display).

  • Currently its checking if the pipewire stream errors out with "no target node" and stores that state in the session_cache. Stopping the stream and subsequent display init's until the session_cache invalidates (and the portal re-initialises). Not ideal but it's what I've come up with so far.

  • Ideally we could leverage the XDG stop signal from the Screencast.Session (https://github.com/flatpak/xdg-desktop-portal/blob/845290fb539e48adcbe2610c73078cb9f1e9d732/data/org.freedesktop.portal.ScreenCast.xml#L36-L39) and instead of doing a capture::re-init for that stream error/pause case just error out once, like is done for stream_stopped (which could IMHO be fully removed if that works). I haven't had time to look into this, but doing it that way seems to be the most correct way.

@Kishi85
Copy link
Copy Markdown
Contributor Author

Kishi85 commented Apr 6, 2026

@psyke83 I've changed the current implementation of XDG stop and made it so that it invalidates the session_cache after the capture errors out, making it possible to connect again without restarting sunshine. It's not the ideal solution I've described but should fix the immediate problem you noted.

@Kishi85 Kishi85 force-pushed the xdgportalgrab-better-multi-monitor-support branch from 70a8637 to 78020a2 Compare April 7, 2026 06:04
…validate session_cache

This should fix the permanent error after closing the stream by ending it
using the XDG portal notification until we can implement the XDG stop event properly.
@Kishi85 Kishi85 force-pushed the xdgportalgrab-better-multi-monitor-support branch from 78020a2 to a404fd5 Compare April 7, 2026 06:08
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 7, 2026

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XDG portalgrab with multiple displays squishes all into one stream

3 participants