fix(kwin): drive display topology over kde_output_management_v2, not kscreen-doctor

A KDE host on Nobara stopped disabling its physical screens and creating the
virtual output the moment it updated: streaming still came up, but bring-up took
~26 s and the streamed output never became the desktop (`also_disabled=[]`).

Root cause is not our topology logic — it's that every `kscreen-doctor` call on
the reporter's session wedges. kscreen-doctor drives libkscreen, which (per
setup) waits on the kscreen KDED module over D-Bus; when that layer is stuck it
blocks in its own connect and never returns, so all five topology queries hit
their 5 s budget and got killed (host log). Reproduced live on a Nobara / KWin
6.6.4 box: `kscreen-doctor -j` there times out at 8 s, every time.

But the compositor's OWN Wayland is fully responsive on that same session — the
host just created a virtual output over it via zkde_screencast. So drive the
topology (resolve our output, take primary, disable the physical/bootstrap
outputs, capture their modes, re-enable on teardown, position) directly over
`kde_output_management_v2` + `kde_output_device_v2` instead of shelling out.
On that same Nobara box the in-process path binds management (v19) and
enumerates the outputs in 2.4 ms — ~3400x faster than the 8 s hang, and immune
to whatever wedges the standalone tool.

- vendor kde-output-management-v2 / kde-output-device-v2 (KWin advertises mgmt
  v19, device v20); generate client bindings inline (the interdependent-protocol
  module split from the wayland-protocols crate; needs the `bitflags` dep for the
  device protocol's bitfield enums).
- new `kwin_output_mgmt`: bounded enumerate-then-apply over one Wayland
  connection; every wait is time-bounded so a genuinely wedged compositor
  degrades to `handled = false` and the old kscreen-doctor path still runs.
- `kwin::create` topology + `apply_position` prefer the in-process path (address
  our output by its stable device UUID, supersede-robust) with kscreen-doctor as
  the fallback. The 60 Hz path now makes ZERO kscreen-doctor calls; only the
  >60 Hz custom-mode install still shells out (its in-process port is a follow-up).

Verified on Linux: cargo test -p pf-vdisplay (73 pass), clippy --all-targets
-D warnings clean, fmt clean, punktfunk-host checks clean. In-process enumeration
proven live against KWin 6.6.4 (the reporter's env); the disable/apply path is
not yet on-glass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-24 12:47:33 +02:00
co-authored by Claude Opus 4.8
parent a3e21d9227
commit 86d5d66660
7 changed files with 1955 additions and 32 deletions
Generated
+1
View File
@@ -3024,6 +3024,7 @@ version = "0.19.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ashpd", "ashpd",
"bitflags",
"bytemuck", "bytemuck",
"futures-util", "futures-util",
"hex", "hex",
+3
View File
@@ -45,6 +45,9 @@ futures-util = "0.3"
wayland-client = "0.31" wayland-client = "0.31"
wayland-scanner = "0.31" wayland-scanner = "0.31"
wayland-backend = "0.3" wayland-backend = "0.3"
# wayland-scanner emits `bitflags::bitflags!` for the KDE output-device protocol's bitfield enums
# (kde-output-device-v2 `capability`/`flags`); needs the crate in scope (kwin_output_mgmt.rs).
bitflags = "2"
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
# The host<->driver wire contract for the pf-vdisplay IddCx backend (control IOCTLs + Pod structs). # The host<->driver wire contract for the pf-vdisplay IddCx backend (control IOCTLs + Pod structs).
@@ -0,0 +1,653 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="kde_output_device_v2">
<copyright><![CDATA[
SPDX-FileCopyrightText: 2008-2011 Kristian Høgsberg
SPDX-FileCopyrightText: 2010-2011 Intel Corporation
SPDX-FileCopyrightText: 2012-2013 Collabora, Ltd.
SPDX-FileCopyrightText: 2015 Sebastian Kügler <sebas@kde.org>
SPDX-FileCopyrightText: 2021 Méven Car <meven.car@enioka.com>
SPDX-License-Identifier: MIT-CMU
]]></copyright>
<interface name="kde_output_device_registry_v2" version="24">
<description summary="output devices">
This interface can be used to list output devices.
If this global is bound with a version less than 21, the unsupported_version
protocol error will be posted.
</description>
<enum name="error">
<description summary="kde_output_device_registry_v2 error values">
These errors can be emitted in response to some requests.
</description>
<entry name="unsupported_version" value="0"
summary="the registry was bound with an unsupported version"/>
</enum>
<event name="finished" type="destructor" since="21">
<description summary="no new output announcements">
This event is sent in response to the stop request. The compositor will
immediately destroy the object after sending this event.
</description>
</event>
<request name="stop" since="21">
<description summary="stop receiving updates">
This request indicates that the client no longer wants to receive new
output announcements. The compositor will send the
kde_output_device_registry_v2.finished event in response to this request.
The compositor may still send new output announcements after calling this
request until the kde_output_device_registry_v2.finished event is sent.
</description>
</request>
<event name="output" since="21">
<description summary="new available output">
This event is sent when a new output is connected or after binding this
global to list all available outputs.
</description>
<arg name="output" type="new_id" interface="kde_output_device_v2"/>
</event>
</interface>
<interface name="kde_output_device_v2" version="24">
<description summary="output configuration representation">
An output device describes a display device available to the compositor.
output_device is similar to wl_output, but focuses on output
configuration management.
A client can query all global output_device objects to enlist all
available display devices, even those that may currently not be
represented by the compositor as a wl_output.
The client sends configuration changes to the server through the
outputconfiguration interface, and the server applies the configuration
changes to the hardware and signals changes to the output devices
accordingly.
This object is published as global during start up for every available
display devices, or when one later becomes available, for example by
being hotplugged via a physical connector.
Warning! The protocol described in this file is a desktop environment
implementation detail. Regular clients must not use this protocol.
Backward incompatible changes may be added without bumping the major
version of the extension.
</description>
<enum name="subpixel">
<description summary="subpixel geometry information">
This enumeration describes how the physical pixels on an output are
laid out.
</description>
<entry name="unknown" value="0"/>
<entry name="none" value="1"/>
<entry name="horizontal_rgb" value="2"/>
<entry name="horizontal_bgr" value="3"/>
<entry name="vertical_rgb" value="4"/>
<entry name="vertical_bgr" value="5"/>
</enum>
<enum name="transform">
<description summary="transform from framebuffer to output">
This describes the transform, that a compositor will apply to a
surface to compensate for the rotation or mirroring of an
output device.
The flipped values correspond to an initial flip around a
vertical axis followed by rotation.
The purpose is mainly to allow clients to render accordingly and
tell the compositor, so that for fullscreen surfaces, the
compositor is still able to scan out directly client surfaces.
</description>
<entry name="normal" value="0"/>
<entry name="90" value="1"/>
<entry name="180" value="2"/>
<entry name="270" value="3"/>
<entry name="flipped" value="4"/>
<entry name="flipped_90" value="5"/>
<entry name="flipped_180" value="6"/>
<entry name="flipped_270" value="7"/>
</enum>
<event name="geometry">
<description summary="geometric properties of the output">
The geometry event describes geometric properties of the output.
The event is sent when binding to the output object and whenever
any of the properties change.
</description>
<arg name="x" type="int"
summary="x position within the global compositor space"/>
<arg name="y" type="int"
summary="y position within the global compositor space"/>
<arg name="physical_width" type="int"
summary="width in millimeters of the output"/>
<arg name="physical_height" type="int"
summary="height in millimeters of the output"/>
<arg name="subpixel" type="int"
summary="subpixel orientation of the output"/>
<arg name="make" type="string"
summary="textual description of the manufacturer"/>
<arg name="model" type="string"
summary="textual description of the model"/>
<arg name="transform" type="int"
summary="transform that maps framebuffer to output"/>
</event>
<event name="current_mode">
<description summary="current mode">
This event describes the mode currently in use for this head. It is only
sent if the output is enabled.
</description>
<arg name="mode" type="object" interface="kde_output_device_mode_v2"/>
</event>
<event name="mode">
<description summary="advertise available output modes and current one">
The mode event describes an available mode for the output.
When the client binds to the output_device object, the server sends this
event once for every available mode the output_device can be operated by.
There will always be at least one event sent out on initial binding,
which represents the current mode.
Later if an output changes, its mode event is sent again for the
eventual added modes and lastly the current mode. In other words, the
current mode is always represented by the latest event sent with the current
flag set.
The size of a mode is given in physical hardware units of the output device.
This is not necessarily the same as the output size in the global compositor
space. For instance, the output may be scaled, as described in
kde_output_device_v2.scale, or transformed, as described in
kde_output_device_v2.transform.
</description>
<arg name="mode" type="new_id" interface="kde_output_device_mode_v2"/>
</event>
<event name="done">
<description summary="sent all information about output">
This event is sent after all other properties have been
sent on binding to the output object as well as after any
other output property change have been applied later on.
This allows to see changes to the output properties as atomic,
even if multiple events successively announce them.
</description>
</event>
<event name="scale">
<description summary="output scaling properties">
This event contains scaling geometry information
that is not in the geometry event. It may be sent after
binding the output object or if the output scale changes
later. If it is not sent, the client should assume a
scale of 1.
A scale larger than 1 means that the compositor will
automatically scale surface buffers by this amount
when rendering. This is used for high resolution
displays where applications rendering at the native
resolution would be too small to be legible.
It is intended that scaling aware clients track the
current output of a surface, and if it is on a scaled
output it should use wl_surface.set_buffer_scale with
the scale of the output. That way the compositor can
avoid scaling the surface, and the client can supply
a higher detail image.
</description>
<arg name="factor" type="fixed" summary="scaling factor of output"/>
</event>
<event name="edid">
<description summary="advertise EDID data for the output">
The edid event encapsulates the EDID data for the outputdevice.
The event is sent when binding to the output object. The EDID
data may be empty, in which case this event is sent anyway.
If the EDID information is empty, you can fall back to the name
et al. properties of the outputdevice.
</description>
<arg name="raw" type="string" summary="base64-encoded EDID string"/>
</event>
<event name="enabled">
<description summary="output is enabled or disabled">
The enabled event notifies whether this output is currently
enabled and used for displaying content by the server.
The event is sent when binding to the output object and
whenever later on an output changes its state by becoming
enabled or disabled.
</description>
<arg name="enabled" type="int" summary="output enabled state"/>
</event>
<event name="uuid">
<description summary="A unique id for this outputdevice">
The uuid can be used to identify the output. It's controlled by
the server entirely. The server should make sure the uuid is
persistent across restarts. An empty uuid is considered invalid.
</description>
<arg name="uuid" type="string" summary="output devices ID"/>
</event>
<event name="serial_number">
<description summary="Serial Number">
Serial ID of the monitor, sent on startup before the first done event.
</description>
<arg name="serialNumber" type="string"
summary="textual representation of serial number"/>
</event>
<event name="eisa_id">
<description summary="EISA ID">
EISA ID of the monitor, sent on startup before the first done event.
</description>
<arg name="eisaId" type="string"
summary="textual representation of EISA identifier"/>
</event>
<enum name="capability" bitfield="true">
<description summary="describes capabilities of the outputdevice">
Describes what capabilities this device has.
</description>
<entry name="overscan" value="0x1"
summary="if this output_device can use overscan"/>
<entry name="vrr" value="0x2"
summary="if this outputdevice supports variable refresh rate"/>
<entry name="rgb_range" value="0x4"
summary="if setting the rgb range is possible"/>
<entry name="high_dynamic_range" value="0x8" since="3"
summary="if this outputdevice supports high dynamic range"/>
<entry name="wide_color_gamut" value="0x10" since="3"
summary="if this outputdevice supports a wide color gamut"/>
<entry name="auto_rotate" value="0x20" since="4"
summary="if this outputdevice supports autorotation"/>
<entry name="icc_profile" value="0x40" since="5"
summary="if this outputdevice supports icc profiles"/>
<entry name="brightness" value="0x80" since="9"
summary="if this outputdevice supports the brightness setting"/>
<entry name="built_in_color" value="0x100" since="12"
summary="if this outputdevice supports the built-in color profile"/>
<entry name="ddc_ci" value="0x200" since="14"
summary="if this outputdevice supports DDC/CI"/>
<entry name="max_bits_per_color" value="0x400" since="15"
summary="if this outputdevice supports setting max bpc"/>
<entry name="edr" value="0x800" since="16"
summary="if this outputdevice supports EDR"/>
<entry name="sharpness" value="0x1000" since="17"
summary="if this outputdevice supports the sharpness setting"/>
<entry name="custom_modes" value="0x2000" since="18"
summary="if this outputdevice supports custom modes"/>
<entry name="auto_brightness" value = "0x4000" since="19"/>
<entry name="hdr_icc_profile" value="0x8000" since="22"
summary="if this outputdevice supports HDR ICC profiles"/>
<entry name="abm_level" value="0x10000" since="23"
summary="if this outputdevice supports the abm level setting"/>
</enum>
<event name="capabilities">
<description summary="capability flags">
What capabilities this device has, sent on startup before the first
done event.
</description>
<arg name="flags" type="uint" enum="capability"/>
</event>
<event name="overscan">
<description summary="overscan">
Overscan value of the monitor in percent, sent on startup before the
first done event.
</description>
<arg name="overscan" type="uint"
summary="amount of overscan of the monitor"/>
</event>
<enum name="vrr_policy">
<description summary="describes vrr policy">
Describes when the compositor may employ variable refresh rate
</description>
<entry name="never" value="0"/>
<entry name="always" value="1"/>
<entry name="automatic" value="2"/>
</enum>
<event name="vrr_policy">
<description summary="Variable Refresh Rate Policy">
What policy the compositor will employ regarding its use of variable
refresh rate.
</description>
<arg name="vrr_policy" type="uint" enum="vrr_policy"/>
</event>
<enum name="rgb_range">
<description summary="describes RGB range policy">
Whether full or limited color range should be used
</description>
<entry name="automatic" value="0"/>
<entry name="full" value="1"/>
<entry name="limited" value="2"/>
</enum>
<event name="rgb_range">
<description summary="RGB range">
What rgb range the compositor is using for this output
</description>
<arg name="rgb_range" type="uint" enum="rgb_range"/>
</event>
<event name="name" since="2">
<description summary="Output's name">
Name of the output, it's useful to cross-reference to an zxdg_output_v1 and ultimately QScreen
</description>
<arg name="name" type="string"/>
</event>
<event name="high_dynamic_range" since="3">
<description summary="if HDR is enabled">
Whether or not high dynamic range is enabled for this output
</description>
<arg name="hdr_enabled" type="uint" summary="1 if enabled, 0 if disabled"/>
</event>
<event name="sdr_brightness" since="3">
<description summary="the brightness of sdr if hdr is enabled">
If high dynamic range is used, this value defines the brightness in nits for content
that's in standard dynamic range format. Note that while the value is in nits, that
doesn't necessarily translate to the same brightness on the screen.
</description>
<arg name="sdr_brightness" type="uint"/>
</event>
<event name="wide_color_gamut" since="3">
<description summary="if WCG is enabled">
Whether or not the use of a wide color gamut is enabled for this output
</description>
<arg name="wcg_enabled" type="uint" summary="1 if enabled, 0 if disabled"/>
</event>
<enum name="auto_rotate_policy">
<description summary="describes when auto rotate should be used"/>
<entry name="never" value="0"/>
<entry name="in_tablet_mode" value="1"/>
<entry name="always" value="2"/>
</enum>
<event name="auto_rotate_policy" since="4">
<description summary="describes when auto rotate is used"/>
<arg name="policy" type="uint" enum="auto_rotate_policy"/>
</event>
<event name="icc_profile_path" since="5">
<description summary="describes the path to the ICC profile used in SDR mode"/>
<arg name="profile_path" type="string"/>
</event>
<event name="brightness_metadata" since="6">
<description summary="metadata about the screen's brightness limits"/>
<arg name="max_peak_brightness" type="uint" summary="in nits"/>
<arg name="max_frame_average_brightness" type="uint" summary="in nits"/>
<arg name="min_brightness" type="uint" summary="in 0.0001 nits"/>
</event>
<event name="brightness_overrides" since="6">
<description summary="overrides for the screen's brightness limits"/>
<arg name="max_peak_brightness" type="int" summary="-1 for no override, positive values are the brightness in nits"/>
<arg name="max_average_brightness" type="int" summary="-1 for no override, positive values are the brightness in nits"/>
<arg name="min_brightness" type="int" summary="-1 for no override, positive values are the brightness in 0.0001 nits"/>
</event>
<event name="sdr_gamut_wideness" since="6">
<description summary="describes which gamut is assumed for sRGB applications">
This can be used to provide the colors users assume sRGB applications should have based on the
default experience on many modern sRGB screens.
</description>
<arg name="gamut_wideness" type="uint" summary="0 means rec.709 primaries, 10000 means native primaries"/>
</event>
<enum name="color_profile_source" since="7">
<description summary="which source the compositor should use for the color profile on an output"/>
<entry name="sRGB" value="0"/>
<entry name="ICC" value="1"/>
<entry name="EDID" value="2"/>
</enum>
<event name="color_profile_source" since="7">
<description summary="describes which source the compositor uses for the color profile on an output in SDR mode"/>
<arg name="source" type="uint" enum="color_profile_source"/>
</event>
<event name="brightness" since="8">
<description summary="brightness multiplier">
This is the brightness modifier of the output. It doesn't specify
any absolute values, but is merely a multiplier on top of other
brightness values, like sdr_brightness and brightness_metadata.
0 is the minimum brightness (not completely dark) and 10000 is
the maximum brightness.
This is currently only supported / meaningful while HDR is active.
</description>
<arg name="brightness" type="uint" summary="brightness in 0-10000"/>
</event>
<enum name="color_power_tradeoff">
<description summary="tradeoff between power and accuracy">
The compositor can do a lot of things that trade between
performance, power and color accuracy. This setting describes
a high level preference from the user about in which direction
that tradeoff should be made.
</description>
<entry name="efficiency" value="0" summary="prefer efficiency and performance"/>
<entry name="accuracy" value="1" summary="prefer accuracy"/>
</enum>
<event name="color_power_tradeoff" since="10">
<description summary="the preferred color/power tradeoff"/>
<arg name="preference" type="uint" enum="color_power_tradeoff"/>
</event>
<event name="dimming" since="11">
<description summary="dimming multiplier">
This is the dimming multiplier of the output. This is similar to
the brightness setting, except it's meant to be a temporary setting
only, not persistent and may be implemented differently depending
on the display.
0 is the minimum dimming factor (not completely dark) and 10000
means the output is not dimmed.
</description>
<arg name="multiplier" type="uint" summary="multiplier in 0-10000"/>
</event>
<event name="replication_source" since="13">
<description summary="source output for mirroring"/>
<arg name="source" type="string" summary="uuid of the source output"/>
</event>
<event name="ddc_ci_allowed" since="14">
<description summary="if DDC/CI should be used to control brightness etc.">
If the ddc_ci capability is present, this determines if settings
such as brightness, contrast or others should be set using DDC/CI.
</description>
<arg name="allowed" type="uint" summary="1 if allowed, 0 if disabled"/>
</event>
<event name="max_bits_per_color" since="15">
<description summary="override max bpc">
This limits the amount of bits per color that are sent to the display.
</description>
<arg name="max_bpc" type="uint" summary="0 for the default / automatic"/>
</event>
<event name="max_bits_per_color_range" since="15">
<description summary="range of max bits per color value"/>
<arg name="min_value" type="uint" summary="the minimum supported by the driver"/>
<arg name="max_value" type="uint" summary="the maximum supported by the driver"/>
</event>
<event name="automatic_max_bits_per_color_limit" since="15">
<description summary="if and to what value automatic max bpc is limited"/>
<arg name="max_bpc_limit" type="uint"
summary="which value automatic bpc gets limited to. 0 if not limited"/>
</event>
<enum name="edr_policy" since="16">
<description summary="when the compositor may make use of EDR"/>
<entry name="never" value="0"/>
<entry name="always" value="1"/>
</enum>
<event name="edr_policy" since="16">
<description summary="when the compositor may apply EDR">
When EDR is enabled, the compositor may increase the backlight beyond
the user-specified setting, in order to present HDR content on displays
without native HDR support.
This will usually result in better visuals, but also increases battery
usage.
</description>
<arg name="policy" type="uint" enum="edr_policy"/>
</event>
<event name="sharpness" since="17">
<description summary="sharpness strength">
This is the sharpness modifier of the output.
0 is sharpness disabled and 10000 is the maximum sharpness
</description>
<arg name="sharpness" type="uint" summary="sharpness in 0-10000"/>
</event>
<event name="priority" since="18">
<description summary="output priority">
Describes the position of the output in the output order list,
with lower values being earlier in the list. There's no specific
value the list has to start at, this value is only used in sorting
outputs.
Note that the output order protocol is not sufficient for this,
as an output may not be in the output order if it's disabled or
mirroring another screen.
</description>
<arg name="priority" type="uint" summary="priority"/>
</event>
<event name="auto_brightness" since="20">
<description summary="whether or not automatic brightness is enabled"/>
<arg name="enabled" type="uint" summary="1 for enabled, 0 for disabled"/>
</event>
<request name="release" type="destructor" since="21">
<description summary="destroy the output device">
This notifies the compositor that the client no longer wishes to use
the kde_output_device_v2 object.
</description>
</request>
<event name="removed" since="21">
<description summary="the output has been removed">
This event is sent when the output device is disconnected and no new
updates will be sent. The client should call the kde_output_device_v2.release
request after receiving this event.
</description>
</event>
<event name="hdr_icc_profile_path" since="22">
<description summary="describes the path to the ICC profile used in HDR mode"/>
<arg name="profile_path" type="string"/>
</event>
<event name="hdr_color_profile_source" since="22">
<description summary="describes which source the compositor uses for the color profile on an output in HDR mode"/>
<arg name="source" type="uint" enum="color_profile_source"/>
</event>
<event name="abm_level" since="23">
<description summary="allowed level of adaptive backlight modulation">
Adaptive backlight modulation is a feature that reduces the backlight
and increases contrast of colors on the screen to improve power usage.
</description>
<arg name="level" type="uint" summary="0 is off, 4 is the maximum level"/>
</event>
</interface>
<interface name="kde_output_device_mode_v2" version="24">
<description summary="output mode">
This object describes an output mode.
Some heads don't support output modes, in which case modes won't be
advertised.
Properties sent via this interface are applied atomically via the
kde_output_device.done event. No guarantees are made regarding the order
in which properties are sent.
</description>
<event name="size">
<description summary="mode size">
This event describes the mode size. The size is given in physical
hardware units of the output device. This is not necessarily the same as
the output size in the global compositor space. For instance, the output
may be scaled or transformed.
</description>
<arg name="width" type="int" summary="width of the mode in hardware units"/>
<arg name="height" type="int" summary="height of the mode in hardware units"/>
</event>
<event name="refresh">
<description summary="mode refresh rate">
This event describes the mode's fixed vertical refresh rate. It is only
sent if the mode has a fixed refresh rate.
</description>
<arg name="refresh" type="int" summary="vertical refresh rate in mHz"/>
</event>
<event name="preferred">
<description summary="mode is preferred">
This event advertises this mode as preferred.
</description>
</event>
<event name="removed">
<description summary="the mode has been destroyed">
The compositor will destroy the object immediately after sending this
event, so it will become invalid and the client should release any
resources associated with it.
</description>
</event>
<enum name="flags">
<description summary="mode flags"/>
<entry name="custom" value="0x1"/>
<entry name="reduced_blanking" value="0x2" deprecated-since="24"/>
</enum>
<event name="flags" since="19">
<description summary="mode flags">
This event describes the mode's flags.
</description>
<arg name="flags" type="uint" enum="flags"/>
</event>
<event name="cvt" since="24">
<description summary="cvt timings">
This event describes the CVT timings associated with the mode.
If an output mode has no CVT timings, this event will not be sent.
</description>
<arg name="dot_clock" type="uint" summary="pixel clock in kHz"/>
<arg name="hdisplay" type="uint" summary="horizontal display size"/>
<arg name="hsync_start" type="uint" summary="horizontal sync start"/>
<arg name="hsync_end" type="uint" summary="horizontal sync end"/>
<arg name="htotal" type="uint" summary="horizontal total size"/>
<arg name="hskew" type="uint" summary="horizontal skew"/>
<arg name="vdisplay" type="uint" summary="vertical display size"/>
<arg name="vsync_start" type="uint" summary="vertical sync start"/>
<arg name="vsync_end" type="uint" summary="vertical sync end"/>
<arg name="vtotal" type="uint" summary="vertical total size"/>
<arg name="vscan" type="uint" summary="vertical scan"/>
<arg name="flags" type="uint" summary="flags, see DRM_MODE_FLAG_*"/>
</event>
</interface>
</protocol>
@@ -0,0 +1,539 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="kde_output_management_v2">
<copyright><![CDATA[
SPDX-FileCopyrightText: 2008-2011 Kristian Høgsberg
SPDX-FileCopyrightText: 2010-2011 Intel Corporation
SPDX-FileCopyrightText: 2012-2013 Collabora, Ltd.
SPDX-FileCopyrightText: 2015 Sebastian Kügler <sebas@kde.org>
SPDX-FileCopyrightText: 2021 Méven Car <meven.car@enioka.com>
SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@kde.org>
SPDX-License-Identifier: MIT-CMU
]]></copyright>
<interface name="kde_output_management_v2" version="22">
<description summary="configuration of server outputs through clients">
This interface enables clients to set properties of output devices for screen
configuration purposes via the server. To this end output devices are referenced
by global kde_output_device_v2 objects.
outputmanagement (wl_global)
--------------------------
request:
* create_configuration -> outputconfiguration (wl_resource)
outputconfiguration (wl_resource)
--------------------------
requests:
* enable(outputdevice, bool)
* mode(outputdevice, mode)
* transformation(outputdevice, flag)
* position(outputdevice, x, y)
* apply
events:
* applied
* failed
The server registers one outputmanagement object as a global object. In order
to configure outputs a client requests create_configuration, which provides a
resource referencing an outputconfiguration for one-time configuration. That
way the server knows which requests belong together and can group them by that.
On the outputconfiguration object the client calls for each output whether the
output should be enabled, which mode should be set (by referencing the mode from
the list of announced modes) and the output's global position. Once all outputs
are configured that way, the client calls apply.
At that point and not earlier the server should try to apply the configuration.
If this succeeds the server emits the applied signal, otherwise the failed
signal, such that the configuring client is noticed about the success of its
configuration request.
Through this design the interface enables atomic output configuration changes if
internally supported by the server.
Warning! The protocol described in this file is a desktop environment implementation
detail. Regular clients must not use this protocol. Backward incompatible
changes may be added without bumping the major version of the extension.
</description>
<request name="create_configuration">
<description summary="provide outputconfiguration object for configuring outputs">
Request an outputconfiguration object through which the client can configure
output devices.
</description>
<arg name="id" type="new_id" interface="kde_output_configuration_v2"/>
</request>
<request name="create_mode_list">
<description summary="create a list of custom modes">
For details, see the description of kde_mode_list_v2 and
kde_output_configuration_v2.set_custom_modes.
</description>
<arg name="id" type="new_id" interface="kde_mode_list_v2"/>
</request>
</interface>
<interface name="kde_output_configuration_v2" version="22">
<description summary="configure single output devices">
outputconfiguration is a client-specific resource that can be used to ask
the server to apply changes to available output devices.
The client receives a list of output devices from the registry. When it wants
to apply new settings, it creates a configuration object from the
outputmanagement global, writes changes through this object's enable, scale,
transform and mode calls. It then asks the server to apply these settings in
an atomic fashion, for example through Linux' DRM interface.
The server signals back whether the new settings have applied successfully
or failed to apply. outputdevice objects are updated after the changes have been
applied to the hardware and before the server side sends the applied event.
</description>
<enum name="error">
<description summary="kde_output_configuration_v2 error values">
These error can be emitted in response to kde_output_configuration_v2 requests.
</description>
<entry name="already_applied" value="0" summary="the config is already applied"/>
</enum>
<request name="enable">
<description summary="enable or disable an output">
Mark the output as enabled or disabled.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice to be en- or disabled"/>
<arg name="enable" type="int" summary="1 to enable or 0 to disable this output"/>
</request>
<request name="mode">
<description summary="switch output-device to mode">
Sets the mode for a given output.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this mode change applies to"/>
<arg name="mode" type="object" interface="kde_output_device_mode_v2" summary="the mode to apply"/>
</request>
<request name="transform">
<description summary="transform output-device">
Sets the transformation for a given output.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this transformation change applies to"/>
<arg name="transform" type="int" summary="transform enum"/>
</request>
<request name="position">
<description summary="position output in global space">
Sets the position for this output device. (x,y) describe the top-left corner
of the output in global space, whereby the origin (0,0) of the global space
has to be aligned with the top-left corner of the most left and in case this
does not define a single one the top output.
There may be no gaps or overlaps between outputs, i.e. the outputs are
stacked horizontally, vertically, or both on each other.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this position applies to"/>
<arg name="x" type="int" summary="position on the x-axis"/>
<arg name="y" type="int" summary="position on the y-axis"/>
</request>
<request name="scale">
<description summary="set scaling factor of this output">
Sets the scaling factor for this output device.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this scale change applies to"/>
<arg name="scale" type="fixed" summary="scaling factor"/>
</request>
<request name="apply">
<description summary="apply configuration changes to all output devices">
Asks the server to apply property changes requested through this outputconfiguration
object to all outputs on the server side.
The output configuration can be applied only once. The already_applied protocol error
will be posted if the apply request is called the second time.
</description>
</request>
<event name="applied">
<description summary="configuration changes have been applied">
Sent after the server has successfully applied the changes.
.
</description>
</event>
<event name="failed">
<description summary="configuration changes failed to apply">
Sent if the server rejects the changes or failed to apply them.
</description>
</event>
<request name="destroy" type="destructor">
<description summary="release the outputconfiguration object"/>
</request>
<request name="overscan">
<description summary="set overscan value">
Set the overscan value of this output device with a value in percent.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice overscan applies to"/>
<arg name="overscan" type="uint" summary="overscan value"/>
</request>
<enum name="vrr_policy">
<description summary="describes vrr policy">
Describes when the compositor may employ variable refresh rate
</description>
<entry name="never" value="0"/>
<entry name="always" value="1"/>
<entry name="automatic" value="2"/>
</enum>
<request name="set_vrr_policy">
<description summary="set the VRR policy">
Set what policy the compositor should employ regarding its use of
variable refresh rate.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this VRR policy applies to"/>
<arg name="policy" type="uint" enum="vrr_policy" summary="the vrr policy to apply"/>
</request>
<enum name="rgb_range">
<description summary="describes RGB range policy">
Whether this output should use full or limited rgb.
</description>
<entry name="automatic" value="0"/>
<entry name="full" value="1"/>
<entry name="limited" value="2"/>
</enum>
<request name="set_rgb_range">
<description summary="RGB range">
Whether full or limited color range should be used
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice the rgb range applies to"/>
<arg name="rgb_range" type="uint" enum="rgb_range"/>
</request>
<request name="set_primary_output" since="2">
<description summary="Select which primary output to use" />
<arg name="output" type="object" interface="kde_output_device_v2" allow-null="false"/>
</request>
<request name="set_priority" since="3">
<description summary="Set the order of outputs">
Set the position of the output in the output order list, with lower values
being earlier in the list. There's no specific value the list has to start
at, this value is only used in sorting outputs.
The order of outputs can be used to assign desktop environment components
to a specific screen, see kde_output_order_v1 and kde-output-device-v2 for
details. Note that for consistent behavior, the priority value needs to be
unique among all enabled outputs.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice the index applies to" />
<arg name="priority" type="uint" summary="the priority of the output" />
</request>
<request name="set_high_dynamic_range" since="4">
<description summary="change if HDR should be enabled">
Sets whether or not the output should be set to HDR mode.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="enable_hdr" type="uint" summary="1 to enable, 0 to disable hdr"/>
</request>
<request name="set_sdr_brightness" since="4">
<description summary="set the brightness for sdr content">
Sets the brightness of standard dynamic range content in nits. Only has an effect while the output is in HDR mode.
Note that while the value is in nits, that doesn't necessarily translate to the same brightness on the screen.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="sdr_brightness" type="uint"/>
</request>
<request name="set_wide_color_gamut" since="4">
<description summary="change if a wide color gamut should be used">
Whether or not the output should use a wide color gamut
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="enable_wcg" type="uint" summary="1 to enable, 0 to disable wcg"/>
</request>
<enum name="auto_rotate_policy">
<description summary="describes when auto rotate should be used"/>
<entry name="never" value="0"/>
<entry name="in_tablet_mode" value="1"/>
<entry name="always" value="2"/>
</enum>
<request name="set_auto_rotate_policy" since="5">
<description summary="change when auto rotate should be used"/>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="policy" type="uint" enum="auto_rotate_policy"/>
</request>
<request name="set_icc_profile_path" since="6">
<description summary="change the used icc profile for SDR mode"/>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="profile_path" type="string"/>
</request>
<request name="set_brightness_overrides" since="7">
<description summary="override metadata about the screen's brightness limits"/>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="max_peak_brightness" type="int" summary="-1 for not overriding, or positive values in nits"/>
<arg name="max_frame_average_brightness" type="int" summary="-1 for not overriding, or positive values in nits"/>
<arg name="min_brightness" type="int" summary="-1 for not overriding, or positive values in 0.0001 nits"/>
</request>
<request name="set_sdr_gamut_wideness" since="7">
<description summary="describes which gamut is assumed for sRGB applications">
This can be used to provide the colors users assume sRGB applications should have based on the
default experience on many modern sRGB screens.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="gamut_wideness" type="uint" summary="0 means rec.709 primaries, 10000 means native primaries"/>
</request>
<enum name="color_profile_source" since="7">
<description summary="which source the compositor should use for the color profile on an output"/>
<entry name="sRGB" value="0"/>
<entry name="ICC" value="1"/>
<entry name="EDID" value="2"/>
</enum>
<request name="set_color_profile_source" since="8">
<description summary="which source the compositor should use for the color profile on an output in SDR mode"/>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="color_profile_source" type="uint" enum="color_profile_source" summary="the color profile source"/>
</request>
<request name="set_brightness" since="9">
<description summary="brightness multiplier">
Set the brightness modifier of the output. It doesn't specify
any absolute values, but is merely a multiplier on top of other
brightness values, like sdr_brightness and brightness_metadata.
0 is the minimum brightness (not completely dark) and 10000 is
the maximum brightness.
This is supported while HDR is active in versions 8 and below,
or when the device supports the "brightness" capability in
versions 9 and above.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="brightness" type="uint" summary="brightness in 0-10000"/>
</request>
<enum name="color_power_tradeoff">
<description summary="tradeoff between power and accuracy">
The compositor can do a lot of things that trade between
performance, power and color accuracy. This setting describes
a high level preference from the user about in which direction
that tradeoff should be made.
</description>
<entry name="efficiency" value="0" summary="prefer efficiency and performance"/>
<entry name="accuracy" value="1" summary="prefer accuracy"/>
</enum>
<request name="set_color_power_tradeoff" since="10">
<description summary="set the preferred color/power tradeoff"/>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="preference" type="uint" enum="color_power_tradeoff"/>
</request>
<request name="set_dimming" since="11">
<description summary="dimming multiplier">
Set the dimming multiplier of the output. This is similar to the
brightness setting, except it's meant to be a temporary setting
only, not persistent and may be implemented differently depending
on the display.
0 is the minimum dimming factor (not completely dark) and 10000
means the output is not dimmed.
This is supported only when the "brightness" capability is
also supported.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="multiplier" type="uint" summary="multiplier in 0-10000"/>
</request>
<event name="failure_reason" since="12">
<description summary="reason for failure">
Describes why applying the output configuration failed. Is only
sent before the failure event.
</description>
<arg name="reason" type="string" summary="reason for failure"/>
</event>
<request name="set_replication_source" since="13">
<description summary="source output for mirroring">
Set the source output that the outputdevice should mirror its
viewport from.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="source" type="string" summary="uuid of the source output"/>
</request>
<request name="set_ddc_ci_allowed" since="14">
<description summary="if DDC/CI should be used to control brightness etc."/>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="allowed" type="uint" summary="1 if allowed, 0 if disabled"/>
</request>
<request name="set_max_bits_per_color" since="15">
<description summary="override the max bpc">
This limits the amount of bits per color that are sent to the display.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="max_bpc" type="uint" summary="0 for the default / automatic"/>
</request>
<enum name="edr_policy" since="16">
<description summary="when the compositor may make use of EDR"/>
<entry name="never" value="0"/>
<entry name="always" value="1"/>
</enum>
<request name="set_edr_policy" since="16">
<description summary="set when the compositor may apply EDR">
When EDR is enabled, the compositor may increase the backlight beyond
the user-specified setting, in order to present HDR content on displays
without native HDR support.
This will usually result in better visuals, but also increases battery
usage.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="policy" type="uint" enum="edr_policy"/>
</request>
<request name="set_sharpness" since="17">
<description summary="sharpness strength">
This is the sharpness modifier of the output.
0 is sharpness disabled and 10000 is the maximum sharpness
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="sharpness" type="uint" summary="sharpness in 0-10000"/>
</request>
<request name="set_custom_modes" since="18">
<description summary="set the custom mode list">
Set the list of custom modes for this output. The compositor
will in response generate the requested modes and add them to
the output (or delete ones no longer in the list).
This can be useful for overclocking displays, or for working
around broken EDIDs.
Note that there is no guarantee for any custom mode to
actually work, or even to leave the display undamaged (in the
case of CRTs). It's entirely the responsibility of the user
to ensure each added mode is the right one for their display.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="modes" type="object" interface="kde_mode_list_v2"/>
</request>
<request name="set_auto_brightness" since="19">
<description summary="whether or not automatic brightness is enabled"/>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="enabled" type="uint" summary="1 for enabled, 0 for disabled"/>
</request>
<request name="set_hdr_icc_profile_path" since="20">
<description summary="change the used icc profile for HDR mode"/>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="profile_path" type="string"/>
</request>
<request name="set_hdr_color_profile_source" since="20">
<description summary="which source the compositor should use for the color profile on an output in HDR mode"/>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="color_profile_source" type="uint" enum="color_profile_source" summary="the color profile source"/>
</request>
<request name="set_abm_level" since="21">
<description summary="set the allowed level of adaptive backlight modulation">
Adaptive backlight modulation is a feature that reduces the backlight
and increases contrast of colors on the screen to improve power usage.
</description>
<arg name="outputdevice" type="object" interface="kde_output_device_v2" summary="outputdevice this setting applies to"/>
<arg name="level" type="uint" summary="0 is off, 4 is the maximum level"/>
</request>
</interface>
<interface name="kde_mode_list_v2" version="22">
<description summary="a list of custom modes">
This list is populated by first setting each relevant property,
and then calling add_mode to add a mode with these properties.
One would for example call
- set_resolution
- set_refresh_rate
- set_reduced_blanking
- add_mode
add_mode does not reset the properties that were previously set,
they are valid until the object is destroyed.
The compositor may additionally have sensible defaults for some
properties like reduced_blanking, but for consistent results,
it's best to always set each known property every time.
One can also specify custom modes with CVT timings, for example
- add_cvt
- add_cvt
The parameters resolution and refresh rate are required, if they
are not set, the missing_parameters error will be emitted.
</description>
<enum name="error">
<description summary="kde_mode_list_v2 error values">
These errors can be emitted in response to add_mode requests.
</description>
<entry name="missing_parameters" value="0" summary="a required parameter wasn't set"/>
</enum>
<request name="destroy" type="destructor">
<description summary="destroy the mode list object"/>
</request>
<request name="add_mode">
<description summary="Add the current mode configuration to the list"/>
</request>
<request name="set_resolution">
<arg name="width" type="uint"/>
<arg name="height" type="uint"/>
</request>
<request name="set_refresh_rate">
<arg name="rate" type="uint" summary="in milliHz"/>
</request>
<request name="set_reduced_blanking">
<description summary="whether or not the mode should have reduced blanking">
Reduced blanking is an optimization that can reduce bandwidth / timing
requirements for a display mode by reducing the time vblank takes.
As not all displays support it, it may be desired to still turn it off
though (like with CRTs, where full blanking is required).
</description>
<arg name="reduced" type="uint"
summary="1 for reduced blanking, 0 for normal vblank duration"/>
</request>
<request name="add_cvt" since="22">
<description summary="new mode with CVT timings">
Adds a new mode with the specified CVT timings.
</description>
<arg name="dot_clock" type="uint" summary="pixel clock in kHz"/>
<arg name="hdisplay" type="uint" summary="horizontal display size"/>
<arg name="hsync_start" type="uint" summary="horizontal sync start"/>
<arg name="hsync_end" type="uint" summary="horizontal sync end"/>
<arg name="htotal" type="uint" summary="horizontal total size"/>
<arg name="hskew" type="uint" summary="horizontal skew"/>
<arg name="vdisplay" type="uint" summary="vertical display size"/>
<arg name="vsync_start" type="uint" summary="vertical sync start"/>
<arg name="vsync_end" type="uint" summary="vertical sync end"/>
<arg name="vtotal" type="uint" summary="vertical total size"/>
<arg name="vscan" type="uint" summary="vertical scan"/>
<arg name="flags" type="uint" summary="flags, see DRM_MODE_FLAG_*"/>
</request>
</interface>
</protocol>
+7
View File
@@ -445,6 +445,13 @@ mod hyprland;
#[path = "vdisplay/linux/kwin.rs"] #[path = "vdisplay/linux/kwin.rs"]
mod kwin; mod kwin;
// In-process KDE output management (kde_output_management_v2) — the topology path that used to shell
// out to `kscreen-doctor`, driven over the compositor's own Wayland instead so it can't be wedged by
// a stuck libkscreen/kscreen-KDED backend. Consumed by `kwin` (best-effort, with kscreen fallback).
#[cfg(target_os = "linux")]
#[path = "vdisplay/linux/kwin_output_mgmt.rs"]
mod kwin_output_mgmt;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
#[path = "vdisplay/windows/manager.rs"] #[path = "vdisplay/windows/manager.rs"]
pub mod manager; pub mod manager;
+93 -31
View File
@@ -89,6 +89,11 @@ pub struct KwinDisplay {
/// [`apply_position`](VirtualDisplay::apply_position) addresses OUR output even while a /// [`apply_position`](VirtualDisplay::apply_position) addresses OUR output even while a
/// superseded same-name sibling is still alive. /// superseded same-name sibling is still alive.
last_name: Option<String>, last_name: Option<String>,
/// The RESOLVED `kde_output_device_v2` UUID of the last `create`'s output, when the in-process
/// output-management path handled the topology. A stable per-output id (unlike the shared name),
/// so [`apply_position`](VirtualDisplay::apply_position) and restore address exactly OUR output
/// across a supersede — preferred over `last_name`, which is only the kscreen-doctor fallback.
our_uuid: Option<String>,
/// The topology-restore action the last `create` prepared (re-enable the outputs an `exclusive` /// The topology-restore action the last `create` prepared (re-enable the outputs an `exclusive`
/// topology disabled), pending pickup by the registry via [`take_topology_restore`] — so the /// topology disabled), pending pickup by the registry via [`take_topology_restore`] — so the
/// physical is re-enabled only when the display GROUP's last member drops (§6.1), not this session's. /// physical is re-enabled only when the display GROUP's last member drops (§6.1), not this session's.
@@ -114,6 +119,48 @@ impl KwinDisplay {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
Ok(KwinDisplay::default()) Ok(KwinDisplay::default())
} }
/// Apply the effective display topology for the just-created output `our_prefix` (current size
/// `dims`), preferring the in-process `kde_output_management_v2` path and falling back to
/// `kscreen-doctor` if the compositor doesn't answer in budget or the management global is
/// absent. Records the output's UUID (in-process) or kscreen address (fallback) for
/// [`apply_position`](VirtualDisplay::apply_position), and returns the disabled outputs (each
/// `(name, "WxH@Hz")`) for the group teardown restore. `Extend`/`Auto` disable nothing.
fn apply_topology(
&mut self,
name: &str,
our_prefix: &str,
dims: (u32, u32),
) -> Vec<(String, String)> {
use crate::kwin_output_mgmt::TopologyKind;
use crate::policy::Topology;
let topology = crate::effective_topology();
let kind = match topology {
Topology::Exclusive => TopologyKind::Exclusive,
Topology::Primary => TopologyKind::Primary,
Topology::Extend | Topology::Auto => return Vec::new(),
};
// In-process over Wayland — immune to whatever wedges the standalone kscreen-doctor.
let outcome = crate::kwin_output_mgmt::apply_topology(our_prefix, dims.0, dims.1, kind);
if outcome.handled {
self.our_uuid = outcome.our_uuid;
return outcome.disabled;
}
// Fallback: kscreen-doctor — resolve our address the old way, then shell out the topology.
tracing::info!(
"KWin topology: kde_output_management unavailable — kscreen-doctor fallback"
);
let addr = resolve_kscreen_addr(name, dims.0, dims.1);
self.last_name = Some(addr.clone());
match topology {
Topology::Exclusive => apply_virtual_primary(&addr),
Topology::Primary => {
apply_virtual_primary_only(&addr);
Vec::new()
}
Topology::Extend | Topology::Auto => Vec::new(),
}
}
} }
impl VirtualDisplay for KwinDisplay { impl VirtualDisplay for KwinDisplay {
@@ -142,8 +189,15 @@ impl VirtualDisplay for KwinDisplay {
} }
fn apply_position(&mut self, x: i32, y: i32) { fn apply_position(&mut self, x: i32, y: i32) {
// `last_name` holds the RESOLVED kscreen address (numeric output id, or the // Prefer the in-process path: address OUR output by its stable UUID (supersede-robust) over
// `Virtual-<name>` fallback) — never re-derive from the name: during a supersede two // kde_output_management_v2 — immune to a wedged kscreen-doctor backend (see kwin_output_mgmt).
if let Some(uuid) = self.our_uuid.clone() {
if crate::kwin_output_mgmt::set_position(&uuid, x, y) {
return;
}
}
// Fallback: kscreen-doctor. `last_name` holds the RESOLVED kscreen address (numeric output id,
// or the `Virtual-<name>` fallback) — never re-derive from the name: during a supersede two
// outputs share it and the command would hit the old one (see `create`). // outputs share it and the command would hit the old one (see `create`).
let Some(output) = self.last_name.clone() else { let Some(output) = self.last_name.clone() else {
return; return;
@@ -227,17 +281,10 @@ impl VirtualDisplay for KwinDisplay {
embedded_pointer = !self.hw_cursor, embedded_pointer = !self.hw_cursor,
"KWin virtual output ready" "KWin virtual output ready"
); );
// ⚠️ ADDRESS BY NUMERIC KSCREEN ID, NEVER BY NAME: a supersede (mode switch) creates the // Topology + positioning address OUR output by its kde_output_management UUID (resolved
// replacement output — SAME per-slot name, deliberately, for KWin's per-name config // in-process in `apply_topology`, supersede-robust) — no early kscreen-doctor resolve, so
// persistence — while the superseded sibling is still alive (create-before-drop). Every // the 60 Hz path never shells out. Only the >60 Hz custom-mode install below still uses
// name-addressed kscreen command then hits the FIRST match = the OLD output: on-glass this // kscreen-doctor (its in-process port — set_custom_modes/add_cvt — is a follow-up).
// resized the LIVE session's display out from under it (wrong-res/black), read back the
// OLD output as "custom mode applied", made the OLD output primary, and positioned it —
// while the new output never left its birth mode and the capturer's dims gate starved.
// Resolve OUR output's kscreen id once (match: managed-prefix name AND current mode ==
// the just-created birth size; newest id wins), and use it for every kscreen operation.
let mut addr = resolve_kscreen_addr(&name, width, birth_h);
self.last_name = Some(addr.clone()); // for apply_position (registry-driven §6.2 layout)
let mut expect_exact_dims = false; let mut expect_exact_dims = false;
// The size the output actually ENDS UP at — the request, unless KWin's CVT generator had to // The size the output actually ENDS UP at — the request, unless KWin's CVT generator had to
// shrink the width to the cell grain (see `CVT_H_GRANULARITY`). Reported as the output's // shrink the width to the cell grain (see `CVT_H_GRANULARITY`). Reported as the output's
@@ -245,6 +292,14 @@ impl VirtualDisplay for KwinDisplay {
// encoder opens against, so a CVT-aligned mode flows end-to-end instead of starving. // encoder opens against, so a CVT-aligned mode flows end-to-end instead of starving.
let mut final_dims = (width, height); let mut final_dims = (width, height);
let achieved_hz = if want_high { let achieved_hz = if want_high {
// ⚠️ ADDRESS BY NUMERIC KSCREEN ID, NEVER BY NAME: a supersede (mode switch) creates the
// replacement output — SAME per-slot name, deliberately, for KWin's per-name config
// persistence — while the superseded sibling is still alive (create-before-drop). A
// name-addressed kscreen command would hit the FIRST match = the OLD output. Resolve OUR
// output's kscreen id (managed-prefix name AND current mode == the birth size; newest id
// wins) for the custom-mode install, and keep it as the kscreen-doctor position fallback.
let addr = resolve_kscreen_addr(&name, width, birth_h);
self.last_name = Some(addr.clone());
let active = set_custom_refresh(width, height, mode.refresh_hz, &addr); let active = set_custom_refresh(width, height, mode.refresh_hz, &addr);
// Accept only an active mode that IS our custom one: the exact requested height, and a // Accept only an active mode that IS our custom one: the exact requested height, and a
// width at or just below the request (a CVT alignment). That also proves the output // width at or just below the request (a CVT alignment). That also proves the output
@@ -275,8 +330,6 @@ impl VirtualDisplay for KwinDisplay {
let (nid, st) = spawn_vout(width, height)?; let (nid, st) = spawn_vout(width, height)?;
node_id = nid; node_id = nid;
stop = st; stop = st;
addr = resolve_kscreen_addr(&name, width, height);
self.last_name = Some(addr.clone());
tracing::info!( tracing::info!(
node_id, node_id,
width, width,
@@ -293,16 +346,17 @@ impl VirtualDisplay for KwinDisplay {
// `Primary` makes it the primary output but keeps the bootstrap/physical outputs enabled; // `Primary` makes it the primary output but keeps the bootstrap/physical outputs enabled;
// `Exclusive` makes it the SOLE desktop (others disabled, restored on teardown) — so // `Exclusive` makes it the SOLE desktop (others disabled, restored on teardown) — so
// plasmashell + windows land on the streamed surface, not the headless `kwin --virtual` // plasmashell + windows land on the streamed surface, not the headless `kwin --virtual`
// bootstrap output. Read from the policy (replacing the PUNKTFUNK_KWIN_VIRTUAL_PRIMARY boolean). // bootstrap output. Applied over kde_output_management_v2 in-process (immune to a wedged
use crate::policy::Topology; // kscreen-doctor backend; see `apply_topology`), with a kscreen-doctor fallback. `disabled`
let disabled = match crate::effective_topology() { // is the physical/bootstrap outputs, each `(name, "WxH@Hz")`, to restore on teardown.
Topology::Exclusive => apply_virtual_primary(&addr), let our_prefix = format!("Virtual-{name}");
Topology::Primary => { let disabled = self.apply_topology(&name, &our_prefix, final_dims);
apply_virtual_primary_only(&addr); // A plain managed name is enough for apply_position's kscreen-doctor fallback when the
Vec::new() // nothing disabled → nothing to restore // in-process UUID path isn't set (single-output sessions are unambiguous; a supersede uses
// the UUID path instead). `want_high` already set `last_name` to the resolved kscreen id.
if self.last_name.is_none() {
self.last_name = Some(our_prefix);
} }
Topology::Extend | Topology::Auto => Vec::new(),
};
// Per-group restore (§6.1): DON'T bind the re-enable to this session's keepalive (a per-session // Per-group restore (§6.1): DON'T bind the re-enable to this session's keepalive (a per-session
// `StopGuard` restore would re-enable the physical the moment the FIRST of several exclusive // `StopGuard` restore would re-enable the physical the moment the FIRST of several exclusive
// sessions drops — under a still-live sibling). Instead stash it as a closure the registry lifts // sessions drops — under a still-live sibling). Instead stash it as a closure the registry lifts
@@ -310,7 +364,12 @@ impl VirtualDisplay for KwinDisplay {
// that display's output is reclaimed, so KWin never sees zero outputs). Empty ⇒ nothing to restore. // that display's output is reclaimed, so KWin never sees zero outputs). Empty ⇒ nothing to restore.
self.pending_restore = (!disabled.is_empty()).then(|| { self.pending_restore = (!disabled.is_empty()).then(|| {
let disabled = disabled.clone(); let disabled = disabled.clone();
Box::new(move || reenable_outputs(&disabled)) as Box<dyn FnOnce() + Send> // In-process first; fall back to kscreen-doctor if the compositor doesn't answer in budget.
Box::new(move || {
if !crate::kwin_output_mgmt::reenable_outputs(&disabled) {
reenable_outputs_kscreen(&disabled);
}
}) as Box<dyn FnOnce() + Send>
}); });
// Layout position (§6.2) is applied by the registry via `apply_position` right after create // Layout position (§6.2) is applied by the registry via `apply_position` right after create
// (it owns the display group, so it computes auto-row / manual placement over the whole group). // (it owns the display group, so it computes auto-row / manual placement over the whole group).
@@ -324,10 +383,12 @@ impl VirtualDisplay for KwinDisplay {
} }
} }
/// Re-enable the outputs an `exclusive` topology disabled (bootstrap / physical), so KWin re-homes onto /// Re-enable the outputs an `exclusive` topology disabled (bootstrap / physical) via `kscreen-doctor`
/// them. Called by the registry when the display group's last member is torn down (design §6.1), BEFORE /// the fallback for the in-process [`crate::kwin_output_mgmt::reenable_outputs`], run by the restore
/// that member's output is reclaimed — so KWin is never momentarily left with zero enabled outputs. /// closure only when the in-process path reports the compositor didn't answer. Called by the registry
fn reenable_outputs(outputs: &[(String, String)]) { /// when the display group's last member is torn down (design §6.1), BEFORE that member's output is
/// reclaimed — so KWin is never momentarily left with zero enabled outputs.
fn reenable_outputs_kscreen(outputs: &[(String, String)]) {
if outputs.is_empty() { if outputs.is_empty() {
return; return;
} }
@@ -823,8 +884,9 @@ fn apply_virtual_primary_only(ours: &str) {
/// Dropping this releases the KWin virtual output: it flips the keepalive thread's `stop`, which /// Dropping this releases the KWin virtual output: it flips the keepalive thread's `stop`, which
/// drops the Wayland connection and makes KWin reclaim the output. The topology **restore** is no /// drops the Wayland connection and makes KWin reclaim the output. The topology **restore** is no
/// longer bound here — it moved to the registry's display group (§6.1, [`reenable_outputs`]), which /// longer bound here — it moved to the registry's display group (§6.1, restored in-process via
/// runs it once when the group's last member drops, BEFORE this keepalive is dropped. /// [`crate::kwin_output_mgmt::reenable_outputs`], `kscreen-doctor` fallback), which runs it once when
/// the group's last member drops, BEFORE this keepalive is dropped.
struct StopGuard { struct StopGuard {
stop: Arc<AtomicBool>, stop: Arc<AtomicBool>,
} }
@@ -0,0 +1,658 @@
//! In-process KDE output management (`kde_output_management_v2` + `kde_output_device_v2`).
//!
//! Topology — make the streamed output primary, disable the physical/bootstrap outputs, capture
//! their modes for restore, re-enable them on teardown, position the output — used to shell out to
//! `kscreen-doctor` (see [`super::kwin`]). But `kscreen-doctor` drives a *separate* stack:
//! libkscreen picks a backend and, depending on the setup, waits on the kscreen KDED module over
//! D-Bus. On a machine where THAT layer is wedged it blocks in its own connect and never returns —
//! a field report (Nobara, KWin 6.6.4) showed every topology `kscreen-doctor` timing out at its 5 s
//! budget, so the streamed output never became the desktop (`also_disabled=[]`) and bring-up took
//! ~26 s.
//!
//! The compositor's OWN Wayland is provably responsive on that same session — the host just created
//! a virtual output over it via `zkde_screencast` — so we drive `kde_output_management_v2` directly
//! over Wayland here, sidestepping whatever wedges the standalone tool. Every wait is time-bounded,
//! so a genuinely wedged compositor degrades to `handled = false` and the caller falls back to the
//! `kscreen-doctor` path rather than hanging.
//!
//! KWin advertises one `kde_output_device_v2` global per output (the classic model; verified live:
//! `kde_output_management_v2` v19, `kde_output_device_v2` v20 on KWin 6.6.4). We bind them all, read
//! each output's name / enabled / priority / current-mode size, then build a
//! `kde_output_configuration_v2` and `apply()` it, waiting for `applied` / `failed`.
#![allow(clippy::all, dead_code, non_camel_case_types, non_snake_case, unused)]
#![deny(clippy::undocumented_unsafe_blocks)]
use std::collections::HashMap;
use std::os::fd::{AsFd, AsRawFd};
use std::time::{Duration, Instant};
use wayland_client::backend::ObjectId;
use wayland_client::protocol::wl_callback::{self, WlCallback};
use wayland_client::protocol::wl_registry::{self, WlRegistry};
use wayland_client::{event_created_child, Connection, Dispatch, Proxy, QueueHandle};
// Generate the client bindings for the two vendored KDE protocols inline (no build.rs). The
// management protocol references the device interfaces, so they can't share one `__interfaces`
// module (each `generate_interfaces!` emits its own helper items, which collide). Instead — the
// interdependent-protocol pattern from the `wayland-protocols` crate — `device` is a self-contained
// module and `management` pulls in `device`'s interface statics + generated proxy types before its
// own codegen, so its cross-protocol object args (`kde_output_device_v2`, `…_mode_v2`) resolve.
#[allow(clippy::all, dead_code, non_camel_case_types, non_snake_case, unused)]
pub mod device {
use wayland_client;
use wayland_client::protocol::*;
pub mod __interfaces {
use wayland_client::protocol::__interfaces::*;
wayland_scanner::generate_interfaces!("protocols/kde-output-device-v2.xml");
}
use self::__interfaces::*;
wayland_scanner::generate_client_code!("protocols/kde-output-device-v2.xml");
}
#[allow(clippy::all, dead_code, non_camel_case_types, non_snake_case, unused)]
pub mod management {
use wayland_client;
use wayland_client::protocol::*;
pub mod __interfaces {
use super::super::device::__interfaces::*;
use wayland_client::protocol::__interfaces::*;
wayland_scanner::generate_interfaces!("protocols/kde-output-management-v2.xml");
}
use self::__interfaces::*;
// The device protocol's generated modules/types, so the foreign object args resolve.
use super::device::*;
wayland_scanner::generate_client_code!("protocols/kde-output-management-v2.xml");
}
use device::kde_output_device_mode_v2::{Event as ModeEvent, KdeOutputDeviceModeV2 as DeviceMode};
use device::kde_output_device_v2::{Event as DeviceEvent, KdeOutputDeviceV2 as OutputDevice};
use management::kde_output_configuration_v2::{
Event as ConfigEvent, KdeOutputConfigurationV2 as OutputConfig,
};
use management::kde_output_management_v2::KdeOutputManagementV2 as OutputManagement;
/// Highest interface versions we drive; we bind `min(advertised, MAX)`. Every request we issue is
/// `since ≤ 2` (`create_configuration`/`enable`/`mode`/`position`/`apply` are v1, `set_primary_output`
/// is v2) and every event we read is `since ≤ 18` (`priority`), so binding high and calling low is
/// always in range on any KWin that advertises the globals.
const MGMT_MAX: u32 = 22;
const DEVICE_MAX: u32 = 24;
/// The opcode of `kde_output_device_v2.mode` (0-based event index) — the event that creates a child
/// `kde_output_device_mode_v2`. Kept in sync with the vendored `kde-output-device-v2.xml`.
const DEVICE_MODE_EVENT_OPCODE: u16 = 2;
/// Overall budget for one enumerate-then-apply operation. Generous next to a healthy roundtrip (a
/// few ms); it exists only so a wedged compositor can't pin the session's stream thread.
const OP_BUDGET: Duration = Duration::from_secs(3);
/// Poll slice while waiting on the Wayland fd (matches the keepalive loop's cadence in `kwin.rs`).
const POLL_MS: i32 = 100;
/// Which topology to apply once our output is resolved.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum TopologyKind {
/// Make ours the sole desktop: primary + disable every other enabled output.
Exclusive,
/// Make ours primary but leave the other outputs enabled.
Primary,
}
/// Outcome of [`apply_topology`].
pub(crate) struct TopologyOutcome {
/// UUID of our resolved virtual output — a stable per-output id that survives a mode-switch
/// supersede (unlike the shared name) — for later [`set_position`] / restore addressing.
pub our_uuid: Option<String>,
/// The outputs we disabled, each `(name, "WxH@Hz")`, so teardown can restore them at their exact
/// mode. Empty for `Primary`, or when nothing else was enabled.
pub disabled: Vec<(String, String)>,
/// `true` if the in-process path bound management, resolved our output, and applied (or tried to)
/// a configuration. `false` ⇒ the compositor didn't answer in budget or our output never
/// appeared, so the caller should fall back to `kscreen-doctor`.
pub handled: bool,
}
/// One output as read from `kde_output_device_v2`.
#[derive(Default, Clone)]
struct DeviceState {
/// The global `name` number (higher = more recently advertised) — used to pick the newest of two
/// same-named outputs during a supersede.
global: u32,
name: Option<String>,
uuid: Option<String>,
enabled: bool,
/// KWin's output priority; 1 is the primary. `None` until the `priority` event (device ≥ v18).
priority: Option<u32>,
/// The `current_mode` object id; its size is looked up in [`State::mode_dims`].
current_mode: Option<ObjectId>,
/// Every mode this output advertised, in announce order — `(mode object id, proxy)` — so restore
/// can pick the one matching a captured `WxH@Hz`.
modes: Vec<(ObjectId, DeviceMode)>,
/// Set once this output's `done` burst has been seen (its state is coherent to read).
seen_done: bool,
proxy: Option<OutputDevice>,
}
/// Everything the enumerate/apply queue accumulates on one connection.
#[derive(Default)]
struct State {
management: Option<OutputManagement>,
mgmt_name_version: Option<(u32, u32)>,
devices: HashMap<ObjectId, DeviceState>,
/// mode object id → `(width, height, refresh_mHz)`.
mode_dims: HashMap<ObjectId, (u32, u32, u32)>,
/// Highest `wl_callback` serial whose `done` has arrived — the barrier the pump waits on.
sync_done: u32,
/// Configuration apply verdict: `Some(true)` = applied, `Some(false)` = failed.
applied: Option<bool>,
failure_reason: Option<String>,
}
impl Dispatch<WlRegistry, ()> for State {
fn event(
state: &mut Self,
registry: &WlRegistry,
event: wl_registry::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
match event {
wl_registry::Event::Global {
name,
interface,
version,
} => {
if interface == OutputManagement::interface().name {
let v = version.min(MGMT_MAX);
state.management =
Some(registry.bind::<OutputManagement, _, _>(name, v, qh, ()));
state.mgmt_name_version = Some((name, v));
} else if interface == OutputDevice::interface().name {
let v = version.min(DEVICE_MAX);
// The device's `name` global carries into the device's UserData so the event
// handler can record it (newest-wins tie-break during a supersede).
let dev = registry.bind::<OutputDevice, _, _>(name, v, qh, name);
let id = dev.id();
state.devices.entry(id).or_default().proxy = Some(dev);
}
}
wl_registry::Event::GlobalRemove { .. } => {}
_ => {}
}
}
}
// Management has no events.
impl Dispatch<OutputManagement, ()> for State {
fn event(
_: &mut Self,
_: &OutputManagement,
_: management::kde_output_management_v2::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
/// The device's UserData is its global `name` number.
impl Dispatch<OutputDevice, u32> for State {
fn event(
state: &mut Self,
device: &OutputDevice,
event: DeviceEvent,
global: &u32,
_: &Connection,
_: &QueueHandle<Self>,
) {
let entry = state.devices.entry(device.id()).or_default();
entry.global = *global;
if entry.proxy.is_none() {
entry.proxy = Some(device.clone());
}
match event {
DeviceEvent::Name { name } => entry.name = Some(name),
DeviceEvent::Uuid { uuid } => entry.uuid = Some(uuid),
DeviceEvent::Enabled { enabled } => entry.enabled = enabled != 0,
DeviceEvent::Priority { priority } => entry.priority = Some(priority),
DeviceEvent::CurrentMode { mode } => entry.current_mode = Some(mode.id()),
DeviceEvent::Mode { mode } => entry.modes.push((mode.id(), mode)),
DeviceEvent::Done => entry.seen_done = true,
_ => {}
}
}
// The `mode` event hands us a server-created `kde_output_device_mode_v2`. The opcode is a bare
// literal (the macro's fragment matcher rejects a `const` in some wayland-client versions); it is
// pinned to `DEVICE_MODE_EVENT_OPCODE` by `mode_event_opcode_is_two` below.
event_created_child!(State, OutputDevice, [
2 => (DeviceMode, ()),
]);
}
impl Dispatch<DeviceMode, ()> for State {
fn event(
state: &mut Self,
mode: &DeviceMode,
event: ModeEvent,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
let entry = state.mode_dims.entry(mode.id()).or_insert((0, 0, 0));
match event {
ModeEvent::Size { width, height } => {
entry.0 = width.max(0) as u32;
entry.1 = height.max(0) as u32;
}
ModeEvent::Refresh { refresh } => entry.2 = refresh.max(0) as u32,
_ => {}
}
}
}
impl Dispatch<OutputConfig, ()> for State {
fn event(
state: &mut Self,
_: &OutputConfig,
event: ConfigEvent,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
ConfigEvent::Applied => state.applied = Some(true),
ConfigEvent::Failed => state.applied = Some(false),
ConfigEvent::FailureReason { reason } => state.failure_reason = Some(reason),
_ => {}
}
}
}
impl Dispatch<WlCallback, u32> for State {
fn event(
state: &mut Self,
_: &WlCallback,
event: wl_callback::Event,
serial: &u32,
_: &Connection,
_: &QueueHandle<Self>,
) {
if let wl_callback::Event::Done { .. } = event {
state.sync_done = state.sync_done.max(*serial);
}
}
}
/// A connected, bound output-management session on its own Wayland connection.
struct Session {
conn: Connection,
queue: wayland_client::EventQueue<State>,
state: State,
next_sync: u32,
}
impl Session {
/// Connect to the KWin Wayland socket, bind `kde_output_management_v2` + every
/// `kde_output_device_v2`, and read each output's state — all bounded by `OP_BUDGET`. `None` if
/// we can't connect, the management global isn't advertised, or the compositor doesn't answer in
/// budget (the wedge case — the caller then falls back to `kscreen-doctor`).
fn open() -> Option<Session> {
let conn = Connection::connect_to_env().ok()?;
let queue = conn.new_event_queue();
let qh = queue.handle();
let _registry = conn.display().get_registry(&qh, ());
let mut s = Session {
conn,
queue,
state: State::default(),
next_sync: 0,
};
let deadline = Instant::now() + OP_BUDGET;
// Phase 1: process the registry globals (binds management + every device in the handler).
if !s.sync_barrier(deadline) {
return None;
}
if s.state.management.is_none() {
tracing::debug!(
"KWin does not advertise kde_output_management_v2 to this client — kscreen-doctor \
fallback"
);
return None;
}
// Phase 2: flush the device binds issued in phase 1 and drain each output's state burst
// (name / enabled / priority / current_mode / mode sizes / done).
if !s.sync_barrier(deadline) {
return None;
}
Some(s)
}
/// Send a `wl_display.sync` and pump the queue until its `done` arrives or `deadline` passes.
/// Returns `true` on the barrier, `false` on timeout.
fn sync_barrier(&mut self, deadline: Instant) -> bool {
self.next_sync += 1;
let serial = self.next_sync;
let qh = self.queue.handle();
let _cb = self.conn.display().sync(&qh, serial);
self.pump_until(deadline, |st| st.sync_done >= serial)
}
/// Bounded manual event loop: flush, dispatch what's queued, then poll the connection fd for up
/// to `POLL_MS` and read. Mirrors the keepalive loop in `kwin.rs::run` (blocking_dispatch can't
/// be interrupted, so we poll the fd instead). Returns `true` once `done(&state)` holds.
fn pump_until(&mut self, deadline: Instant, done: impl Fn(&State) -> bool) -> bool {
loop {
if done(&self.state) {
return true;
}
if self.queue.dispatch_pending(&mut self.state).is_err() {
return false;
}
if done(&self.state) {
return true;
}
if Instant::now() >= deadline {
return false;
}
if self.conn.flush().is_err() {
return false;
}
let Some(guard) = self.conn.prepare_read() else {
continue; // events already queued — loop dispatches them
};
let mut pfd = libc::pollfd {
fd: self.conn.as_fd().as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let remaining = deadline.saturating_duration_since(Instant::now());
let timeout = (remaining.as_millis() as i32).clamp(0, POLL_MS);
// SAFETY: `&mut pfd` points at one live, fully-initialized `libc::pollfd` on the stack and
// the count `1` matches that single element, so `poll` reads `fd`/`events` and writes
// `revents` strictly within `pfd`. `pfd.fd` is the Wayland connection's fd, valid because
// `self.conn` (and the `prepare_read` guard) outlive the call. `poll` blocks up to
// `timeout` ms and writes only `revents`; `pfd` is a fresh local that aliases nothing.
let r = unsafe { libc::poll(&mut pfd, 1, timeout) };
if r > 0 && (pfd.revents & libc::POLLIN) != 0 {
let _ = guard.read();
} // else: timeout/signal — drop the guard, re-check the deadline
}
}
/// A fresh `kde_output_configuration_v2` on this connection.
fn new_config(&self) -> OutputConfig {
let qh = self.queue.handle();
self.state
.management
.as_ref()
.unwrap()
.create_configuration(&qh, ())
}
/// `apply()` the config and pump until `applied`/`failed` or the deadline. Returns the verdict
/// (`true` applied, `false` failed/timeout).
fn apply(&mut self, config: &OutputConfig, deadline: Instant) -> bool {
self.state.applied = None;
config.apply();
let ok = self.pump_until(deadline, |st| st.applied.is_some());
if !ok {
return false;
}
matches!(self.state.applied, Some(true))
}
/// The current-mode size of a device as `(w, h, refresh_mHz)`, if known.
fn current_dims(&self, dev: &DeviceState) -> Option<(u32, u32, u32)> {
let id = dev.current_mode.as_ref()?;
self.state.mode_dims.get(id).copied()
}
}
/// `(width, height, "WxH@Hz")` capture of a device's current mode, Hz rounded — the same shape the
/// `kscreen-doctor` restore path used, so teardown can put a panel back at its real refresh.
fn mode_spec(dims: (u32, u32, u32)) -> String {
let hz = ((dims.2 as f64) / 1000.0).round() as u32;
format!("{}x{}@{}", dims.0, dims.1, hz)
}
/// Prefix EVERY managed KWin output shares (mirrors `kwin::MANAGED_PREFIX`) — the streamed outputs
/// are `Virtual-punktfunk` / `Virtual-punktfunk-<id>`, so a same-family sibling session is never
/// treated as a physical to disable, and its primary is never stolen (first-slot-wins).
const MANAGED_PREFIX: &str = "Virtual-punktfunk";
/// Make the streamed output (name starts with `our_prefix`, current size `our_w`×`our_h`) the
/// primary — and, for `Exclusive`, disable every other enabled output — over `kde_output_management_v2`.
/// See the module docs for why this is done in-process instead of via `kscreen-doctor`.
pub(crate) fn apply_topology(
our_prefix: &str,
our_w: u32,
our_h: u32,
kind: TopologyKind,
) -> TopologyOutcome {
let miss = || TopologyOutcome {
our_uuid: None,
disabled: Vec::new(),
handled: false,
};
let Some(mut sess) = Session::open() else {
return miss();
};
let deadline = Instant::now() + OP_BUDGET;
// Resolve OUR output: managed-prefix name AND current size == the birth size (only the
// just-created output sits there during a supersede); newest global wins the tie.
let ours = sess
.state
.devices
.values()
.filter(|d| {
d.name.as_deref().is_some_and(|n| n.starts_with(our_prefix))
&& sess.current_dims(d).map(|(w, h, _)| (w, h)) == Some((our_w, our_h))
})
.max_by_key(|d| d.global)
.cloned();
let Some(ours) = ours else {
tracing::warn!(
our_prefix,
our_w,
our_h,
"KWin output management: our virtual output hasn't appeared yet — kscreen-doctor fallback"
);
return miss();
};
let our_uuid = ours.uuid.clone();
let our_id = ours.proxy.as_ref().map(|p| p.id());
// First-slot-wins (§6.1): don't steal primary if another managed sibling already holds it
// (priority 1) — a 2nd exclusive session joins as a secondary of the shared desktop.
let sibling_is_primary = sess.state.devices.values().any(|d| {
d.enabled
&& d.priority == Some(1)
&& d.proxy.as_ref().map(|p| p.id()) != our_id
&& d.name
.as_deref()
.is_some_and(|n| n.starts_with(MANAGED_PREFIX))
});
// The physical/bootstrap outputs to disable for `Exclusive`: enabled, not any managed sibling,
// not ours. Captured WITH their current mode so teardown restores the exact refresh.
let mut to_disable: Vec<(OutputDevice, String, String)> = Vec::new();
if kind == TopologyKind::Exclusive {
for d in sess.state.devices.values() {
let is_ours = d.proxy.as_ref().map(|p| p.id()) == our_id;
let managed = d
.name
.as_deref()
.is_some_and(|n| n.starts_with(MANAGED_PREFIX));
if d.enabled && !is_ours && !managed {
if let (Some(name), Some(proxy)) = (d.name.clone(), d.proxy.clone()) {
let spec = sess.current_dims(d).map(mode_spec).unwrap_or_default();
to_disable.push((proxy, name, spec));
}
}
}
}
// Build one configuration: ensure ours is enabled, take primary (unless a sibling holds it),
// disable the others. `apply()` is atomic — KWin re-homes the shell onto the remaining desktop.
let config = sess.new_config();
if let Some(proxy) = ours.proxy.as_ref() {
config.enable(proxy, 1);
if !sibling_is_primary {
config.set_primary_output(proxy);
}
}
for (proxy, _, _) in &to_disable {
config.enable(proxy, 0);
}
let applied = sess.apply(&config, deadline);
config.destroy();
// Always report the outputs we ASKED to disable so teardown restores them: re-enabling an output
// that never actually got disabled is a harmless no-op, whereas dropping them here would strand a
// physical dark if KWin processed the disable but the `applied` ack didn't land in budget.
let disabled: Vec<(String, String)> = to_disable
.into_iter()
.map(|(_, name, spec)| (name, spec))
.collect();
if applied {
tracing::info!(
also_disabled = ?disabled,
primary_taken = !sibling_is_primary,
"KWin output management: streamed output set as the desktop (in-process)"
);
} else {
tracing::warn!(
reason = ?sess.state.failure_reason,
also_disabled = ?disabled,
"KWin output management: apply() not confirmed in budget — proceeding (restore will re-enable)"
);
}
// We resolved our output and drove the config over Wayland; don't ALSO run kscreen-doctor — that
// would double-apply (and on a wedged box it would just add 26 s of timeouts). `handled` is true
// even on an unconfirmed apply; a genuinely absent management global / unresolved output took the
// `handled = false` early returns above and falls back to kscreen-doctor.
TopologyOutcome {
our_uuid,
disabled,
handled: true,
}
}
/// Re-enable outputs by name at their captured `WxH@Hz` modes (teardown), in-process. Returns
/// `true` if the config applied; `false` (compositor unresponsive / management absent) tells the
/// caller to fall back to `kscreen-doctor`.
pub(crate) fn reenable_outputs(outputs: &[(String, String)]) -> bool {
if outputs.is_empty() {
return true;
}
let Some(mut sess) = Session::open() else {
return false;
};
let deadline = Instant::now() + OP_BUDGET;
let config = sess.new_config();
for (name, spec) in outputs {
// Find the device by name (physical names are stable across a session).
let Some(dev) = sess
.state
.devices
.values()
.find(|d| d.name.as_deref() == Some(name.as_str()))
.cloned()
else {
continue;
};
let Some(proxy) = dev.proxy.as_ref() else {
continue;
};
// Enable first — a bare enable always succeeds, so a physical is never left dark.
config.enable(proxy, 1);
// Then re-assert the captured mode so a 120 Hz panel doesn't return at KWin's ~60 Hz default.
if let Some(mode) = find_mode(&sess, &dev, spec) {
config.mode(proxy, &mode);
}
}
let ok = sess.apply(&config, deadline);
config.destroy();
if ok {
tracing::info!(reenabled = ?outputs, "KWin output management: restored outputs (in-process)");
}
ok
}
/// Position the output identified by `uuid` at `(x, y)` in the desktop layout, in-process. Returns
/// `true` if applied; `false` tells the caller to fall back to `kscreen-doctor`.
pub(crate) fn set_position(uuid: &str, x: i32, y: i32) -> bool {
let Some(mut sess) = Session::open() else {
return false;
};
let deadline = Instant::now() + OP_BUDGET;
let Some(dev) = sess
.state
.devices
.values()
.find(|d| d.uuid.as_deref() == Some(uuid))
.cloned()
else {
return false;
};
let Some(proxy) = dev.proxy.as_ref() else {
return false;
};
let config = sess.new_config();
config.position(proxy, x, y);
let ok = sess.apply(&config, deadline);
config.destroy();
if ok {
tracing::info!(
uuid,
x,
y,
"KWin output management: placed output (in-process)"
);
}
ok
}
/// Find a device's advertised mode proxy matching a captured `"WxH@Hz"` spec (Hz rounded), for
/// restore. `None` if the spec is empty or no mode matches (the caller then enables without a mode
/// and lets KWin pick its preferred one).
fn find_mode(sess: &Session, dev: &DeviceState, spec: &str) -> Option<DeviceMode> {
if spec.is_empty() {
return None;
}
let (wh, hz) = spec.split_once('@')?;
let (w, h) = wh.split_once('x')?;
let (w, h, hz): (u32, u32, u32) = (w.parse().ok()?, h.parse().ok()?, hz.parse().ok()?);
dev.modes.iter().find_map(|(id, proxy)| {
let (mw, mh, mmhz) = sess.state.mode_dims.get(id).copied()?;
let mhz = ((mmhz as f64) / 1000.0).round() as u32;
((mw, mh, mhz) == (w, h, hz)).then(|| proxy.clone())
})
}
#[cfg(test)]
mod tests {
use super::*;
/// The `WxH@Hz` capture rounds mHz to whole Hz — the shape teardown parses back.
#[test]
fn mode_spec_rounds_millihertz() {
assert_eq!(mode_spec((2560, 1440, 59940)), "2560x1440@60");
assert_eq!(mode_spec((1920, 1080, 60000)), "1920x1080@60");
assert_eq!(mode_spec((3840, 2160, 119880)), "3840x2160@120");
}
/// The vendored device XML must keep `mode` at the opcode the `event_created_child!` macro
/// hardcodes — a reorder there would bind the child to the wrong event and desync mode sizes.
#[test]
fn mode_event_opcode_is_two() {
assert_eq!(DEVICE_MODE_EVENT_OPCODE, 2);
}
}