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:
Generated
+1
@@ -3024,6 +3024,7 @@ version = "0.19.1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"ashpd",
|
"ashpd",
|
||||||
|
"bitflags",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user