mirror of
https://github.com/wayvr-org/wayvr.git
synced 2026-10-28 14:02:26 +01:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92009e56fa | ||
|
|
eeff59abeb | ||
|
|
3b8b73c0c3 | ||
|
|
07ae2c0037 |
No files matched your search
@@ -1,46 +0,0 @@
|
||||
# Contributing
|
||||
|
||||
Contributions are warmly welcome. Before you start, please read the entire document carefully.
|
||||
|
||||
## AI Policy
|
||||
|
||||
Do not use LLMs to generate substantial pieces of code. You need to fully understand every line of code you change. No exceptions.
|
||||
|
||||
Commits, issues and pull requests must be made by humans. Co-authoring by AI is not allowed.
|
||||
Looking to create an issue on the project, such as a feature request or bug report? Focus on writing what you know for sure in simple terms. We'll figure it out from there.
|
||||
|
||||
Usage of Gen-AI for visuals and audio is prohibited.
|
||||
|
||||
The only exception is LLM-translated strings generated by the scripts/translator tool.
|
||||
|
||||
## Basics
|
||||
|
||||
Before committing, make sure to format your Rust code by running:
|
||||
|
||||
```
|
||||
cargo fmt
|
||||
```
|
||||
|
||||
You should also check for any compiler warnings too:
|
||||
|
||||
```
|
||||
cargo check
|
||||
```
|
||||
|
||||
or even better:
|
||||
|
||||
```
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
## Large contributions
|
||||
|
||||
If you want to submit a relatively big pull requests, it's highly advisable to create a separate issue request for it. That way we can discuss the approach and sort out the details first.
|
||||
|
||||
## Adding translations
|
||||
|
||||
Look at `scripts/translator/run.sh` for more information.
|
||||
|
||||
## Licensing
|
||||
|
||||
WayVR is developed and published under [GPL-3 License](https://www.gnu.org/licenses/gpl-3.0.en.html).
|
||||
Generated
+812
-604
File diff suppressed because it is too large.
Load diff
@@ -23,7 +23,6 @@ dbus = { version = "0.9.12" }
|
||||
glam = { version = "0.30.9", features = ["mint", "serde"] }
|
||||
idmap = "0.2.2"
|
||||
idmap-derive = "0.2.22"
|
||||
image = { version = "0.25.10", default-features = false, features = ["png"] }
|
||||
interprocess = { version = "2.2.3" }
|
||||
libc = "0.2.178"
|
||||
log = "0.4.29"
|
||||
|
||||
@@ -1008,7 +1008,7 @@ fn create_dropdown<B: 'static + BindingsDropdown>(
|
||||
.iter()
|
||||
.map(|item| {
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(item.action_str(&action, side)),
|
||||
action_name: Some(item.action_str(&action, side).into()),
|
||||
title: item.translation(),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -1019,7 +1019,7 @@ fn create_dropdown<B: 'static + BindingsDropdown>(
|
||||
cells.insert(
|
||||
0,
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(action_str),
|
||||
action_name: Some(action_str.into()),
|
||||
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
|
||||
..Default::default()
|
||||
}),
|
||||
|
||||
@@ -1,32 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This is the translation tool intended for machine-translating strings.
|
||||
#
|
||||
# ###########################
|
||||
# # Adding new translations #
|
||||
# ###########################
|
||||
#
|
||||
# If you want to add a new language, follow these steps:
|
||||
#
|
||||
# 1. Include the language in `wlx-common/src/locale.rs`:
|
||||
# - add your entry in `enum Language`
|
||||
# - add your entry in Language::code() function
|
||||
# - add your language code in Language::add_codes() function
|
||||
#
|
||||
# 2. Create a new template file
|
||||
# - Add your language code in `scripts/translator/templates/<lang code>.json`
|
||||
# (you can copy one of nearby files and adapt them accordingly)
|
||||
#
|
||||
# 3. Modify this very file
|
||||
# - Go to the bottom and add your language code entry here too
|
||||
#
|
||||
# 4. Run the script (it will hint you which LANG_PATHs are supported)
|
||||
#
|
||||
# ############################################
|
||||
# # Running machine translations (local LLM) #
|
||||
# ############################################
|
||||
#
|
||||
# Do not run this script if you don't have at least ~16 GiB of free system memory.
|
||||
# Do not run this script if you don't have at least 32 GiB of system memory.
|
||||
# Submitting PRs that lack translated strings is okay if you don't meet the
|
||||
# system requirements to run this script, or if you simply prefer not to; we are
|
||||
# regularly updating the missing translation strings anyway.
|
||||
@@ -34,18 +8,15 @@
|
||||
# Base language: English (en.json)
|
||||
#
|
||||
# This tool expects llama.cpp server being present (llama-server) at port 8080.
|
||||
#
|
||||
# Currently we're using Gemma 4 for machine translations.
|
||||
# Model URL:
|
||||
# https://huggingface.co/unsloth/gemma-4-31B-it-GGUF
|
||||
# Recommended quantization: Q4_K_M
|
||||
#
|
||||
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
bun install
|
||||
|
||||
# model URL:
|
||||
# https://huggingface.co/unsloth/gemma-4-31B-it-GGUF
|
||||
# you can specify your own if you want to.
|
||||
if [ -z "${MODEL}" ]; then
|
||||
export MODEL="gemma-4-31B-it-Q4_K_M"
|
||||
fi
|
||||
|
||||
+1
-3
@@ -54,8 +54,7 @@ strum.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
uuid = { workspace = true }
|
||||
wayland-client = { workspace = true }
|
||||
wayland-protocols = { workspace = true, features = ["server", "staging"] }
|
||||
wayland-scanner = "0.31.10"
|
||||
wayland-protocols = { workspace = true }
|
||||
wayland-protocols-misc = { workspace = true }
|
||||
winit = { workspace = true, optional = true }
|
||||
xdg.workspace = true
|
||||
@@ -64,7 +63,6 @@ bytes = { workspace = true }
|
||||
chrono-tz = "0.10.4"
|
||||
config = "0.15.25"
|
||||
futures = "0.3.32"
|
||||
image.workspace = true
|
||||
image_dds = {
|
||||
version = "0.7.2",
|
||||
default-features = false,
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="xdg_session_management_v1">
|
||||
<copyright>
|
||||
Copyright 2018 Mike Blumenkrantz
|
||||
Copyright 2018 Samsung Electronics Co., Ltd
|
||||
Copyright 2018 Red Hat Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<description summary="Protocol for managing application sessions">
|
||||
This description provides a high-level overview of the interplay between
|
||||
the interfaces defined this protocol. For details, see the protocol
|
||||
specification.
|
||||
|
||||
The xdg_session_manager protocol declares interfaces necessary to
|
||||
allow clients to restore toplevel state from previous executions. The
|
||||
xdg_session_manager_v1.get_session request can be used to obtain a
|
||||
xdg_session_v1 resource representing the state of a set of toplevels.
|
||||
|
||||
Clients may obtain the session string to use in future calls through
|
||||
the xdg_session_v1.created event. Compositors will use this string
|
||||
as an identifiable token for future runs, possibly storing data about
|
||||
the related toplevels in persistent storage. Clients that wish to
|
||||
track sessions in multiple environments may use the $XDG_CURRENT_DESKTOP
|
||||
environment variable.
|
||||
|
||||
Toplevels are managed through the xdg_session_v1.add_toplevel and
|
||||
xdg_session_v1.remove_toplevel pair of requests. Clients will explicitly
|
||||
request a toplevel to be restored according to prior state through the
|
||||
xdg_session_v1.restore_toplevel request before the toplevel is mapped.
|
||||
|
||||
Compositors may store session information up to any arbitrary level, and
|
||||
apply any limits and policies to the amount of data stored and its lifetime.
|
||||
Clients must account for missing sessions and partial session restoration.
|
||||
|
||||
Warning! The protocol described in this file is currently in the testing
|
||||
phase. Backward compatible changes may be added together with the
|
||||
corresponding interface version bump. Backward incompatible changes can
|
||||
only be done by creating a new major version of the extension.
|
||||
</description>
|
||||
|
||||
<interface name="xdg_session_manager_v1" version="1">
|
||||
<description summary="manage sessions for applications">
|
||||
The xdg_session_manager_v1 interface defines base requests for creating and
|
||||
managing a session for an application. Sessions persist across application
|
||||
and compositor restarts unless explicitly destroyed. A session is created
|
||||
for the purpose of maintaining an application's xdg_toplevel surfaces
|
||||
across compositor or application restarts. The compositor should remember
|
||||
as many states as possible for surfaces in a given session, but there is
|
||||
no requirement for which states must be remembered.
|
||||
|
||||
Policies such as cache eviction are declared an implementation detail of
|
||||
the compositor. Clients should account for no longer existing sessions.
|
||||
</description>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="in_use" summary="a requested session is already in use"
|
||||
value="1"/>
|
||||
<entry name="invalid_session_id" summary="invalid session identifier"
|
||||
value="2"/>
|
||||
<entry name="invalid_reason" summary="invalid reason" value="3"/>
|
||||
</enum>
|
||||
|
||||
<enum name="reason">
|
||||
<description summary="reason for getting a session">
|
||||
The reason may determine in what way a session restores the window
|
||||
management state of associated toplevels.
|
||||
|
||||
For example newly launched applications might be launched on the active
|
||||
workspace with restored size and position, while a recovered
|
||||
application might restore additional state such as active workspace and
|
||||
stacking order.
|
||||
</description>
|
||||
<entry name="launch" value="1">
|
||||
<description summary="an app is newly launched">
|
||||
A new app instance is launched, for example from an app launcher.
|
||||
</description>
|
||||
</entry>
|
||||
<entry name="recover" value="2">
|
||||
<description summary="an app recovered">
|
||||
An app instance is recovering from for example a compositor or app crash.
|
||||
</description>
|
||||
</entry>
|
||||
<entry name="session_restore" value="3">
|
||||
<description summary="an app restored">
|
||||
An app instance is restored, for example part of a restored session, or
|
||||
restored from having been temporarily terminated due to resource
|
||||
constraints.
|
||||
</description>
|
||||
</entry>
|
||||
</enum>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="Destroy this object">
|
||||
Destroy the manager object. The existing session objects will be
|
||||
unaffected.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="get_session">
|
||||
<description summary="create or restore a session">
|
||||
Create a session object corresponding to either an existing session
|
||||
identified by the given session identifier string or a new session.
|
||||
While the session object exists, the session is considered to be "in
|
||||
use".
|
||||
|
||||
If an identifier string represents a session that is currently actively
|
||||
in use by the same client, an 'in_use' error is raised. If some
|
||||
other client is currently using the same session, the new session will
|
||||
replace managing the associated state.
|
||||
|
||||
If the reason is not a valid enum entry, the 'invalid_reason' protocol
|
||||
error is raised.
|
||||
|
||||
NULL is passed to initiate a new session. If a session_id is passed
|
||||
which does not represent a valid session, the compositor treats it as if
|
||||
NULL had been passed.
|
||||
|
||||
The session id string must be UTF-8 encoded. It is also limited by the
|
||||
maximum length of wayland messages (around 4KB). The 'invalid_session_id'
|
||||
protocol error will be raised if an invalid string is provided.
|
||||
|
||||
A client is allowed to have any number of in use sessions at the same
|
||||
time.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="xdg_session_v1"/>
|
||||
<arg name="reason" type="uint" enum="reason"
|
||||
summary="reason for session"/>
|
||||
<arg name="session_id" type="string"
|
||||
summary="the session to restore"
|
||||
allow-null="true"/>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="xdg_session_v1" version="1">
|
||||
<description summary="A session for an application">
|
||||
A xdg_session_v1 object represents a session for an application. While the
|
||||
object exists, all surfaces which have been added to the session will
|
||||
have states stored by the compositor which can be reapplied at a later
|
||||
time. Two sessions cannot exist for the same identifier string.
|
||||
|
||||
States for surfaces added to a session are automatically updated by the
|
||||
compositor when they are changed.
|
||||
</description>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="name_in_use"
|
||||
summary="toplevel name is already in use"
|
||||
value="1"/>
|
||||
<entry name="already_mapped"
|
||||
summary="toplevel was already mapped when restored"
|
||||
value="2"/>
|
||||
<entry name="invalid_name"
|
||||
summary="provided toplevel name is invalid"
|
||||
value="3"/>
|
||||
<entry name="already_added"
|
||||
summary="toplevel already added"
|
||||
value="4"/>
|
||||
</enum>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="Destroy the session">
|
||||
Destroy a session object, preserving the current state but not continuing
|
||||
to make further updates if state changes occur. This makes the associated
|
||||
xdg_toplevel_session_v1 objects inert.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="remove" type="destructor">
|
||||
<description summary="Remove the session">
|
||||
Remove the session, making it no longer available for restoration. A
|
||||
compositor should in response to this request remove the data related to
|
||||
this session from its storage.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="add_toplevel">
|
||||
<description summary="add a new surface to the session">
|
||||
Attempt to add a given surface to the session. The passed name is used
|
||||
to identify what window is being restored, and may be used to store
|
||||
window specific state within the session.
|
||||
|
||||
The name given to the toplevel must not correspond to any previously
|
||||
existing toplevel names in the session. If the name matches an already
|
||||
known toplevel name in the session, a 'name_in_use' protocol error will
|
||||
be raised.
|
||||
|
||||
The toplevel object must not be added more than once to any session
|
||||
created by the client, otherwise the 'already_added' protocol error
|
||||
will be raised.
|
||||
|
||||
This request will return a xdg_toplevel_session_v1 for later
|
||||
manipulation. As this resource is created from an empty initial state,
|
||||
compositors must not emit a xdg_toplevel_session_v1.restored event for
|
||||
resources created through this request.
|
||||
|
||||
The name string must be UTF-8 encoded. It is also limited by the maximum
|
||||
length of wayland messages (around 4KB). The 'invalid_name' protocol
|
||||
error will be raised if an invalid string is provided.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="xdg_toplevel_session_v1"/>
|
||||
<arg name="toplevel" type="object" interface="xdg_toplevel"/>
|
||||
<arg name="name" type="string" summary="name identifying the toplevel"/>
|
||||
</request>
|
||||
|
||||
<request name="restore_toplevel">
|
||||
<description summary="restore a surface state">
|
||||
Inform the compositor that the toplevel associated with the passed name
|
||||
should have its window management state restored.
|
||||
|
||||
If the toplevel name was previously granted to another xdg_toplevel,
|
||||
the 'name_in_use' protocol error will be raised.
|
||||
|
||||
The toplevel object must not be added more than once to any session
|
||||
created by the client, otherwise the 'already_added' protocol error
|
||||
will be raised.
|
||||
|
||||
This request must be called prior to the first commit on the associated
|
||||
wl_surface after creating the toplevel, otherwise an 'already_mapped'
|
||||
error is raised.
|
||||
|
||||
As part of the initial configure sequence, if the toplevel was
|
||||
successfully restored, a xdg_toplevel_session_v1.restored event is
|
||||
emitted. If the toplevel name was not known in the session, this request
|
||||
will be equivalent to the xdg_toplevel_session_v1.add_toplevel request,
|
||||
and no such event will be emitted. See the xdg_toplevel_session_v1.restored
|
||||
event for further details.
|
||||
|
||||
The name string must be UTF-8 encoded. It is also limited by the maximum
|
||||
length of wayland messages (around 4KB). The 'invalid_name' protocol
|
||||
error will be raised if an invalid string is provided.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="xdg_toplevel_session_v1"/>
|
||||
<arg name="toplevel" type="object" interface="xdg_toplevel"/>
|
||||
<arg name="name" type="string" summary="name identifying the toplevel"/>
|
||||
</request>
|
||||
|
||||
<request name="remove_toplevel">
|
||||
<description summary="remove a surface from the session">
|
||||
Remove a specified surface from the session and render any related
|
||||
xdg_toplevel_session_v1 object inert. The compositor should remove any
|
||||
data related to the toplevel in the corresponding session from its internal
|
||||
storage.
|
||||
|
||||
The window is specified by its name in the session. The name string
|
||||
must be encoded in UTF-8, and it is limited in size by the maximum
|
||||
length of wayland messages (around 4KB).
|
||||
</description>
|
||||
<arg name="name" type="string" summary="name identifying the toplevel"/>
|
||||
</request>
|
||||
|
||||
<event name="created">
|
||||
<description summary="newly-created session id">
|
||||
Emitted at most once some time after getting a new session object. It
|
||||
means that no previous state was restored, and a new session was created.
|
||||
The passed id can be persistently stored and used to restore previous
|
||||
sessions.
|
||||
</description>
|
||||
<arg name="session_id" type="string"/>
|
||||
</event>
|
||||
|
||||
<event name="restored">
|
||||
<description summary="the session has been restored">
|
||||
Emitted at most once some time after getting a new session object. It
|
||||
means that previous state was at least partially restored. The same id
|
||||
can again be used to restore previous sessions.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="replaced">
|
||||
<description summary="the session has been replaced">
|
||||
Emitted at most once, if the session was taken over by some other
|
||||
client. When this happens, the session and all its toplevel session
|
||||
objects become inert, and should be destroyed.
|
||||
</description>
|
||||
</event>
|
||||
</interface>
|
||||
|
||||
<interface name="xdg_toplevel_session_v1" version="1">
|
||||
<description summary="A session for an application">
|
||||
A xdg_toplevel_session_v1 resource acts as a handle for the given
|
||||
toplevel in the session. It allows for receiving events after a
|
||||
toplevel state was restored, and has the requests to manage them.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="Destroy the object">
|
||||
Destroy the object. This has no effect over window management of the
|
||||
associated toplevel.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="rename">
|
||||
<description summary="change the name of toplevel session">
|
||||
Renames the toplevel session. The new name can be used in subsequent requests
|
||||
to identify this session object. The state associated with this toplevel
|
||||
session will be preserved.
|
||||
|
||||
If the xdg_session_v1 already contains a toplevel with the specified name,
|
||||
the 'name_in_use' protocol error will be raised.
|
||||
</description>
|
||||
<arg name="name" type="string" summary="new name to identify the toplevel"/>
|
||||
</request>
|
||||
|
||||
<event name="restored">
|
||||
<description summary="a toplevel's session has been restored">
|
||||
The "restored" event is emitted prior to the first
|
||||
xdg_toplevel.configure for the toplevel. It will only be emitted after
|
||||
xdg_session_v1.restore_toplevel, and the initial empty surface state has
|
||||
been applied, and it indicates that the surface's session is being
|
||||
restored with this configure event.
|
||||
</description>
|
||||
</event>
|
||||
</interface>
|
||||
</protocol>
|
||||
@@ -874,11 +874,11 @@ fn start_grab(par: StartGrabParams) {
|
||||
// - not in editmode
|
||||
// - not using grab_float
|
||||
// - grabbing with one hand. (grabbing with the 2nd hand will grab the individual overlay instead)
|
||||
let grab_anchor = !(par.edit_mode
|
||||
|| par.grab_float
|
||||
|| app.anchor_grabbed
|
||||
|| !matches!(state.positioning, Positioning::Anchored)
|
||||
|| app.session.config.middle_grabs_single
|
||||
let grab_anchor = !par.edit_mode
|
||||
&& !par.grab_float
|
||||
&& !app.anchor_grabbed
|
||||
&& matches!(state.positioning, Positioning::Anchored)
|
||||
&& !(app.session.config.middle_grabs_single
|
||||
&& pointer.interaction.mode == PointerMode::Middle);
|
||||
|
||||
let relative_grab_transform = if grab_anchor {
|
||||
|
||||
@@ -723,7 +723,7 @@ fn for_each_path(spec: Option<&OneOrMany<String>>, mut f: impl FnMut(&str)) {
|
||||
}
|
||||
}
|
||||
|
||||
const fn button_actions(profile: &OpenXrInputProfile) -> [Option<&OpenXrInputAction>; 11] {
|
||||
fn button_actions(profile: &OpenXrInputProfile) -> [Option<&OpenXrInputAction>; 11] {
|
||||
[
|
||||
profile.click.as_ref(),
|
||||
profile.alt_click.as_ref(),
|
||||
@@ -925,7 +925,7 @@ fn add_button_bindings(
|
||||
|
||||
for (side, hand) in hands.iter_mut().enumerate() {
|
||||
for binding in build_button_bindings(action, side, profile_index, physical_input_map) {
|
||||
field(hand).add_binding(binding);
|
||||
field(&mut **hand).add_binding(binding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,30 +133,34 @@ impl PlayspaceMover {
|
||||
self.rotate = Some(data);
|
||||
// drag and rotate can share a button
|
||||
return;
|
||||
}
|
||||
// start space rotate
|
||||
for (i, pointer) in app.input_state.pointers.iter().enumerate() {
|
||||
if pointer.now.space_rotate {
|
||||
let Ok(pose) = monado
|
||||
.ipc
|
||||
.get_reference_space_offset(ReferenceSpaceType::Stage)
|
||||
.log_err("Could not get reference space offset.")
|
||||
.map(|p| {
|
||||
Affine3A::from_rotation_translation(p.orientation.into(), p.position.into())
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
} else {
|
||||
// start space rotate
|
||||
for (i, pointer) in app.input_state.pointers.iter().enumerate() {
|
||||
if pointer.now.space_rotate {
|
||||
let Ok(pose) = monado
|
||||
.ipc
|
||||
.get_reference_space_offset(ReferenceSpaceType::Stage)
|
||||
.log_err("Could not get reference space offset.")
|
||||
.map(|p| {
|
||||
Affine3A::from_rotation_translation(
|
||||
p.orientation.into(),
|
||||
p.position.into(),
|
||||
)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let hand_pose = pose * pointer.raw_pose;
|
||||
self.rotate = Some(MoverData {
|
||||
pose,
|
||||
hand: i,
|
||||
hand_pose,
|
||||
});
|
||||
self.drag = None;
|
||||
log::info!("Start space rotate");
|
||||
return;
|
||||
let hand_pose = pose * pointer.raw_pose;
|
||||
self.rotate = Some(MoverData {
|
||||
pose,
|
||||
hand: i,
|
||||
hand_pose,
|
||||
});
|
||||
self.drag = None;
|
||||
log::info!("Start space rotate");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ impl WlxSwapchain {
|
||||
|
||||
struct RetiredSwapchain {
|
||||
// keep alive until completion is signaled
|
||||
swapchain: WlxSwapchain,
|
||||
_swapchain: WlxSwapchain,
|
||||
completion: Option<GpuCompletionMarker>,
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ impl SwapchainRetirementQueue {
|
||||
/// queue the swapchain for non-blocking retirement
|
||||
pub(super) fn retire(&mut self, swapchain: WlxSwapchain, gfx: &Arc<WGfx>) {
|
||||
self.retired.push(RetiredSwapchain {
|
||||
swapchain,
|
||||
_swapchain: swapchain,
|
||||
completion: None,
|
||||
});
|
||||
|
||||
@@ -177,7 +177,7 @@ impl SwapchainRetirementQueue {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
retired.swapchain.ensure_image_released()?;
|
||||
retired._swapchain.ensure_image_released()?;
|
||||
retired.completion = Some(gfx.submit_graphics_completion_marker()?);
|
||||
Ok(())
|
||||
}
|
||||
@@ -185,12 +185,12 @@ impl SwapchainRetirementQueue {
|
||||
pub(super) fn collect(&mut self, gfx: &Arc<WGfx>) {
|
||||
let mut idx = 0;
|
||||
while idx < self.retired.len() {
|
||||
if self.retired[idx].completion.is_none()
|
||||
&& let Err(e) = Self::arm(&mut self.retired[idx], gfx)
|
||||
{
|
||||
log::warn!("Failed re-arming retired OpenXR swapchain: {e:#}");
|
||||
idx += 1;
|
||||
continue;
|
||||
if self.retired[idx].completion.is_none() {
|
||||
if let Err(e) = Self::arm(&mut self.retired[idx], gfx) {
|
||||
log::warn!("Failed re-arming retired OpenXR swapchain: {e:#}");
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let complete = match self.retired[idx]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{
|
||||
cmp,
|
||||
collections::{BinaryHeap, VecDeque},
|
||||
sync::Arc,
|
||||
sync::atomic::{self, AtomicUsize},
|
||||
time::Instant,
|
||||
};
|
||||
@@ -11,7 +10,6 @@ use strum::EnumIs;
|
||||
|
||||
use crate::{
|
||||
backend::input,
|
||||
backend::wayvr as wvr,
|
||||
state::AppState,
|
||||
windowing::{
|
||||
OverlayID, OverlaySelector,
|
||||
@@ -111,8 +109,6 @@ pub enum SpawnPos {
|
||||
Spread,
|
||||
/// Spawn relative to a different overlay
|
||||
Parent(OverlaySelector),
|
||||
/// Resolve parent or app-id at the point of spawn
|
||||
WlDefault(wvr::window::WindowHandle, Option<OverlayID>),
|
||||
}
|
||||
|
||||
pub enum GlobalChange {
|
||||
@@ -142,10 +138,6 @@ pub enum OverlayTask {
|
||||
Drop(OverlaySelector),
|
||||
DropImmediate(OverlaySelector),
|
||||
DropExpired(OverlayID),
|
||||
SetOverlayName(OverlayID, Arc<str>, bool, bool),
|
||||
ForgetWindowState(OverlayID, Arc<str>),
|
||||
RenameWindowState(Arc<str>, Arc<str>),
|
||||
ClearWindowState(Arc<str>),
|
||||
}
|
||||
|
||||
pub type GlobalTask = dyn FnOnce(&mut AppState) + Send;
|
||||
|
||||
@@ -21,6 +21,8 @@ use smithay::{
|
||||
use wgui::log::LogErr;
|
||||
use xkbcommon::xkb;
|
||||
|
||||
use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask};
|
||||
|
||||
use super::{
|
||||
ProcessWayVREnv,
|
||||
comp::{self, ClientState},
|
||||
@@ -30,7 +32,6 @@ use super::{
|
||||
pub struct WayVRClient {
|
||||
pub client: wayland_server::Client,
|
||||
pub pid: u32,
|
||||
pub process: process::ProcessHandle,
|
||||
}
|
||||
|
||||
pub struct WayVRCompositor {
|
||||
@@ -159,17 +160,16 @@ impl WayVRCompositor {
|
||||
let process_env = get_wayvr_env_from_pid(credentials.pid)?;
|
||||
|
||||
// Find suitable auth key from the process list
|
||||
for (handle, process) in processes.iter() {
|
||||
if let process::Process::Managed(process) = process
|
||||
for (_, process) in processes {
|
||||
if let process::Process::Managed(process) = &process
|
||||
&& let Some(auth_key) = &process_env.display_auth
|
||||
{
|
||||
// Find process with matching auth key
|
||||
if process.auth_key.as_str() == auth_key {
|
||||
// Associate client now, instead of trying to match env later
|
||||
// Add client
|
||||
self.add_client(WayVRClient {
|
||||
client,
|
||||
pid: credentials.pid as u32,
|
||||
process: handle,
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
@@ -178,15 +178,18 @@ impl WayVRCompositor {
|
||||
|
||||
// This is a new process which we didn't met before.
|
||||
// Treat external processes exclusively (spawned by the user or external program)
|
||||
let pid = credentials.pid as u32;
|
||||
log::warn!("External process ID {pid} connected to this Wayland server");
|
||||
let process =
|
||||
processes.insert(process::Process::External(process::ExternalProcess { pid }));
|
||||
self.add_client(WayVRClient {
|
||||
client,
|
||||
pid,
|
||||
process,
|
||||
});
|
||||
log::warn!(
|
||||
"External process ID {} connected to this Wayland server",
|
||||
credentials.pid
|
||||
);
|
||||
|
||||
self.state
|
||||
.wayvr_tasks
|
||||
.send(WayVRTask::NewExternalProcess(ExternalProcessRequest {
|
||||
env: process_env,
|
||||
client,
|
||||
pid: credentials.pid as u32,
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ use smithay::output::Output;
|
||||
use smithay::reexports::rustix::fs::{OFlags, fcntl_setfl};
|
||||
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1;
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::XdgToplevel;
|
||||
use smithay::reexports::wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration;
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::reexports::wayland_server::backend::ObjectId;
|
||||
@@ -47,8 +46,6 @@ use smithay::wayland::shell::xdg::{
|
||||
use smithay::wayland::shm::{ShmHandler, ShmState, shm_format_to_fourcc, with_buffer_contents};
|
||||
use smithay::wayland::single_pixel_buffer::get_single_pixel_buffer;
|
||||
use smithay::wayland::viewporter::ViewporterState;
|
||||
use smithay::wayland::xdg_toplevel_icon::{XdgToplevelIconHandler, XdgToplevelIconManager};
|
||||
use smithay::wayland::xdg_toplevel_tag::{XdgToplevelTagHandler, XdgToplevelTagManager};
|
||||
use smithay::{
|
||||
delegate_compositor, delegate_data_control, delegate_data_device, delegate_dmabuf,
|
||||
delegate_ext_data_control, delegate_kde_decoration, delegate_output,
|
||||
@@ -65,7 +62,6 @@ use wayland_server::backend::{ClientData, ClientId, DisconnectReason};
|
||||
use wayland_server::protocol::wl_surface::WlSurface;
|
||||
use wlx_capture::DrmFourcc;
|
||||
|
||||
use super::session::SessionRegistry;
|
||||
use crate::backend::wayvr::image_importer::ImageImporter;
|
||||
use crate::backend::wayvr::{SurfaceBufWithImage, WAYVR_SCREEN_RES, time};
|
||||
use crate::ipc::event_queue::SyncEventQueue;
|
||||
@@ -78,13 +74,6 @@ pub struct Application {
|
||||
pub dmabuf_state: (DmabufState, DmabufGlobal, Option<DmabufFeedback>),
|
||||
pub compositor: compositor::CompositorState,
|
||||
pub xdg_shell: XdgShellState,
|
||||
#[allow(dead_code)]
|
||||
pub xdg_toplevel_tags: XdgToplevelTagManager,
|
||||
#[allow(dead_code)]
|
||||
pub xdg_toplevel_icons: XdgToplevelIconManager,
|
||||
pub pending_icon_changes: HashSet<ObjectId>,
|
||||
pub sessions: SessionRegistry,
|
||||
pub toplevel_tags: HashMap<ObjectId, Arc<str>>,
|
||||
pub seat: Seat<Application>,
|
||||
pub seat_state: SeatState<Application>,
|
||||
pub shm: ShmState,
|
||||
@@ -246,7 +235,7 @@ impl Application {
|
||||
}
|
||||
}
|
||||
|
||||
const fn drm_format_has_alpha(format: DrmFourcc) -> bool {
|
||||
fn drm_format_has_alpha(format: DrmFourcc) -> bool {
|
||||
matches!(
|
||||
format,
|
||||
DrmFourcc::Abgr8888 | DrmFourcc::Argb8888 | DrmFourcc::Abgr2101010
|
||||
@@ -267,32 +256,6 @@ impl compositor::CompositorHandler for Application {
|
||||
|
||||
#[allow(clippy::significant_drop_tightening)]
|
||||
fn commit(&mut self, surface: &WlSurface) {
|
||||
if self.pending_icon_changes.remove(&surface.id())
|
||||
&& let Some(toplevel) = self
|
||||
.xdg_shell
|
||||
.toplevel_surfaces()
|
||||
.iter()
|
||||
.find(|t| t.wl_surface() == surface)
|
||||
{
|
||||
self.wayvr_tasks
|
||||
.send(WayVRTask::IconChange(toplevel.clone()));
|
||||
}
|
||||
if let Some(toplevel) = self
|
||||
.xdg_shell
|
||||
.toplevel_surfaces()
|
||||
.iter()
|
||||
.find(|toplevel| toplevel.wl_surface() == surface)
|
||||
{
|
||||
// first commit is special in terms of restoring session
|
||||
if self
|
||||
.sessions
|
||||
.committed_toplevels
|
||||
.insert(toplevel.xdg_toplevel().id())
|
||||
{
|
||||
self.wayvr_tasks
|
||||
.send(WayVRTask::FirstToplevelCommit(toplevel.clone()));
|
||||
}
|
||||
}
|
||||
self.popups_commit(surface);
|
||||
|
||||
smithay::wayland::compositor::with_states(surface, |states| {
|
||||
@@ -534,8 +497,6 @@ impl XdgShellHandler for Application {
|
||||
}
|
||||
|
||||
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
|
||||
self.toplevel_tags.remove(&surface.xdg_toplevel().id());
|
||||
self.sessions.forget_toplevel(&surface);
|
||||
self.output.leave(surface.wl_surface());
|
||||
self.no_server_decorations
|
||||
.remove(&surface.wl_surface().id());
|
||||
@@ -692,16 +653,6 @@ impl XdgShellHandler for Application {
|
||||
}
|
||||
}
|
||||
|
||||
fn app_id_changed(&mut self, surface: ToplevelSurface) {
|
||||
self.wayvr_tasks
|
||||
.send(WayVRTask::WindowIdentityChanged(surface));
|
||||
}
|
||||
|
||||
fn parent_changed(&mut self, surface: ToplevelSurface) {
|
||||
self.wayvr_tasks
|
||||
.send(WayVRTask::WindowIdentityChanged(surface));
|
||||
}
|
||||
|
||||
fn title_changed(&mut self, surface: ToplevelSurface) {
|
||||
if let Some(client) = surface.wl_surface().client() {
|
||||
self.wayvr_tasks
|
||||
@@ -710,26 +661,6 @@ impl XdgShellHandler for Application {
|
||||
}
|
||||
}
|
||||
|
||||
impl XdgToplevelIconHandler for Application {
|
||||
fn set_icon(&mut self, _toplevel: XdgToplevel, surface: WlSurface) {
|
||||
self.pending_icon_changes.insert(surface.id());
|
||||
}
|
||||
}
|
||||
|
||||
impl XdgToplevelTagHandler for Application {
|
||||
fn set_tag(&mut self, toplevel: XdgToplevel, tag: String) {
|
||||
if tag.is_empty() {
|
||||
self.toplevel_tags.remove(&toplevel.id());
|
||||
} else {
|
||||
self.toplevel_tags.insert(toplevel.id(), tag.into());
|
||||
}
|
||||
if let Some(surface) = self.xdg_shell.get_toplevel(&toplevel) {
|
||||
self.wayvr_tasks
|
||||
.send(WayVRTask::WindowIdentityChanged(surface));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShmHandler for Application {
|
||||
fn shm_state(&self) -> &ShmState {
|
||||
&self.shm
|
||||
@@ -834,8 +765,6 @@ impl KdeDecorationHandler for Application {
|
||||
|
||||
delegate_dmabuf!(Application);
|
||||
delegate_xdg_shell!(Application);
|
||||
smithay::delegate_xdg_toplevel_tag!(Application);
|
||||
smithay::delegate_xdg_toplevel_icon!(Application);
|
||||
delegate_compositor!(Application);
|
||||
delegate_viewporter!(Application);
|
||||
delegate_shm!(Application);
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
use smithay::reexports::wayland_server::protocol::wl_shm;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shm::with_buffer_contents;
|
||||
use smithay::wayland::xdg_toplevel_icon::ToplevelIconCachedState;
|
||||
use std::sync::Arc;
|
||||
use wlx_common::desktop_finder::DesktopFinder;
|
||||
|
||||
pub fn toplevel_icon(surface: &WlSurface, finder: &DesktopFinder) -> Option<Arc<str>> {
|
||||
let (name, buffers) = with_states(surface, |states| {
|
||||
let mut icon = states.cached_state.get::<ToplevelIconCachedState>();
|
||||
let current = icon.current();
|
||||
(
|
||||
current.icon_name().map(str::to_owned),
|
||||
current.buffers().to_vec(),
|
||||
)
|
||||
});
|
||||
|
||||
for (buffer, _scale) in buffers {
|
||||
let mut png_path = None;
|
||||
let _ = with_buffer_contents(&buffer, |data, size, info| {
|
||||
let data = unsafe { std::slice::from_raw_parts(data, size) };
|
||||
let (w, h) = (info.width as usize, info.height as usize);
|
||||
if info.width <= 0
|
||||
|| info.height <= 0
|
||||
|| info.stride <= 0
|
||||
|| info.offset < 0
|
||||
|| w != h
|
||||
|| w > 1024
|
||||
{
|
||||
return;
|
||||
}
|
||||
let stride = info.stride as usize;
|
||||
let offset = info.offset as usize;
|
||||
if stride < w * 4
|
||||
|| offset
|
||||
.checked_add((h - 1).saturating_mul(stride) + w * 4)
|
||||
.is_none_or(|end| end > size)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let has_alpha = match info.format {
|
||||
wl_shm::Format::Argb8888 => true,
|
||||
wl_shm::Format::Xrgb8888 => false,
|
||||
_ => return,
|
||||
};
|
||||
let mut rgba = vec![0u8; w * h * 4];
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let pos = offset + y * stride + x * 4;
|
||||
let dst = (y * w + x) * 4;
|
||||
rgba[dst] = data[pos + 2];
|
||||
rgba[dst + 1] = data[pos + 1];
|
||||
rgba[dst + 2] = data[pos];
|
||||
rgba[dst + 3] = if has_alpha { data[pos + 3] } else { 255 };
|
||||
}
|
||||
}
|
||||
let path = wlx_common::cache_dir::get_path(&format!(
|
||||
"icons/wayland-{}.png",
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
if image::save_buffer_with_format(
|
||||
&path,
|
||||
&rgba,
|
||||
w as u32,
|
||||
h as u32,
|
||||
image::ColorType::Rgba8,
|
||||
image::ImageFormat::Png,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
png_path = Some(path.to_string_lossy().into_owned().into());
|
||||
}
|
||||
});
|
||||
if png_path.is_some() {
|
||||
return png_path;
|
||||
}
|
||||
}
|
||||
name.and_then(|n| finder.resolve_icon(&n)).map(Into::into)
|
||||
}
|
||||
@@ -114,6 +114,8 @@ impl ImageImporter {
|
||||
num_planes: dmabuf.num_planes(),
|
||||
planes: Default::default(),
|
||||
mouse: None,
|
||||
acquire_sync: Default::default(),
|
||||
lease: None,
|
||||
};
|
||||
|
||||
for (i, handle) in dmabuf.handles().enumerate() {
|
||||
@@ -129,7 +131,7 @@ impl ImageImporter {
|
||||
frame.planes[i].stride = stride as _;
|
||||
}
|
||||
|
||||
let image = self.gfx.dmabuf_texture(frame)?;
|
||||
let image = self.gfx.dmabuf_texture(&frame)?;
|
||||
log::info!("DMA-buf import: creating image view");
|
||||
let image_view = self.gfx.create_image_view(image)?;
|
||||
log::info!("DMA-buf import: image view created");
|
||||
|
||||
+69
-327
@@ -1,19 +1,15 @@
|
||||
pub mod client;
|
||||
mod comp;
|
||||
pub mod hit_test;
|
||||
mod icon;
|
||||
mod image_importer;
|
||||
mod input_capture;
|
||||
pub mod process;
|
||||
mod session;
|
||||
mod session_protocol;
|
||||
mod time;
|
||||
pub mod window;
|
||||
|
||||
use anyhow::Context;
|
||||
use comp::Application;
|
||||
use glam::{DVec2, Vec2};
|
||||
use session::{SessionRegistry, app_key, tag_key};
|
||||
use slotmap::{DenseSlotMap, SecondaryMap};
|
||||
use smallvec::SmallVec;
|
||||
use smithay::{
|
||||
@@ -23,7 +19,7 @@ use smithay::{
|
||||
reexports::{
|
||||
wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration_manager as kde_decoration,
|
||||
wayland_server::{
|
||||
self, Resource,
|
||||
self,
|
||||
backend::ClientId,
|
||||
protocol::{wl_buffer, wl_surface::WlSurface},
|
||||
},
|
||||
@@ -122,19 +118,22 @@ pub struct ProcessWayVREnv {
|
||||
pub display_name: Option<String>, // Externally spawned process by a user script
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ExternalProcessRequest {
|
||||
#[allow(dead_code)]
|
||||
pub env: ProcessWayVREnv,
|
||||
pub client: wayland_server::Client,
|
||||
pub pid: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum WayVRTask {
|
||||
NewToplevel(ClientId, ToplevelSurface),
|
||||
FirstToplevelCommit(ToplevelSurface),
|
||||
DropToplevel(ClientId, ToplevelSurface),
|
||||
MinimizeRequest(ClientId, ToplevelSurface),
|
||||
TitleChange(ClientId, ToplevelSurface),
|
||||
IconChange(ToplevelSurface),
|
||||
WindowIdentityChanged(ToplevelSurface),
|
||||
FreezeWindowState(ToplevelSurface, Arc<str>),
|
||||
RenameWindowState(Arc<str>, Arc<str>),
|
||||
ForgetWindowState(Arc<str>),
|
||||
VisibilityChange(window::WindowHandle, bool),
|
||||
NewExternalProcess(ExternalProcessRequest),
|
||||
ProcessTerminationRequest(process::ProcessHandle, KillSignal),
|
||||
CloseWindowRequest(window::WindowHandle),
|
||||
DndStarted {
|
||||
@@ -174,16 +173,16 @@ pub enum PointerFocusTarget {
|
||||
None,
|
||||
}
|
||||
|
||||
pub enum TickTask {
|
||||
NewExternalProcess(ExternalProcessRequest), // Call WayVRCompositor::add_client after receiving this message
|
||||
}
|
||||
|
||||
const KEY_REPEAT_DELAY: i32 = 500;
|
||||
const KEY_REPEAT_RATE: i32 = 50;
|
||||
const WAYVR_SCREEN_RES: [i32; 2] = [2560, 1440];
|
||||
|
||||
impl WvrServerState {
|
||||
pub fn new(
|
||||
gfx: Arc<WGfx>,
|
||||
gfx_extras: &WGfxExtras,
|
||||
config: &GeneralConfig,
|
||||
) -> anyhow::Result<Self> {
|
||||
pub fn new(gfx: Arc<WGfx>, gfx_extras: &WGfxExtras) -> anyhow::Result<Self> {
|
||||
const fn filter_allow_any(_: &wayland_server::Client) -> bool {
|
||||
true
|
||||
}
|
||||
@@ -192,19 +191,6 @@ impl WvrServerState {
|
||||
let dh = display.handle();
|
||||
let compositor = compositor::CompositorState::new::<Application>(&dh);
|
||||
let xdg_shell = XdgShellState::new::<Application>(&dh);
|
||||
let _session_global = dh.create_global::<Application,
|
||||
session_protocol::server::xdg_session_manager_v1::XdgSessionManagerV1, _>(1, ());
|
||||
let xdg_toplevel_tags =
|
||||
smithay::wayland::xdg_toplevel_tag::XdgToplevelTagManager::new::<Application>(&dh);
|
||||
let mut xdg_toplevel_icons =
|
||||
smithay::wayland::xdg_toplevel_icon::XdgToplevelIconManager::new::<Application>(&dh);
|
||||
xdg_toplevel_icons.add_icon_size(64);
|
||||
let stored_keys: HashSet<Arc<str>> = config
|
||||
.sets
|
||||
.iter()
|
||||
.flat_map(|set| set.overlays.keys().chain(set.hidden_overlays.keys()))
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut seat_state = SeatState::new();
|
||||
let shm = ShmState::new::<Application>(&dh, Vec::new());
|
||||
let data_device = DataDeviceState::new::<Application>(&dh);
|
||||
@@ -296,11 +282,6 @@ impl WvrServerState {
|
||||
display_handle: dh,
|
||||
compositor,
|
||||
xdg_shell,
|
||||
xdg_toplevel_tags,
|
||||
xdg_toplevel_icons,
|
||||
pending_icon_changes: HashSet::new(),
|
||||
sessions: SessionRegistry::new(stored_keys),
|
||||
toplevel_tags: HashMap::new(),
|
||||
seat,
|
||||
seat_state,
|
||||
shm,
|
||||
@@ -344,29 +325,26 @@ impl WvrServerState {
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[allow(clippy::significant_drop_tightening)]
|
||||
pub fn tick_events(app: &mut AppState) -> anyhow::Result<()> {
|
||||
pub fn tick_events(app: &mut AppState) -> anyhow::Result<Vec<TickTask>> {
|
||||
let mut tasks: Vec<TickTask> = Vec::new();
|
||||
|
||||
let Some(wvr_server) = app.wvr_server.as_mut() else {
|
||||
return Ok(());
|
||||
return Ok(tasks);
|
||||
};
|
||||
|
||||
app.ipc_server.tick(&mut ipc_server::TickParams {
|
||||
wvr_server,
|
||||
input_state: &app.input_state,
|
||||
tasks: &mut tasks,
|
||||
signals: &app.wayvr_signals,
|
||||
hid_wrapper: &mut app.hid_provider,
|
||||
});
|
||||
|
||||
// Keep launch records while wl client is attached
|
||||
let attached_processes: HashSet<_> = wvr_server
|
||||
.manager
|
||||
.clients
|
||||
.iter()
|
||||
.map(|client| client.process)
|
||||
.collect();
|
||||
// Tick all child processes
|
||||
let mut to_remove: SmallVec<[process::ProcessHandle; 2]> = SmallVec::new();
|
||||
|
||||
for (handle, process) in &mut wvr_server.processes {
|
||||
if !process.is_running() && !attached_processes.contains(&handle) {
|
||||
if !process.is_running() {
|
||||
to_remove.push(handle);
|
||||
}
|
||||
}
|
||||
@@ -378,6 +356,9 @@ impl WvrServerState {
|
||||
|
||||
while let Some(task) = wvr_server.tasks.read() {
|
||||
match task {
|
||||
WayVRTask::NewExternalProcess(req) => {
|
||||
tasks.push(TickTask::NewExternalProcess(req));
|
||||
}
|
||||
WayVRTask::NewToplevel(client_id, toplevel) => {
|
||||
let toplevel = Rc::new(toplevel);
|
||||
|
||||
@@ -387,14 +368,15 @@ impl WvrServerState {
|
||||
continue;
|
||||
}
|
||||
|
||||
let process_handle = client.process;
|
||||
if !wvr_server.processes.contains_key(process_handle) {
|
||||
let Some(process_handle) =
|
||||
process::find_by_pid(&wvr_server.processes, client.pid)
|
||||
else {
|
||||
log::error!(
|
||||
"WayVR window creation failed: Process for PID {} was removed after the client connected.",
|
||||
"WayVR window creation failed: Unexpected process ID {}. It wasn't registered before.",
|
||||
client.pid
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let output_bounds = wvr_server.manager.state.output_logical_size();
|
||||
|
||||
@@ -446,29 +428,6 @@ impl WvrServerState {
|
||||
),
|
||||
};
|
||||
|
||||
let (xdg_title, app_id): (Option<String>, Option<String>) =
|
||||
with_states(toplevel.wl_surface(), |states| {
|
||||
states.data_map.get::<XdgToplevelSurfaceData>().map_or(
|
||||
(None, None),
|
||||
|t| {
|
||||
let t = t.lock().unwrap();
|
||||
(t.title.clone(), t.app_id.clone())
|
||||
},
|
||||
)
|
||||
});
|
||||
let app_id: Option<Arc<str>> =
|
||||
app_id.filter(|s| !s.is_empty()).map(Into::into);
|
||||
let parent = toplevel.parent().and_then(|surface| {
|
||||
wvr_server
|
||||
.wm
|
||||
.windows
|
||||
.iter()
|
||||
.find(|(_, window)| *window.toplevel.wl_surface() == surface)
|
||||
.map(|(handle, _)| handle)
|
||||
});
|
||||
let (name, restore_on_spawn) =
|
||||
wvr_server.window_identity(&toplevel, app_id.as_deref(), parent, None);
|
||||
|
||||
let window_handle = wvr_server.wm.create_window(CreateWindowParams {
|
||||
toplevel: toplevel.clone(),
|
||||
process: process_handle,
|
||||
@@ -477,10 +436,6 @@ impl WvrServerState {
|
||||
max_size,
|
||||
size_x: fallback_size.w as _,
|
||||
size_y: fallback_size.h as _,
|
||||
app_id: app_id.clone(),
|
||||
parent,
|
||||
name: name.clone(),
|
||||
restore_on_spawn,
|
||||
});
|
||||
|
||||
toplevel.with_pending_state(|state| {
|
||||
@@ -489,41 +444,42 @@ impl WvrServerState {
|
||||
state.size = Some(fallback_size);
|
||||
state.states.set(xdg_toplevel::State::Activated);
|
||||
});
|
||||
// xdg-shell requires an empty commit before initial configure
|
||||
if wvr_server
|
||||
.manager
|
||||
.state
|
||||
.sessions
|
||||
.committed_toplevels
|
||||
.contains(&toplevel.xdg_toplevel().id())
|
||||
{
|
||||
wvr_server.send_first_configure(&toplevel);
|
||||
}
|
||||
toplevel.send_configure();
|
||||
|
||||
let mut title: Arc<str> = fallback_title
|
||||
.map(Arc::from)
|
||||
.or_else(|| name.clone())
|
||||
.unwrap_or_else(|| format!("P{}", client.pid).into());
|
||||
.unwrap_or_else(|| format!("P{}", client.pid))
|
||||
.into();
|
||||
|
||||
let spawn_pos = SpawnPos::WlDefault(
|
||||
window_handle,
|
||||
wvr_server.last_process_overlay(process_handle),
|
||||
);
|
||||
let spawn_pos = wvr_server
|
||||
.last_process_overlay(process_handle)
|
||||
.map_or(SpawnPos::Spread, |oid| {
|
||||
SpawnPos::Parent(OverlaySelector::Id(oid))
|
||||
});
|
||||
|
||||
let mut icon = icon;
|
||||
|
||||
// Try to get title from xdg_toplevel, unless it's running in cage
|
||||
if !is_cage {
|
||||
let mut needs_title = true;
|
||||
let (xdg_title, app_id): (Option<String>, Option<String>) =
|
||||
with_states(toplevel.wl_surface(), |states| {
|
||||
states.data_map.get::<XdgToplevelSurfaceData>().map_or(
|
||||
(None, None),
|
||||
|t| {
|
||||
let t = t.lock().unwrap();
|
||||
(t.title.clone(), t.app_id.clone())
|
||||
},
|
||||
)
|
||||
});
|
||||
if let Some(xdg_title) = xdg_title {
|
||||
needs_title = false;
|
||||
title = xdg_title.into();
|
||||
}
|
||||
|
||||
// Try to get title & icon from desktop entry
|
||||
if let Some(app_id) = app_id.as_deref()
|
||||
if let Some(app_id) = app_id
|
||||
&& let Some(desktop_entry) =
|
||||
app.desktop_finder.get_cached_entry(app_id)
|
||||
app.desktop_finder.get_cached_entry(&app_id)
|
||||
{
|
||||
if needs_title {
|
||||
title = desktop_entry.app_name.as_ref().into();
|
||||
@@ -536,9 +492,6 @@ impl WvrServerState {
|
||||
}
|
||||
}
|
||||
|
||||
let icon = icon::toplevel_icon(toplevel.wl_surface(), &app.desktop_finder)
|
||||
.or(icon);
|
||||
|
||||
// Fall back to identicon
|
||||
let icon = match icon {
|
||||
Some(icon) => icon,
|
||||
@@ -549,32 +502,8 @@ impl WvrServerState {
|
||||
OverlaySelector::Nothing,
|
||||
spawn_pos,
|
||||
Box::new(move |app: &mut AppState| {
|
||||
let (identity, restore_on_spawn) = app
|
||||
.wvr_server
|
||||
.as_ref()
|
||||
.and_then(|wvr| wvr.wm.windows.get(window_handle))
|
||||
.map_or((None, false), |w| {
|
||||
(w.name.clone(), w.restore_on_spawn)
|
||||
});
|
||||
let persistent = identity.is_some();
|
||||
let name =
|
||||
identity.unwrap_or_else(|| window_handle.transient_name());
|
||||
let icon = app
|
||||
.wvr_server
|
||||
.as_ref()
|
||||
.and_then(|wvr| wvr.wm.windows.get(window_handle))
|
||||
.and_then(|window| {
|
||||
icon::toplevel_icon(
|
||||
window.toplevel.wl_surface(),
|
||||
&app.desktop_finder,
|
||||
)
|
||||
})
|
||||
.unwrap_or(icon);
|
||||
create_wl_window_overlay(
|
||||
name,
|
||||
title,
|
||||
persistent,
|
||||
restore_on_spawn,
|
||||
app,
|
||||
window_handle,
|
||||
icon,
|
||||
@@ -588,11 +517,6 @@ impl WvrServerState {
|
||||
)));
|
||||
}
|
||||
}
|
||||
WayVRTask::FirstToplevelCommit(toplevel) => {
|
||||
if wvr_server.wm.find_window_handle(&toplevel).is_some() {
|
||||
wvr_server.send_first_configure(&toplevel);
|
||||
}
|
||||
}
|
||||
WayVRTask::DropToplevel(client_id, toplevel) => {
|
||||
for client in &wvr_server.manager.clients {
|
||||
if client.client.id() != client_id {
|
||||
@@ -633,81 +557,9 @@ impl WvrServerState {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(key) = wvr_server
|
||||
.wm
|
||||
.windows
|
||||
.get(window_handle)
|
||||
.and_then(|w| w.name.clone())
|
||||
{
|
||||
wvr_server.manager.state.sessions.stored_keys.insert(key);
|
||||
}
|
||||
wvr_server.wm.remove_window(window_handle);
|
||||
}
|
||||
}
|
||||
WayVRTask::WindowIdentityChanged(toplevel) => {
|
||||
let Some(handle) = wvr_server.wm.find_window_handle(&toplevel) else {
|
||||
continue;
|
||||
};
|
||||
let app_id: Option<Arc<str>> = with_states(toplevel.wl_surface(), |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.and_then(|d| {
|
||||
d.lock()
|
||||
.unwrap()
|
||||
.app_id
|
||||
.as_deref()
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(Into::into)
|
||||
})
|
||||
});
|
||||
let parent = toplevel.parent().and_then(|surface| {
|
||||
wvr_server
|
||||
.wm
|
||||
.windows
|
||||
.iter()
|
||||
.find(|(_, w)| *w.toplevel.wl_surface() == surface)
|
||||
.map(|(id, _)| id)
|
||||
});
|
||||
let (key, restore) = wvr_server.window_identity(
|
||||
&toplevel,
|
||||
app_id.as_deref(),
|
||||
parent,
|
||||
Some(handle),
|
||||
);
|
||||
if let Some(window) = wvr_server.wm.windows.get_mut(handle) {
|
||||
window.app_id = app_id;
|
||||
window.parent = parent;
|
||||
if window.name != key || window.restore_on_spawn != restore {
|
||||
window.name.clone_from(&key);
|
||||
window.restore_on_spawn = restore;
|
||||
if let Some(oid) = wvr_server.window_to_overlay.get(&handle) {
|
||||
let persistent = key.is_some();
|
||||
let name = key.unwrap_or_else(|| handle.transient_name());
|
||||
app.tasks
|
||||
.enqueue(TaskType::Overlay(OverlayTask::SetOverlayName(
|
||||
*oid, name, persistent, restore,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WayVRTask::FreezeWindowState(toplevel, key) => {
|
||||
if let Some(handle) = wvr_server.wm.find_window_handle(&toplevel)
|
||||
&& let Some(oid) = wvr_server.window_to_overlay.get(&handle)
|
||||
{
|
||||
app.tasks
|
||||
.enqueue(TaskType::Overlay(OverlayTask::ForgetWindowState(*oid, key)));
|
||||
}
|
||||
}
|
||||
WayVRTask::RenameWindowState(old, new) => {
|
||||
app.tasks
|
||||
.enqueue(TaskType::Overlay(OverlayTask::RenameWindowState(old, new)));
|
||||
}
|
||||
WayVRTask::ForgetWindowState(key) => {
|
||||
app.tasks
|
||||
.enqueue(TaskType::Overlay(OverlayTask::ClearWindowState(key)));
|
||||
}
|
||||
WayVRTask::VisibilityChange(window_handle, visible) => {
|
||||
if let Some(oid) = wvr_server.window_to_overlay.get(&window_handle) {
|
||||
app.tasks
|
||||
@@ -742,42 +594,6 @@ impl WvrServerState {
|
||||
}
|
||||
}
|
||||
}
|
||||
WayVRTask::IconChange(toplevel) => {
|
||||
if let Some(handle) = wvr_server.wm.find_window_handle(&toplevel)
|
||||
&& let Some(oid) = wvr_server.window_to_overlay.get(&handle)
|
||||
{
|
||||
let icon = icon::toplevel_icon(toplevel.wl_surface(), &app.desktop_finder);
|
||||
let fallback = wvr_server
|
||||
.wm
|
||||
.windows
|
||||
.get(handle)
|
||||
.and_then(|w| w.app_id.as_deref())
|
||||
.and_then(|id| app.desktop_finder.get_cached_entry(id))
|
||||
.and_then(|entry| entry.icon_path.as_ref())
|
||||
.map(|s| Arc::<str>::from(s.as_ref()));
|
||||
let icon = icon.or(fallback).or_else(|| {
|
||||
let title =
|
||||
with_states(toplevel.wl_surface(), |states| {
|
||||
states.data_map.get::<XdgToplevelSurfaceData>().and_then(
|
||||
|data| data.lock().ok().and_then(|data| data.title.clone()),
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| "window".to_owned());
|
||||
DesktopFinder::create_icon(&title).ok().map(Into::into)
|
||||
});
|
||||
if let Some(icon) = icon {
|
||||
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
|
||||
OverlaySelector::Id(*oid),
|
||||
Box::new(move |app, owc| {
|
||||
let _ = owc.backend.notify(
|
||||
app,
|
||||
OverlayEventData::WvrCommand(WvrCommand::SetIcon(icon)),
|
||||
);
|
||||
}),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
WayVRTask::TitleChange(client_id, toplevel) => {
|
||||
for client in &wvr_server.manager.clients {
|
||||
if client.client.id() != client_id {
|
||||
@@ -785,33 +601,19 @@ impl WvrServerState {
|
||||
}
|
||||
let Some(window_handle) = wvr_server.wm.find_window_handle(&toplevel)
|
||||
else {
|
||||
log::warn!("TitleChange: Couldn't find matching window handle");
|
||||
log::warn!("MinimizeRequest: Couldn't find matching window handle");
|
||||
continue;
|
||||
};
|
||||
if let Some(oid) = wvr_server.window_to_overlay.get(&window_handle) {
|
||||
let title: Option<Arc<str>> =
|
||||
with_states(toplevel.wl_surface(), |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.and_then(|data| {
|
||||
data.lock().ok().and_then(|data| data.title.clone())
|
||||
})
|
||||
.filter(|title| !title.is_empty())
|
||||
.map(Into::into)
|
||||
});
|
||||
if let Some(title) = title {
|
||||
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
|
||||
OverlaySelector::Id(*oid),
|
||||
Box::new(move |app, owc| {
|
||||
owc.title = Some(title);
|
||||
let _ = owc.backend.notify(
|
||||
app,
|
||||
OverlayEventData::WvrCommand(WvrCommand::ReloadTitle),
|
||||
);
|
||||
}),
|
||||
)));
|
||||
}
|
||||
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
|
||||
OverlaySelector::Id(*oid),
|
||||
Box::new(|app, owc| {
|
||||
let _ = owc.backend.notify(
|
||||
app,
|
||||
OverlayEventData::WvrCommand(WvrCommand::ReloadTitle),
|
||||
);
|
||||
}),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -867,7 +669,7 @@ impl WvrServerState {
|
||||
Box::new(move |app| {
|
||||
create_drag_item(app, icon, pointer)
|
||||
.inspect_err(|e| {
|
||||
log::warn!("Could not create drag item overlay: {e:?}");
|
||||
log::warn!("Could not create drag item overlay: {e:?}")
|
||||
})
|
||||
.ok()
|
||||
}),
|
||||
@@ -901,7 +703,7 @@ impl WvrServerState {
|
||||
|
||||
wvr_server.ticks += 1;
|
||||
|
||||
Ok(())
|
||||
Ok(tasks)
|
||||
}
|
||||
|
||||
pub fn config_changed(&mut self, config: &GeneralConfig) {
|
||||
@@ -944,70 +746,6 @@ impl WvrServerState {
|
||||
.copied()
|
||||
}
|
||||
|
||||
/// session restore send as part of first configure
|
||||
fn send_first_configure(&self, toplevel: &ToplevelSurface) {
|
||||
if toplevel.is_initial_configure_sent() {
|
||||
return;
|
||||
}
|
||||
if let Some(s) = self.manager.state.sessions.get_toplevel(toplevel)
|
||||
&& s.restore_requested
|
||||
&& self.manager.state.sessions.stored_keys.contains(&s.key)
|
||||
&& let Some(resource) = s.restore_resource.as_ref()
|
||||
{
|
||||
resource.restored();
|
||||
}
|
||||
toplevel.send_configure();
|
||||
}
|
||||
|
||||
/// Identifier key based on xdg session or app_id+tags or app_id+pid
|
||||
fn window_identity(
|
||||
&self,
|
||||
toplevel: &ToplevelSurface,
|
||||
app_id: Option<&str>,
|
||||
parent: Option<window::WindowHandle>,
|
||||
except: Option<window::WindowHandle>,
|
||||
) -> (Option<Arc<str>>, bool) {
|
||||
let session = self.manager.state.sessions.get_toplevel(toplevel);
|
||||
let tag = self
|
||||
.manager
|
||||
.state
|
||||
.toplevel_tags
|
||||
.get(&toplevel.xdg_toplevel().id());
|
||||
let identity = session
|
||||
.map(|s| (s.key.clone(), s.restore_requested))
|
||||
.or_else(|| {
|
||||
app_id
|
||||
.zip(tag)
|
||||
.map(|(app_id, tag)| (tag_key(app_id, tag), true))
|
||||
})
|
||||
.or_else(|| {
|
||||
let app_id = app_id?;
|
||||
if parent.is_some()
|
||||
|| self.wm.windows.iter().any(|(id, w)| {
|
||||
Some(id) != except
|
||||
&& w.parent.is_none()
|
||||
&& w.app_id.as_deref() == Some(app_id)
|
||||
})
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some((app_key(app_id), true))
|
||||
}
|
||||
});
|
||||
match identity {
|
||||
Some((key, restore))
|
||||
if !self
|
||||
.wm
|
||||
.windows
|
||||
.iter()
|
||||
.any(|(id, w)| Some(id) != except && w.name.as_ref() == Some(&key)) =>
|
||||
{
|
||||
(Some(key), restore)
|
||||
}
|
||||
_ => (None, false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_removed(&mut self, tasks: &mut TaskContainer, process: process::ProcessHandle) {
|
||||
let mut to_remove = vec![];
|
||||
|
||||
@@ -1488,7 +1226,7 @@ impl WvrServerState {
|
||||
self.manager.send_pointer_button(index, pressed);
|
||||
}
|
||||
|
||||
pub const fn set_vr_pointer(&mut self, pointer: usize) {
|
||||
pub fn set_vr_pointer(&mut self, pointer: usize) {
|
||||
self.manager.state.vr_pointer = pointer;
|
||||
}
|
||||
|
||||
@@ -1503,7 +1241,7 @@ impl WvrServerState {
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn dnd_active(&self) -> bool {
|
||||
pub fn dnd_active(&self) -> bool {
|
||||
self.manager.state.dnd_pointer.is_some()
|
||||
}
|
||||
|
||||
@@ -1569,6 +1307,11 @@ impl WvrServerState {
|
||||
self.manager.state.set_clipboard_text(content);
|
||||
}
|
||||
|
||||
pub fn add_external_process(&mut self, pid: u32) -> process::ProcessHandle {
|
||||
self.processes
|
||||
.insert(process::Process::External(process::ExternalProcess { pid }))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn spawn_process(
|
||||
&mut self,
|
||||
@@ -1725,7 +1468,6 @@ impl SurfaceBufWithImage {
|
||||
|
||||
let mut guard = surface_data.cached_state.get::<ViewportCachedState>();
|
||||
let viewport = *guard.current();
|
||||
drop(guard);
|
||||
let Some(src) = viewport.src else {
|
||||
return [0.0, 0.0, 1.0, 1.0];
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{collections::HashMap, io::Read, sync::Arc};
|
||||
|
||||
use slotmap::new_key_type;
|
||||
use slotmap::{DenseSlotMap, new_key_type};
|
||||
use wayvr_ipc::{packet_client, packet_server};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -87,15 +87,30 @@ impl Process {
|
||||
|
||||
impl Drop for WayVRProcess {
|
||||
fn drop(&mut self) {
|
||||
if self.is_running() {
|
||||
log::info!(
|
||||
"Sending SIGTERM (graceful exit) to process {} ({})",
|
||||
self.child.id(),
|
||||
self.exec_path.as_str()
|
||||
);
|
||||
self.kill(libc::SIGTERM);
|
||||
log::info!(
|
||||
"Sending SIGTERM (graceful exit) to process {} ({})",
|
||||
self.child.id(),
|
||||
self.exec_path.as_str()
|
||||
);
|
||||
self.kill(libc::SIGTERM);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_process_env_value(pid: i32, key: &str) -> anyhow::Result<Option<String>> {
|
||||
let path = format!("/proc/{pid}/environ");
|
||||
let mut env_data = String::new();
|
||||
std::fs::File::open(path)?.read_to_string(&mut env_data)?;
|
||||
let lines: Vec<&str> = env_data.split('\0').filter(|s| !s.is_empty()).collect();
|
||||
|
||||
for line in lines {
|
||||
if let Some(cell) = line.split_once('=')
|
||||
&& cell.0 == key
|
||||
{
|
||||
return Ok(Some(String::from(cell.1)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
impl WayVRProcess {
|
||||
@@ -112,11 +127,9 @@ impl WayVRProcess {
|
||||
}
|
||||
|
||||
fn kill(&mut self, signal: i32) {
|
||||
if self.is_running() {
|
||||
unsafe {
|
||||
// Gracefully stop process
|
||||
libc::kill(self.child.id() as i32, signal);
|
||||
}
|
||||
unsafe {
|
||||
// Gracefully stop process
|
||||
libc::kill(self.child.id() as i32, signal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +179,43 @@ new_key_type! {
|
||||
pub struct ProcessHandle;
|
||||
}
|
||||
|
||||
pub fn find_by_pid(
|
||||
processes: &DenseSlotMap<ProcessHandle, Process>,
|
||||
pid: u32,
|
||||
) -> Option<ProcessHandle> {
|
||||
log::debug!("Finding process with PID {pid}");
|
||||
|
||||
for (handle, process) in processes {
|
||||
match &process {
|
||||
Process::Managed(wayvr_process) => {
|
||||
if wayvr_process.child.id() == pid {
|
||||
return Some(handle);
|
||||
}
|
||||
}
|
||||
Process::External(external_process) => {
|
||||
if external_process.pid == pid {
|
||||
return Some(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("Finding by PID failed, trying WAYVR_DISPLAY_AUTH...");
|
||||
|
||||
if let Ok(Some(value)) = get_process_env_value(pid as i32, "WAYVR_DISPLAY_AUTH") {
|
||||
for (handle, process) in processes {
|
||||
if let Process::Managed(wayvr_process) = &process
|
||||
&& wayvr_process.auth_key == value
|
||||
{
|
||||
return Some(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("Process find with PID {pid} failed");
|
||||
None
|
||||
}
|
||||
|
||||
impl ProcessHandle {
|
||||
pub fn from_packet(handle: packet_server::WvrProcessHandle) -> Self {
|
||||
Self::from(slotmap::KeyData::from_ffi(handle.user))
|
||||
|
||||
@@ -1,419 +0,0 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use smithay::{
|
||||
reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum,
|
||||
backend::ObjectId,
|
||||
},
|
||||
wayland::shell::xdg::ToplevelSurface,
|
||||
};
|
||||
|
||||
use super::{
|
||||
WayVRTask,
|
||||
comp::Application,
|
||||
session_protocol::server::{
|
||||
xdg_session_manager_v1::{self, XdgSessionManagerV1},
|
||||
xdg_session_v1::{self, XdgSessionV1},
|
||||
xdg_toplevel_session_v1::{self, XdgToplevelSessionV1},
|
||||
},
|
||||
};
|
||||
|
||||
const SESSION_PREFIX: &str = "wayvr/session/";
|
||||
|
||||
pub(super) fn session_key(session: &str, name: &str) -> Arc<str> {
|
||||
format!("{SESSION_PREFIX}{}:{session}/{name}", session.len()).into()
|
||||
}
|
||||
|
||||
fn session_prefix(session: &str) -> String {
|
||||
format!("{SESSION_PREFIX}{}:{session}/", session.len())
|
||||
}
|
||||
|
||||
pub(super) fn tag_key(app_id: &str, tag: &str) -> Arc<str> {
|
||||
format!("wayvr/tag/{}:{app_id}/{}:{tag}", app_id.len(), tag.len()).into()
|
||||
}
|
||||
|
||||
pub(super) fn app_key(app_id: &str) -> Arc<str> {
|
||||
format!("wayvr/app/{}:{app_id}", app_id.len()).into()
|
||||
}
|
||||
|
||||
pub struct WindowSession {
|
||||
pub key: Arc<str>,
|
||||
pub restore_requested: bool,
|
||||
pub restore_resource: Option<XdgToplevelSessionV1>,
|
||||
session_id: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SessionRegistry {
|
||||
sessions: HashMap<String, Option<XdgSessionV1>>,
|
||||
toplevels: HashMap<ObjectId, WindowSession>,
|
||||
pub stored_keys: HashSet<Arc<str>>,
|
||||
pub committed_toplevels: HashSet<ObjectId>,
|
||||
}
|
||||
|
||||
impl SessionRegistry {
|
||||
pub fn new(saved_keys: HashSet<Arc<str>>) -> Self {
|
||||
Self {
|
||||
stored_keys: saved_keys,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_toplevel(&self, toplevel: &ToplevelSurface) -> Option<&WindowSession> {
|
||||
self.toplevels.get(&toplevel.xdg_toplevel().id())
|
||||
}
|
||||
|
||||
pub fn forget_toplevel(&mut self, toplevel: &ToplevelSurface) {
|
||||
self.toplevels.remove(&toplevel.xdg_toplevel().id());
|
||||
self.committed_toplevels
|
||||
.remove(&toplevel.xdg_toplevel().id());
|
||||
}
|
||||
}
|
||||
|
||||
impl Application {
|
||||
fn detach_session(&mut self, session_id: &str, forget: bool) {
|
||||
let detached: Vec<_> = self
|
||||
.sessions
|
||||
.toplevels
|
||||
.iter()
|
||||
.filter(|(_, w)| w.session_id == session_id)
|
||||
.map(|(id, w)| (id.clone(), w.key.clone()))
|
||||
.collect();
|
||||
|
||||
for (id, key) in detached {
|
||||
self.sessions.toplevels.remove(&id);
|
||||
let surface = self
|
||||
.xdg_shell
|
||||
.toplevel_surfaces()
|
||||
.iter()
|
||||
.find(|surface| surface.xdg_toplevel().id() == id)
|
||||
.cloned();
|
||||
|
||||
if forget {
|
||||
self.sessions.stored_keys.remove(&key);
|
||||
} else {
|
||||
self.sessions.stored_keys.insert(key.clone());
|
||||
if let Some(surface) = &surface {
|
||||
self.wayvr_tasks
|
||||
.send(WayVRTask::FreezeWindowState(surface.clone(), key.clone()));
|
||||
}
|
||||
}
|
||||
if let Some(surface) = surface {
|
||||
self.wayvr_tasks
|
||||
.send(WayVRTask::WindowIdentityChanged(surface));
|
||||
}
|
||||
if forget {
|
||||
self.wayvr_tasks.send(WayVRTask::ForgetWindowState(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_session_toplevel(&mut self, session_id: &str, name: &str) {
|
||||
let key = session_key(session_id, name);
|
||||
let live = self
|
||||
.sessions
|
||||
.toplevels
|
||||
.iter()
|
||||
.find(|(_, w)| w.session_id == session_id && w.key == key)
|
||||
.map(|(id, _)| id.clone());
|
||||
if let Some(id) = live {
|
||||
self.sessions.toplevels.remove(&id);
|
||||
if let Some(surface) = self
|
||||
.xdg_shell
|
||||
.toplevel_surfaces()
|
||||
.iter()
|
||||
.find(|surface| surface.xdg_toplevel().id() == id)
|
||||
.cloned()
|
||||
{
|
||||
self.wayvr_tasks
|
||||
.send(WayVRTask::WindowIdentityChanged(surface));
|
||||
}
|
||||
}
|
||||
self.sessions.stored_keys.remove(&key);
|
||||
self.wayvr_tasks.send(WayVRTask::ForgetWindowState(key));
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalDispatch<XdgSessionManagerV1, ()> for Application {
|
||||
fn bind(
|
||||
_state: &mut Self,
|
||||
_dh: &DisplayHandle,
|
||||
_client: &Client,
|
||||
resource: New<XdgSessionManagerV1>,
|
||||
_data: &(),
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
data_init.init(resource, ());
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<XdgSessionManagerV1, ()> for Application {
|
||||
fn request(
|
||||
state: &mut Self,
|
||||
client: &Client,
|
||||
resource: &XdgSessionManagerV1,
|
||||
request: xdg_session_manager_v1::Request,
|
||||
_data: &(),
|
||||
_dh: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
match request {
|
||||
xdg_session_manager_v1::Request::GetSession {
|
||||
id,
|
||||
reason,
|
||||
session_id,
|
||||
} => {
|
||||
if let WEnum::Unknown(_) = reason {
|
||||
resource.post_error(
|
||||
xdg_session_manager_v1::Error::InvalidReason,
|
||||
"unrecognized session reason",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let known = session_id.as_ref().is_some_and(|token| {
|
||||
state.sessions.sessions.contains_key(token)
|
||||
|| state
|
||||
.sessions
|
||||
.stored_keys
|
||||
.iter()
|
||||
.any(|key| key.starts_with(session_prefix(token).as_str()))
|
||||
});
|
||||
let token = if known {
|
||||
session_id.unwrap()
|
||||
} else {
|
||||
uuid::Uuid::new_v4().to_string()
|
||||
};
|
||||
|
||||
if let Some(Some(old)) = state.sessions.sessions.get(&token) {
|
||||
if old.client().is_some_and(|c| c.id() == client.id()) {
|
||||
resource.post_error(
|
||||
xdg_session_manager_v1::Error::InUse,
|
||||
"session is already in use by this client",
|
||||
);
|
||||
return;
|
||||
}
|
||||
old.replaced();
|
||||
state.detach_session(&token, false);
|
||||
}
|
||||
|
||||
let session_resource = data_init.init(id, token.clone());
|
||||
if known {
|
||||
session_resource.restored();
|
||||
} else {
|
||||
session_resource.created(token.clone());
|
||||
}
|
||||
state
|
||||
.sessions
|
||||
.sessions
|
||||
.insert(token, Some(session_resource));
|
||||
}
|
||||
xdg_session_manager_v1::Request::Destroy => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<XdgSessionV1, String> for Application {
|
||||
fn request(
|
||||
state: &mut Self,
|
||||
_client: &Client,
|
||||
resource: &XdgSessionV1,
|
||||
request: xdg_session_v1::Request,
|
||||
session_id: &String,
|
||||
_dh: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
if state
|
||||
.sessions
|
||||
.sessions
|
||||
.get(session_id)
|
||||
.and_then(Option::as_ref)
|
||||
!= Some(resource)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let restore_requested = matches!(&request, xdg_session_v1::Request::RestoreToplevel { .. });
|
||||
match request {
|
||||
xdg_session_v1::Request::AddToplevel { id, toplevel, name }
|
||||
| xdg_session_v1::Request::RestoreToplevel { id, toplevel, name } => {
|
||||
let key = session_key(session_id, &name);
|
||||
if state
|
||||
.sessions
|
||||
.toplevels
|
||||
.values()
|
||||
.any(|w| w.session_id == *session_id && w.key == key)
|
||||
|| (!restore_requested && state.sessions.stored_keys.contains(&key))
|
||||
{
|
||||
resource.post_error(
|
||||
xdg_session_v1::Error::NameInUse,
|
||||
"toplevel name already in use",
|
||||
);
|
||||
return;
|
||||
}
|
||||
let toplevel_id = toplevel.id();
|
||||
if state.sessions.toplevels.contains_key(&toplevel_id) {
|
||||
resource.post_error(
|
||||
xdg_session_v1::Error::AlreadyAdded,
|
||||
"toplevel already belongs to a session",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if restore_requested && state.sessions.committed_toplevels.contains(&toplevel_id) {
|
||||
resource.post_error(
|
||||
xdg_session_v1::Error::AlreadyMapped,
|
||||
"restore_toplevel after first commit",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let can_restore = restore_requested && state.sessions.stored_keys.contains(&key);
|
||||
let session_resource = data_init.init(id, ());
|
||||
state.sessions.toplevels.insert(
|
||||
toplevel_id,
|
||||
WindowSession {
|
||||
key,
|
||||
session_id: session_id.clone(),
|
||||
restore_requested: can_restore,
|
||||
restore_resource: Some(session_resource),
|
||||
},
|
||||
);
|
||||
if let Some(surface) = state.xdg_shell.get_toplevel(&toplevel) {
|
||||
state
|
||||
.wayvr_tasks
|
||||
.send(WayVRTask::WindowIdentityChanged(surface));
|
||||
}
|
||||
}
|
||||
xdg_session_v1::Request::RemoveToplevel { name } => {
|
||||
state.remove_session_toplevel(session_id, &name);
|
||||
}
|
||||
xdg_session_v1::Request::Destroy => {
|
||||
state.detach_session(session_id, false);
|
||||
state.sessions.sessions.insert(session_id.clone(), None);
|
||||
}
|
||||
xdg_session_v1::Request::Remove => {
|
||||
state.detach_session(session_id, true);
|
||||
let prefix = session_prefix(session_id);
|
||||
let keys: Vec<_> = state
|
||||
.sessions
|
||||
.stored_keys
|
||||
.iter()
|
||||
.filter(|key| key.starts_with(prefix.as_str()))
|
||||
.cloned()
|
||||
.collect();
|
||||
for key in keys {
|
||||
state.sessions.stored_keys.remove(&key);
|
||||
state.wayvr_tasks.send(WayVRTask::ForgetWindowState(key));
|
||||
}
|
||||
state.sessions.sessions.remove(session_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn destroyed(
|
||||
state: &mut Self,
|
||||
_client: smithay::reexports::wayland_server::backend::ClientId,
|
||||
resource: &XdgSessionV1,
|
||||
session_id: &String,
|
||||
) {
|
||||
if state
|
||||
.sessions
|
||||
.sessions
|
||||
.get(session_id)
|
||||
.and_then(Option::as_ref)
|
||||
== Some(resource)
|
||||
{
|
||||
state.detach_session(session_id, false);
|
||||
state.sessions.sessions.insert(session_id.clone(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<XdgToplevelSessionV1, ()> for Application {
|
||||
fn request(
|
||||
state: &mut Self,
|
||||
_client: &Client,
|
||||
resource: &XdgToplevelSessionV1,
|
||||
request: xdg_toplevel_session_v1::Request,
|
||||
_data: &(),
|
||||
_dh: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
match request {
|
||||
xdg_toplevel_session_v1::Request::Destroy => {
|
||||
for w in state.sessions.toplevels.values_mut() {
|
||||
if w.restore_resource.as_ref() == Some(resource) {
|
||||
w.restore_resource = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
xdg_toplevel_session_v1::Request::Rename { name } => {
|
||||
let entry = state
|
||||
.sessions
|
||||
.toplevels
|
||||
.iter()
|
||||
.find(|(_, w)| w.restore_resource.as_ref() == Some(resource))
|
||||
.map(|(id, w)| (id.clone(), w.session_id.clone(), w.key.clone()));
|
||||
let Some((id, session_id, old_key)) = entry else {
|
||||
return;
|
||||
};
|
||||
let new_key = session_key(&session_id, &name);
|
||||
if old_key == new_key {
|
||||
return;
|
||||
}
|
||||
if state.sessions.stored_keys.contains(&new_key)
|
||||
|| state
|
||||
.sessions
|
||||
.toplevels
|
||||
.values()
|
||||
.any(|w| w.session_id == session_id && w.key == new_key)
|
||||
{
|
||||
if let Some(Some(owner)) = state.sessions.sessions.get(&session_id) {
|
||||
owner.post_error(
|
||||
xdg_session_v1::Error::NameInUse,
|
||||
"toplevel name already in use",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(w) = state.sessions.toplevels.get_mut(&id) {
|
||||
w.key = new_key.clone();
|
||||
}
|
||||
if state.sessions.stored_keys.remove(&old_key) {
|
||||
state.sessions.stored_keys.insert(new_key.clone());
|
||||
}
|
||||
state
|
||||
.wayvr_tasks
|
||||
.send(WayVRTask::RenameWindowState(old_key, new_key));
|
||||
if let Some(surface) = state
|
||||
.xdg_shell
|
||||
.toplevel_surfaces()
|
||||
.iter()
|
||||
.find(|s| s.xdg_toplevel().id() == id)
|
||||
.cloned()
|
||||
{
|
||||
state
|
||||
.wayvr_tasks
|
||||
.send(WayVRTask::WindowIdentityChanged(surface));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn destroyed(
|
||||
state: &mut Self,
|
||||
_client: smithay::reexports::wayland_server::backend::ClientId,
|
||||
resource: &XdgToplevelSessionV1,
|
||||
_data: &(),
|
||||
) {
|
||||
for w in state.sessions.toplevels.values_mut() {
|
||||
if w.restore_resource.as_ref() == Some(resource) {
|
||||
w.restore_resource = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#[allow(dead_code, non_camel_case_types, unused_unsafe, unused_variables)]
|
||||
#[allow(non_upper_case_globals, non_snake_case, unused_imports)]
|
||||
#[allow(clippy::all)]
|
||||
pub mod server {
|
||||
use smithay::reexports::wayland_server;
|
||||
use wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use wayland_server::protocol::*;
|
||||
|
||||
pub mod __interfaces {
|
||||
use super::wayland_server;
|
||||
use wayland_protocols::xdg::shell::server::__interfaces::{
|
||||
XDG_TOPLEVEL_INTERFACE, xdg_toplevel_interface,
|
||||
};
|
||||
use wayland_server::backend as wayland_backend;
|
||||
use wayland_server::protocol::__interfaces::*;
|
||||
wayland_scanner::generate_interfaces!("protocols/xdg-session-management-v1.xml");
|
||||
}
|
||||
use self::__interfaces::{
|
||||
XDG_SESSION_MANAGER_V1_INTERFACE, XDG_SESSION_V1_INTERFACE,
|
||||
XDG_TOPLEVEL_SESSION_V1_INTERFACE,
|
||||
};
|
||||
wayland_scanner::generate_server_code!("protocols/xdg-session-management-v1.xml");
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
use std::rc::Rc;
|
||||
|
||||
use glam::DVec2;
|
||||
use slotmap::{DenseSlotMap, new_key_type};
|
||||
@@ -24,10 +24,6 @@ pub struct Window {
|
||||
pub visible: bool,
|
||||
pub toplevel: Rc<ToplevelSurface>,
|
||||
pub process: process::ProcessHandle,
|
||||
pub app_id: Option<Arc<str>>,
|
||||
pub parent: Option<WindowHandle>,
|
||||
pub name: Option<Arc<str>>,
|
||||
pub restore_on_spawn: bool,
|
||||
|
||||
pub pending_configure: Option<PendingConfigure>,
|
||||
}
|
||||
@@ -39,10 +35,6 @@ impl Window {
|
||||
bounds: Size<i32, Logical>,
|
||||
min_size: Size<i32, Logical>,
|
||||
max_size: Size<i32, Logical>,
|
||||
app_id: Option<Arc<str>>,
|
||||
parent: Option<WindowHandle>,
|
||||
name: Option<Arc<str>>,
|
||||
restore_on_spawn: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
bounds,
|
||||
@@ -53,10 +45,6 @@ impl Window {
|
||||
visible: true,
|
||||
toplevel,
|
||||
process,
|
||||
app_id,
|
||||
parent,
|
||||
name,
|
||||
restore_on_spawn,
|
||||
pending_configure: None,
|
||||
}
|
||||
}
|
||||
@@ -160,10 +148,6 @@ pub struct CreateWindowParams {
|
||||
pub max_size: Size<i32, Logical>,
|
||||
pub size_x: u32,
|
||||
pub size_y: u32,
|
||||
pub app_id: Option<Arc<str>>,
|
||||
pub parent: Option<WindowHandle>,
|
||||
pub name: Option<Arc<str>>,
|
||||
pub restore_on_spawn: bool,
|
||||
}
|
||||
|
||||
impl WindowManager {
|
||||
@@ -191,10 +175,6 @@ impl WindowManager {
|
||||
par.bounds,
|
||||
par.min_size,
|
||||
par.max_size,
|
||||
par.app_id,
|
||||
par.parent,
|
||||
par.name,
|
||||
par.restore_on_spawn,
|
||||
);
|
||||
window.remember_committed_size(Size::new(par.size_x as i32, par.size_y as i32), None);
|
||||
self.windows.insert(window)
|
||||
@@ -219,8 +199,4 @@ impl WindowHandle {
|
||||
user: self.0.as_ffi(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transient_name(self) -> Arc<str> {
|
||||
format!("wayvr/volatile/{}", self.as_packet().user).into()
|
||||
}
|
||||
}
|
||||
+3
-7
@@ -9,9 +9,9 @@ use wayvr_ipc::packet_client::WvrProcessLaunchParams;
|
||||
use wlx_common::{
|
||||
astr_containers::AStrMap,
|
||||
config::{
|
||||
AltModifier, CONFIG_VERSION, CaptureMethod, ChromaKeyParams, DefaultPositioning,
|
||||
GeneralConfig, HandsfreeAltTab, HandsfreePointer, InputCaptureMethod, InputEmulationMethod,
|
||||
PinnedApp, SerializedWindowSet,
|
||||
AltModifier, CaptureMethod, ChromaKeyParams, DefaultPositioning, GeneralConfig,
|
||||
HandsfreeAltTab, HandsfreePointer, InputCaptureMethod, InputEmulationMethod, PinnedApp,
|
||||
SerializedWindowSet,
|
||||
},
|
||||
config_io,
|
||||
locale::Language,
|
||||
@@ -200,7 +200,6 @@ pub struct AutoSettings {
|
||||
pub default_opacity: f32,
|
||||
pub default_positioning: DefaultPositioning,
|
||||
pub middle_grabs_single: bool,
|
||||
pub config_version: u32,
|
||||
}
|
||||
|
||||
fn get_settings_path() -> PathBuf {
|
||||
@@ -278,7 +277,6 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
|
||||
default_opacity: config.default_opacity,
|
||||
default_positioning: config.default_positioning,
|
||||
middle_grabs_single: config.middle_grabs_single,
|
||||
config_version: CONFIG_VERSION,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic
|
||||
@@ -296,7 +294,6 @@ pub struct AutoState {
|
||||
pub spawn_overlays: Vec<Arc<str>>,
|
||||
pub last_set: u32,
|
||||
pub attribs: AStrMap<Vec<BackendAttribValue>>,
|
||||
pub state_version: u32,
|
||||
}
|
||||
|
||||
fn get_state_path() -> PathBuf {
|
||||
@@ -311,7 +308,6 @@ pub fn save_state(config: &GeneralConfig) -> anyhow::Result<()> {
|
||||
last_set: config.last_set,
|
||||
spawn_overlays: config.spawn_overlays.clone(),
|
||||
attribs: config.attribs.clone(),
|
||||
state_version: CONFIG_VERSION,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic
|
||||
|
||||
@@ -4,11 +4,11 @@ use wgui::gfx::{DmabufImportInfo, DmabufPlaneLayout, Format, Image, ImageView, W
|
||||
use wlx_capture::{DrmFormat, DrmFourcc, DrmModifier, frame::DmabufFrame};
|
||||
|
||||
pub trait WGfxDmabuf {
|
||||
fn dmabuf_texture(&self, frame: DmabufFrame) -> anyhow::Result<Arc<Image>>;
|
||||
fn dmabuf_texture(&self, frame: &DmabufFrame) -> anyhow::Result<Arc<Image>>;
|
||||
}
|
||||
|
||||
impl WGfxDmabuf for WGfx {
|
||||
fn dmabuf_texture(&self, frame: DmabufFrame) -> anyhow::Result<Arc<Image>> {
|
||||
fn dmabuf_texture(&self, frame: &DmabufFrame) -> anyhow::Result<Arc<Image>> {
|
||||
let format = fourcc_to_vk(frame.format.drm_format.code)?;
|
||||
let modifier = (!matches!(frame.format.drm_format.modifier, DrmModifier::Invalid))
|
||||
.then(|| u64::from(frame.format.drm_format.modifier));
|
||||
|
||||
@@ -8,6 +8,8 @@ use wgui::gfx::{
|
||||
BufferUsage, BuiltCommandBuffer, Format, ShaderModule, Vert2Buf, Vert2Uv, WGfx, WGfxCreateInfo,
|
||||
};
|
||||
use wlx_capture::DrmFormat;
|
||||
#[cfg(feature = "pipewire")]
|
||||
use wlx_capture::syncobj::DrmRenderNode;
|
||||
|
||||
use crate::shaders::{
|
||||
frag_color, frag_grid, frag_screen, frag_simple, frag_sky, frag_srgb, frag_wayland, vert_quad,
|
||||
@@ -20,6 +22,8 @@ pub struct WGfxExtras {
|
||||
pub drm_formats: Arc<[DrmFormat]>,
|
||||
pub quad_verts: Vert2Buf,
|
||||
pub drm_device: Option<(i64, i64)>,
|
||||
#[cfg(feature = "pipewire")]
|
||||
pub drm_render_node: Option<DrmRenderNode>,
|
||||
}
|
||||
|
||||
impl WGfxExtras {
|
||||
@@ -55,19 +59,32 @@ impl WGfxExtras {
|
||||
];
|
||||
let quad_verts = gfx.new_buffer(BufferUsage::VERTEX_BUFFER, &vertices)?;
|
||||
|
||||
#[cfg(feature = "pipewire")]
|
||||
let drm_device = gfx.device_info().drm_render_node.map(|(major, minor)| {
|
||||
log::info!("DRM render device: {major} {minor}");
|
||||
(i64::from(major), i64::from(minor))
|
||||
});
|
||||
#[cfg(feature = "pipewire")]
|
||||
if drm_device.is_none() {
|
||||
log::warn!("No DRM device.");
|
||||
}
|
||||
#[cfg(feature = "pipewire")]
|
||||
let drm_render_node = drm_device.and_then(|(maj, min)| {
|
||||
use wgui::log::LogErr;
|
||||
use wlx_capture::syncobj::open_render_node;
|
||||
|
||||
open_render_node(maj as u32, min as u32)
|
||||
.log_err("Could not open DRM render node")
|
||||
.ok()
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
shaders,
|
||||
drm_formats,
|
||||
quad_verts,
|
||||
drm_device,
|
||||
#[cfg(feature = "pipewire")]
|
||||
drm_render_node,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ impl OverlayList {
|
||||
}
|
||||
|
||||
params.insert_rc("idx", i.to_string().into());
|
||||
params.insert("name", meta.title.as_deref().unwrap_or(&meta.name));
|
||||
params.insert("name", meta.name.as_ref());
|
||||
parser_state
|
||||
.instantiate_template(doc_params, template, layout, root, params)?;
|
||||
let overlay_button = parser_state
|
||||
|
||||
+27
-3
@@ -1,17 +1,38 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::backend::wayvr::{WayVRTask, WvrServerState};
|
||||
use crate::backend::wayvr::{self, WayVRTask, WvrServerState};
|
||||
use wayvr_ipc::packet_client::{HandsfreeMode, HandsfreeParams};
|
||||
use wayvr_ipc::packet_server::PacketServer;
|
||||
use wlx_common::config::HandsfreePointer;
|
||||
|
||||
use crate::{
|
||||
backend::task::{InputTask, OverlayTask, TaskType, ToggleMode},
|
||||
backend::{
|
||||
self,
|
||||
task::{InputTask, OverlayTask, TaskType, ToggleMode},
|
||||
},
|
||||
ipc::{signal::WayVRSignal, window_attrib, window_state},
|
||||
state::AppState,
|
||||
windowing::{OverlaySelector, manager::OverlayWindowManager},
|
||||
};
|
||||
|
||||
fn process_tick_tasks(
|
||||
tick_tasks: Vec<backend::wayvr::TickTask>,
|
||||
server_state: &mut WvrServerState,
|
||||
) {
|
||||
for tick_task in tick_tasks {
|
||||
match tick_task {
|
||||
backend::wayvr::TickTask::NewExternalProcess(request) => {
|
||||
log::info!("Registering external process with PID {}", request.pid);
|
||||
server_state.add_external_process(request.pid);
|
||||
server_state.manager.add_client(wayvr::client::WayVRClient {
|
||||
client: request.client,
|
||||
pid: request.pid,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick_events<O>(
|
||||
app: &mut AppState,
|
||||
overlays: &mut OverlayWindowManager<O>,
|
||||
@@ -114,7 +135,10 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
WvrServerState::tick_events(app)?;
|
||||
let tick_tasks = WvrServerState::tick_events(app)?;
|
||||
if let Some(wayvr_server) = app.wvr_server.as_mut() {
|
||||
process_tick_tasks(tick_tasks, wayvr_server);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -89,6 +89,8 @@ fn read_payload(conn: &mut local_socket::Stream, size: u32) -> anyhow::Result<Op
|
||||
|
||||
pub struct TickParams<'a> {
|
||||
pub wvr_server: &'a mut WvrServerState,
|
||||
#[allow(dead_code)]
|
||||
pub tasks: &'a mut Vec<wayvr::TickTask>,
|
||||
pub signals: &'a SyncEventQueue<WayVRSignal>,
|
||||
pub input_state: &'a InputState,
|
||||
pub hid_wrapper: &'a mut HidWrapper,
|
||||
|
||||
+19
-38
@@ -180,37 +180,43 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
setup_signal_hooks()?;
|
||||
|
||||
let mut used_backend = None;
|
||||
auto_run(&args, &mut used_backend);
|
||||
auto_run(args, &mut used_backend);
|
||||
|
||||
if RESTART.load(Ordering::Relaxed) {
|
||||
log::warn!("Restarting...");
|
||||
let exe = std::env::current_exe()?;
|
||||
let _ = Command::new(exe)
|
||||
.args(restart_args(&args, used_backend))
|
||||
.spawn();
|
||||
let mut args = vec!["--replace", "--show"];
|
||||
|
||||
match used_backend {
|
||||
Some(XrBackend::OpenXR) => args.push("--openxr"),
|
||||
Some(XrBackend::OpenVR) => args.push("--openvr"),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = Command::new(exe).args(args).spawn();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused_mut, clippy::similar_names)]
|
||||
fn auto_run(args: &Args, out_backend: &mut Option<XrBackend>) {
|
||||
fn auto_run(args: Args, used_backend: &mut Option<XrBackend>) {
|
||||
let mut tried_xr = false;
|
||||
let mut tried_vr = false;
|
||||
|
||||
loop {
|
||||
#[cfg(feature = "openxr")]
|
||||
if !args_get_openvr(args) {
|
||||
if !args_get_openvr(&args) {
|
||||
use crate::backend::{BackendError, openxr::openxr_run};
|
||||
tried_xr = true;
|
||||
match openxr_run(args, init_run_params(args.no_audio)) {
|
||||
match openxr_run(&args, init_run_params(args.no_audio)) {
|
||||
Ok(()) => {
|
||||
out_backend.replace(XrBackend::OpenXR);
|
||||
used_backend.replace(XrBackend::OpenXR);
|
||||
return;
|
||||
}
|
||||
Err(BackendError::NotSupported) => (),
|
||||
Err(e) => {
|
||||
out_backend.replace(XrBackend::OpenXR);
|
||||
used_backend.replace(XrBackend::OpenXR);
|
||||
log::error!("{e:?}");
|
||||
return;
|
||||
}
|
||||
@@ -218,17 +224,17 @@ fn auto_run(args: &Args, out_backend: &mut Option<XrBackend>) {
|
||||
}
|
||||
|
||||
#[cfg(feature = "openvr")]
|
||||
if !args_get_openxr(args) {
|
||||
if !args_get_openxr(&args) {
|
||||
use crate::backend::{BackendError, openvr::openvr_run};
|
||||
tried_vr = true;
|
||||
match openvr_run(args, init_run_params(args.no_audio)) {
|
||||
match openvr_run(&args, init_run_params(args.no_audio)) {
|
||||
Ok(()) => {
|
||||
out_backend.replace(XrBackend::OpenVR);
|
||||
used_backend.replace(XrBackend::OpenVR);
|
||||
return;
|
||||
}
|
||||
Err(BackendError::NotSupported) => (),
|
||||
Err(e) => {
|
||||
out_backend.replace(XrBackend::OpenVR);
|
||||
used_backend.replace(XrBackend::OpenVR);
|
||||
log::error!("{e:?}");
|
||||
return;
|
||||
}
|
||||
@@ -297,31 +303,6 @@ const fn args_get_openxr(args: &Args) -> bool {
|
||||
ret
|
||||
}
|
||||
|
||||
fn restart_args(args: &Args, backend: Option<XrBackend>) -> Vec<&'static str> {
|
||||
let mut flags = vec!["--replace", "--show"];
|
||||
|
||||
match backend {
|
||||
Some(XrBackend::OpenXR) => flags.push("--openxr"),
|
||||
Some(XrBackend::OpenVR) => flags.push("--openvr"),
|
||||
None => {}
|
||||
}
|
||||
|
||||
if args.headless {
|
||||
flags.push("--headless");
|
||||
}
|
||||
if args.no_autostart {
|
||||
flags.push("--no-autostart");
|
||||
}
|
||||
if args.install {
|
||||
flags.push("--install");
|
||||
}
|
||||
if args.no_audio {
|
||||
flags.push("--no-audio");
|
||||
}
|
||||
|
||||
flags
|
||||
}
|
||||
|
||||
#[allow(clippy::single_match_else)]
|
||||
fn setup_signal_hooks() -> anyhow::Result<()> {
|
||||
let mut signals = Signals::new([SIGINT, SIGTERM, SIGUSR1])?;
|
||||
|
||||
@@ -48,10 +48,12 @@ pub(super) enum CaptureType {
|
||||
|
||||
pub struct ScreenBackend {
|
||||
name: Arc<str>,
|
||||
// frames mut come before `capture` to maintain drop order
|
||||
cur_frame: Option<WlxCaptureOut>,
|
||||
pending_frame: Option<WlxCaptureOut>,
|
||||
capture_type: CaptureType,
|
||||
capture: Box<dyn WlxCapture<WlxCaptureIn, WlxCaptureOut>>,
|
||||
pipeline: Option<ScreenPipeline>,
|
||||
cur_frame: Option<WlxCaptureOut>,
|
||||
meta: Option<FrameMeta>,
|
||||
mouse_transform: Affine2,
|
||||
interaction_transform: Option<Affine2>,
|
||||
@@ -77,6 +79,7 @@ impl ScreenBackend {
|
||||
capture,
|
||||
pipeline: None,
|
||||
cur_frame: None,
|
||||
pending_frame: None,
|
||||
meta: None,
|
||||
mouse_transform: Affine2::ZERO,
|
||||
interaction_transform: None,
|
||||
@@ -204,7 +207,26 @@ impl OverlayBackend for ScreenBackend {
|
||||
return Ok(ShouldRender::Unable);
|
||||
}
|
||||
|
||||
if let Some(frame) = self.capture.receive() {
|
||||
// don't submit the new image until the producer signaled
|
||||
if let Some(mut frame) = self.capture.receive()
|
||||
&& (self.pending_frame.is_none() || matches!(frame.is_ready(), Ok(true)))
|
||||
{
|
||||
self.pending_frame = Some(frame);
|
||||
}
|
||||
let pending_ready = match self.pending_frame.as_mut() {
|
||||
Some(frame) => match frame.is_ready() {
|
||||
Ok(ready) => ready,
|
||||
Err(e) => {
|
||||
log::warn!("{}: failed to check DMA-buf acquire fence: {e}", self.name);
|
||||
self.pending_frame = None;
|
||||
false
|
||||
}
|
||||
},
|
||||
None => false,
|
||||
};
|
||||
|
||||
if pending_ready {
|
||||
let frame = self.pending_frame.take().unwrap();
|
||||
let stereo = self.stereo.unwrap_or(StereoMode::None);
|
||||
let mut meta = frame.get_frame_meta(&app.session.config, stereo);
|
||||
|
||||
|
||||
@@ -147,7 +147,6 @@ impl ScreenPipeline {
|
||||
|
||||
/// Select normalized rect from backing image
|
||||
pub fn set_source_uv(&mut self, source_uv: [f32; 4]) {
|
||||
#[allow(clippy::float_cmp)]
|
||||
if self.source_uv != source_uv {
|
||||
self.source_uv = source_uv;
|
||||
self.pass.clear();
|
||||
@@ -559,9 +558,15 @@ pub(super) struct WlxCaptureOut {
|
||||
pub(super) image: Arc<ImageView>,
|
||||
pub(super) format: FrameFormat,
|
||||
pub(super) mouse: Option<MouseMeta>,
|
||||
pub(super) acquire_sync: wlx_frame::FrameAcquireSync,
|
||||
pub(super) _lease: Option<wlx_frame::FrameLease>,
|
||||
}
|
||||
|
||||
impl WlxCaptureOut {
|
||||
pub(super) fn is_ready(&mut self) -> std::io::Result<bool> {
|
||||
self.acquire_sync.check_ready()
|
||||
}
|
||||
|
||||
pub(super) fn get_frame_meta(&self, config: &GeneralConfig, stereo: StereoMode) -> FrameMeta {
|
||||
FrameMeta {
|
||||
clear: WGfxClearMode::DontCare,
|
||||
@@ -720,18 +725,23 @@ fn upload_image_with_row_length(
|
||||
|
||||
pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<WlxCaptureOut> {
|
||||
match frame {
|
||||
WlxFrame::Dmabuf(frame) => {
|
||||
WlxFrame::Dmabuf(mut frame) => {
|
||||
if !frame.is_valid() {
|
||||
log::error!("{}: Invalid frame", me.name);
|
||||
return None;
|
||||
}
|
||||
log::trace!("{}: New DMA-buf frame", me.name);
|
||||
let format = frame.format;
|
||||
match me.gfx.dmabuf_texture(frame) {
|
||||
let lease = frame.lease.clone();
|
||||
let acquire_sync = std::mem::take(&mut frame.acquire_sync);
|
||||
|
||||
match me.gfx.dmabuf_texture(&frame) {
|
||||
Ok(image) => Some(WlxCaptureOut {
|
||||
image: me.gfx.create_image_view(image).ok()?,
|
||||
format,
|
||||
mouse: None,
|
||||
acquire_sync,
|
||||
_lease: lease,
|
||||
}),
|
||||
Err(e) => {
|
||||
log::error!("{}: Failed to create DMA-buf vkImage: {}", me.name, e);
|
||||
@@ -767,6 +777,8 @@ pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<Wlx
|
||||
image: me.gfx.create_image_view(image).ok()?,
|
||||
format: frame.format,
|
||||
mouse: None,
|
||||
acquire_sync: wlx_frame::FrameAcquireSync::Ready,
|
||||
_lease: None,
|
||||
})
|
||||
}
|
||||
WlxFrame::MemPtr(frame) => {
|
||||
@@ -801,6 +813,8 @@ pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<Wlx
|
||||
image: me.gfx.create_image_view(image).ok()?,
|
||||
format: frame.format,
|
||||
mouse: frame.mouse,
|
||||
acquire_sync: wlx_frame::FrameAcquireSync::Ready,
|
||||
_lease: None,
|
||||
})
|
||||
}
|
||||
WlxFrame::Implicit(transform) => {
|
||||
@@ -824,6 +838,8 @@ pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<Wlx
|
||||
image,
|
||||
format,
|
||||
mouse: None,
|
||||
acquire_sync: wlx_frame::FrameAcquireSync::Ready,
|
||||
_lease: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,8 +344,14 @@ fn check(mut par: CheckParams, finalize_fn: ScreenCastFinalizeFn) {
|
||||
par.app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
|
||||
OverlaySelector::Name(par.name.clone()),
|
||||
Box::new(move |app, owc| {
|
||||
let capture =
|
||||
new_wlx_capture!(app, PipewireCapture::new(par.name.clone(), node_id));
|
||||
let capture = new_wlx_capture!(
|
||||
app,
|
||||
PipewireCapture::new(
|
||||
par.name.clone(),
|
||||
node_id,
|
||||
app.gfx_extras.drm_render_node.clone()
|
||||
)
|
||||
);
|
||||
|
||||
owc.backend =
|
||||
finalize_fn(par.name, par.logical_pos, par.logical_size, capture, app);
|
||||
|
||||
+13
-25
@@ -70,7 +70,6 @@ use crate::{
|
||||
#[derive(Clone)]
|
||||
pub enum WvrCommand {
|
||||
ReloadTitle,
|
||||
SetIcon(Arc<str>),
|
||||
CloseWindow,
|
||||
KillProcess(KillSignal),
|
||||
}
|
||||
@@ -89,9 +88,6 @@ const BLEND_WAYLAND_ALPHA: AttachmentBlend = AttachmentBlend {
|
||||
|
||||
pub fn create_wl_window_overlay(
|
||||
name: Arc<str>,
|
||||
title: Arc<str>,
|
||||
persistent: bool,
|
||||
restore_on_spawn: bool,
|
||||
app: &mut AppState,
|
||||
window: wayvr::window::WindowHandle,
|
||||
icon: Arc<str>,
|
||||
@@ -113,10 +109,7 @@ pub fn create_wl_window_overlay(
|
||||
.is_some_and(Window::resizable);
|
||||
|
||||
Ok(OverlayWindowConfig {
|
||||
name,
|
||||
title: Some(title.clone()),
|
||||
persistent,
|
||||
restore_on_spawn,
|
||||
name: name.clone(),
|
||||
default_state: OverlayWindowState {
|
||||
grabbable: true,
|
||||
interactable: true,
|
||||
@@ -138,7 +131,7 @@ pub fn create_wl_window_overlay(
|
||||
category: OverlayCategory::WayVR,
|
||||
show_on_spawn: true,
|
||||
..OverlayWindowConfig::from_backend(Box::new(WvrWindowBackend::new(
|
||||
title,
|
||||
name,
|
||||
app,
|
||||
window,
|
||||
icon,
|
||||
@@ -149,7 +142,7 @@ pub fn create_wl_window_overlay(
|
||||
}
|
||||
|
||||
pub struct WvrWindowBackend {
|
||||
title: Arc<str>,
|
||||
name: Arc<str>,
|
||||
icon: Arc<str>,
|
||||
pipeline: Option<ScreenPipeline>,
|
||||
subsurface_pipeline_opaque: Arc<WGfxPipeline<Vert2Uv>>,
|
||||
@@ -183,7 +176,7 @@ pub struct WvrWindowBackend {
|
||||
|
||||
impl WvrWindowBackend {
|
||||
fn new(
|
||||
title: Arc<str>,
|
||||
name: Arc<str>,
|
||||
app: &mut AppState,
|
||||
window: wayvr::window::WindowHandle,
|
||||
icon: Arc<str>,
|
||||
@@ -250,7 +243,7 @@ impl WvrWindowBackend {
|
||||
NewGuiPanelParams {
|
||||
resize_to_parent: true,
|
||||
on_custom_attrib: Some(on_custom_attrib),
|
||||
extra_vars: [("title".into(), title.as_ref().into())].into(),
|
||||
extra_vars: [("title".into(), name.as_ref().into())].into(),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
@@ -258,7 +251,7 @@ impl WvrWindowBackend {
|
||||
panel.update_layout(app)?;
|
||||
|
||||
Ok(Self {
|
||||
title,
|
||||
name,
|
||||
icon,
|
||||
pipeline: None,
|
||||
window,
|
||||
@@ -397,7 +390,6 @@ impl WvrWindowBackend {
|
||||
}
|
||||
|
||||
fn update_title(&mut self, new_title: Rc<str>) {
|
||||
self.title = new_title.as_ref().into();
|
||||
let mut common = CallbackDataCommon {
|
||||
state: &self.panel.layout.state,
|
||||
alterables: &mut self.panel.layout.alterables,
|
||||
@@ -576,7 +568,7 @@ impl OverlayBackend for WvrWindowBackend {
|
||||
else {
|
||||
log::debug!(
|
||||
"{:?}: WayVR overlay without matching window entry",
|
||||
self.title
|
||||
self.name
|
||||
);
|
||||
return Ok(ShouldRender::Unable);
|
||||
};
|
||||
@@ -661,11 +653,10 @@ impl OverlayBackend for WvrWindowBackend {
|
||||
let source_uv = surf.source_uv(states);
|
||||
let mut xdg_state = states.cached_state.get::<SurfaceCachedState>();
|
||||
let window_geometry = xdg_state.current().geometry;
|
||||
drop(xdg_state);
|
||||
Some((surf, surface_size, source_uv, window_geometry))
|
||||
})
|
||||
else {
|
||||
log::trace!("{}: no buffer for wl_surface", self.title);
|
||||
log::trace!("{}: no buffer for wl_surface", self.name);
|
||||
return Ok(ShouldRender::Unable);
|
||||
};
|
||||
|
||||
@@ -700,9 +691,9 @@ impl OverlayBackend for WvrWindowBackend {
|
||||
}
|
||||
|
||||
let inner_extent = meta.extent;
|
||||
let committed_window_extent = window_geometry.map_or(inner_extent, |geometry| {
|
||||
[geometry.size.w.max(1) as u32, geometry.size.h.max(1) as u32]
|
||||
});
|
||||
let committed_window_extent = window_geometry
|
||||
.map(|geometry| [geometry.size.w.max(1) as u32, geometry.size.h.max(1) as u32])
|
||||
.unwrap_or(inner_extent);
|
||||
self.sync_committed_toplevel_size(app, committed_window_extent);
|
||||
|
||||
if self.server_side_decorations {
|
||||
@@ -780,7 +771,7 @@ impl OverlayBackend for WvrWindowBackend {
|
||||
{
|
||||
log::trace!(
|
||||
"{}: new {} image",
|
||||
self.title,
|
||||
self.name,
|
||||
if surf.dmabuf { "DMA-buf" } else { "SHM" }
|
||||
);
|
||||
self.cur_image = Some(surf.image);
|
||||
@@ -866,9 +857,6 @@ impl OverlayBackend for WvrWindowBackend {
|
||||
let wvr_server = app.wvr_server.as_mut().unwrap(); //never None
|
||||
wvr_server.overlay_added(oid, self.window);
|
||||
}
|
||||
OverlayEventData::WvrCommand(WvrCommand::SetIcon(icon)) => {
|
||||
self.icon = icon;
|
||||
}
|
||||
OverlayEventData::WvrCommand(WvrCommand::ReloadTitle) => {
|
||||
let wvr_server = app.wvr_server.as_mut().unwrap(); //never None
|
||||
if let Some(window) = wvr_server.wm.windows.get(self.window) {
|
||||
@@ -1173,7 +1161,7 @@ impl OverlayBackend for WvrWindowBackend {
|
||||
match value {
|
||||
BackendAttribValue::Stereo(new) => {
|
||||
if let Some(stereo) = self.stereo.as_mut() {
|
||||
log::debug!("{}: stereo: {stereo:?} → {new:?}", self.title);
|
||||
log::debug!("{}: stereo: {stereo:?} → {new:?}", self.name);
|
||||
*stereo = new;
|
||||
if let Some(meta) = self.meta {
|
||||
let _ = self.apply_extent(app, &meta);
|
||||
|
||||
@@ -223,22 +223,26 @@
|
||||
},
|
||||
{
|
||||
"inputs" : {
|
||||
"position" : {
|
||||
"scroll" : {
|
||||
"output" : "/actions/default/in/scroll"
|
||||
}
|
||||
},
|
||||
"mode" : "joystick",
|
||||
"parameters" : {},
|
||||
"mode" : "scroll",
|
||||
"parameters" : {
|
||||
"scroll_mode" : "smooth"
|
||||
},
|
||||
"path" : "/user/hand/left/input/thumbstick"
|
||||
},
|
||||
{
|
||||
"inputs" : {
|
||||
"position" : {
|
||||
"scroll" : {
|
||||
"output" : "/actions/default/in/scroll"
|
||||
}
|
||||
},
|
||||
"mode" : "joystick",
|
||||
"parameters" : {},
|
||||
"mode" : "scroll",
|
||||
"parameters" : {
|
||||
"scroll_mode" : "smooth"
|
||||
},
|
||||
"path" : "/user/hand/right/input/thumbstick"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -141,22 +141,26 @@
|
||||
},
|
||||
{
|
||||
"inputs" : {
|
||||
"position" : {
|
||||
"scroll" : {
|
||||
"output" : "/actions/default/in/scroll"
|
||||
}
|
||||
},
|
||||
"mode" : "joystick",
|
||||
"parameters" : {},
|
||||
"mode" : "scroll",
|
||||
"parameters" : {
|
||||
"scroll_mode" : "smooth"
|
||||
},
|
||||
"path" : "/user/hand/left/input/thumbstick"
|
||||
},
|
||||
{
|
||||
"inputs" : {
|
||||
"position" : {
|
||||
"scroll" : {
|
||||
"output" : "/actions/default/in/scroll"
|
||||
}
|
||||
},
|
||||
"mode" : "joystick",
|
||||
"parameters" : {},
|
||||
"mode" : "scroll",
|
||||
"parameters" : {
|
||||
"scroll_mode" : "smooth"
|
||||
},
|
||||
"path" : "/user/hand/right/input/thumbstick"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -107,22 +107,26 @@
|
||||
},
|
||||
{
|
||||
"inputs" : {
|
||||
"position" : {
|
||||
"scroll" : {
|
||||
"output" : "/actions/default/in/scroll"
|
||||
}
|
||||
},
|
||||
"mode" : "joystick",
|
||||
"parameters" : {},
|
||||
"mode" : "scroll",
|
||||
"parameters" : {
|
||||
"scroll_mode" : "smooth"
|
||||
},
|
||||
"path" : "/user/hand/left/input/joystick"
|
||||
},
|
||||
{
|
||||
"inputs" : {
|
||||
"position" : {
|
||||
"scroll" : {
|
||||
"output" : "/actions/default/in/scroll"
|
||||
}
|
||||
},
|
||||
"mode" : "joystick",
|
||||
"parameters" : {},
|
||||
"mode" : "scroll",
|
||||
"parameters" : {
|
||||
"scroll_mode" : "smooth"
|
||||
},
|
||||
"path" : "/user/hand/right/input/joystick"
|
||||
}
|
||||
]
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ impl AppState {
|
||||
let wvr_signals = SyncEventQueue::new();
|
||||
|
||||
let wvr_server = {
|
||||
let mut maybe_wvr = WvrServerState::new(gfx.clone(), &gfx_extras, &session.config)
|
||||
let mut maybe_wvr = WvrServerState::new(gfx.clone(), &gfx_extras)
|
||||
.log_err("Could not initialize WayVR Server")
|
||||
.ok();
|
||||
if let Some(wvr) = maybe_wvr.as_mut() {
|
||||
|
||||
@@ -127,7 +127,6 @@ macro_rules! attrib_value {
|
||||
pub struct OverlayMeta {
|
||||
pub id: OverlayID,
|
||||
pub name: Arc<str>,
|
||||
pub title: Option<Arc<str>>,
|
||||
pub category: OverlayCategory,
|
||||
pub visible: bool,
|
||||
pub icon: Option<Arc<str>>,
|
||||
|
||||
+44
-309
@@ -1,7 +1,6 @@
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
sync::atomic::Ordering,
|
||||
};
|
||||
|
||||
@@ -10,11 +9,9 @@ use glam::{Affine3A, Vec3, Vec3A};
|
||||
use slotmap::{Key, SecondaryMap, SlotMap};
|
||||
use wgui::{i18n::Translation, log::LogErr};
|
||||
use wlx_common::{
|
||||
DesktopBackend,
|
||||
astr_containers::{AStrMap, AStrMapExt},
|
||||
config::SerializedWindowSet,
|
||||
overlays::{BackendAttrib, BackendAttribValue, ToastTopic},
|
||||
windowing::{OverlayWindowState, Positioning},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -87,8 +84,6 @@ where
|
||||
};
|
||||
|
||||
if headless {
|
||||
app.screens.clear();
|
||||
app.feats.desktop_backend = DesktopBackend::Headless;
|
||||
log::info!("Running in headless mode; keyboard will be en-US");
|
||||
} else {
|
||||
// create one window set for each screen.
|
||||
@@ -97,12 +92,12 @@ where
|
||||
match create_screens(app) {
|
||||
Ok((data, backend)) => {
|
||||
app.feats.desktop_backend = backend;
|
||||
let screen_count = data.screens.len();
|
||||
let last_idx = data.screens.len() - 1;
|
||||
for (idx, (meta, mut config)) in data.screens.into_iter().enumerate() {
|
||||
config.show_on_spawn = true;
|
||||
me.add(OverlayWindowData::from_config(config), app);
|
||||
|
||||
if idx + 1 < screen_count {
|
||||
if idx < last_idx {
|
||||
me.sets.push(OverlayWindowSet::default());
|
||||
me.switch_to_set(app, Some(me.current_set.unwrap() + 1), false);
|
||||
}
|
||||
@@ -248,7 +243,6 @@ where
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let key = o.config.name.clone();
|
||||
let parent_set = if o.config.global {
|
||||
&mut self.global_set
|
||||
} else {
|
||||
@@ -260,8 +254,8 @@ where
|
||||
|
||||
parent_set
|
||||
.hidden_overlays
|
||||
.arc_set(key.clone(), active_state);
|
||||
} else if let Some(state) = parent_set.hidden_overlays.arc_rm(&key) {
|
||||
.arc_set(o.config.name.clone(), active_state);
|
||||
} else if let Some(state) = parent_set.hidden_overlays.arc_rm(&o.config.name) {
|
||||
let o = &mut self.overlays[id];
|
||||
log::debug!("{}: toggle on", o.config.name);
|
||||
o.config.dirty = true;
|
||||
@@ -393,23 +387,9 @@ where
|
||||
OverlayTask::Modify(sel, f) => {
|
||||
if let Some(o) = self.mut_by_selector(&sel) {
|
||||
let was_visible = o.config.is_active();
|
||||
let old_title = o.config.title.clone();
|
||||
let old_icon = match o.config.backend.get_attrib(BackendAttrib::Icon) {
|
||||
Some(BackendAttribValue::Icon(icon)) => Some(icon),
|
||||
_ => None,
|
||||
};
|
||||
f(app, &mut o.config);
|
||||
let new_icon = match o.config.backend.get_attrib(BackendAttrib::Icon) {
|
||||
Some(BackendAttribValue::Icon(icon)) => Some(icon),
|
||||
_ => None,
|
||||
};
|
||||
let title_changed = old_title != o.config.title || old_icon != new_icon;
|
||||
let visibility_changed = was_visible != o.config.is_active();
|
||||
|
||||
if title_changed {
|
||||
self.overlays_changed(app)?;
|
||||
}
|
||||
if visibility_changed {
|
||||
if was_visible != o.config.is_active() {
|
||||
let _ = self.visible_overlays_changed(app);
|
||||
}
|
||||
} else {
|
||||
@@ -420,52 +400,65 @@ where
|
||||
self.spawn_overlay(app, sel, spawn_pos, f);
|
||||
}
|
||||
OverlayTask::Drop(sel) => {
|
||||
let id = self.id_by_selector(&sel);
|
||||
let (id, name) = match &sel {
|
||||
OverlaySelector::Id(id) => {
|
||||
let id = *id;
|
||||
let name = self.overlays.get(id).map(|o| o.config.name.clone());
|
||||
(Some(id), name)
|
||||
}
|
||||
OverlaySelector::Name(name) => {
|
||||
let id = self.lookup(name);
|
||||
(id, Some(name.clone()))
|
||||
}
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
if let Some(o) = self.mut_by_selector(&sel)
|
||||
&& o.birthframe < FRAME_COUNTER.load(Ordering::Relaxed)
|
||||
&& let Some(o) = self.remove_by_selector(&sel, app)
|
||||
{
|
||||
log::debug!("Dropping overlay {}", o.config.name);
|
||||
if let Some(id) = id {
|
||||
self.on_overlay_dropped(id, &o);
|
||||
if let (Some(id), Some(name)) = (id, name) {
|
||||
self.remove_saved_state(id, &name);
|
||||
}
|
||||
self.dropped_overlays.push_back(o);
|
||||
}
|
||||
}
|
||||
OverlayTask::DropImmediate(sel) => {
|
||||
let id = self.id_by_selector(&sel);
|
||||
let (id, name) = match &sel {
|
||||
OverlaySelector::Id(id) => {
|
||||
let id = *id;
|
||||
let name = self.overlays.get(id).map(|o| o.config.name.clone());
|
||||
(Some(id), name)
|
||||
}
|
||||
OverlaySelector::Name(name) => {
|
||||
let id = self.lookup(name);
|
||||
(id, Some(name.clone()))
|
||||
}
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
if let Some(o) = self.remove_by_selector(&sel, app) {
|
||||
log::debug!("Dropping overlay {} immediately", o.config.name);
|
||||
if let Some(id) = id {
|
||||
self.on_overlay_dropped(id, &o);
|
||||
if let (Some(id), Some(name)) = (id, name) {
|
||||
self.remove_saved_state(id, &name);
|
||||
}
|
||||
self.dropped_overlays.push_back(o);
|
||||
}
|
||||
}
|
||||
OverlayTask::SetOverlayName(id, name, persistent, restore_on_spawn) => {
|
||||
self.set_window_name(app, id, name, persistent, restore_on_spawn);
|
||||
}
|
||||
OverlayTask::ForgetWindowState(id, key) => {
|
||||
self.freeze_window_state(id, &key);
|
||||
}
|
||||
OverlayTask::RenameWindowState(old, new) => {
|
||||
self.rename_window_state(app, &old, &new);
|
||||
}
|
||||
OverlayTask::ClearWindowState(key) => {
|
||||
self.clear_window_state(app, &key);
|
||||
}
|
||||
OverlayTask::DropExpired(id) => {
|
||||
let should_drop = self
|
||||
.overlays
|
||||
.get(id)
|
||||
.and_then(|overlay| overlay.config.lifetime.as_ref())
|
||||
.is_some_and(super::window::OverlayLifetime::is_expired);
|
||||
.is_some_and(|lifetime| lifetime.is_expired());
|
||||
|
||||
if should_drop
|
||||
&& let Some(o) = self.remove_by_selector(&OverlaySelector::Id(id), app)
|
||||
{
|
||||
log::debug!("Dropping expired overlay {}", o.config.name);
|
||||
self.on_overlay_dropped(id, &o);
|
||||
let name = o.config.name.clone();
|
||||
self.remove_saved_state(id, &name);
|
||||
self.dropped_overlays.push_back(o);
|
||||
}
|
||||
}
|
||||
@@ -539,195 +532,14 @@ impl<T> OverlayWindowManager<T> {
|
||||
self.dropped_overlays.pop_front()
|
||||
}
|
||||
|
||||
fn on_overlay_dropped(&mut self, id: OverlayID, overlay: &OverlayWindowData<T>) {
|
||||
let name = &overlay.config.name;
|
||||
let remember_position =
|
||||
overlay.config.category == OverlayCategory::WayVR && overlay.config.can_persist();
|
||||
|
||||
// keep removed overlays by name
|
||||
for set in &mut self.sets {
|
||||
let state = set.overlays.remove(id);
|
||||
let hidden_state = set.hidden_overlays.arc_rm(name);
|
||||
if remember_position {
|
||||
if let Some(state) = state.or(hidden_state) {
|
||||
set.inactive_overlays.arc_set(name.clone(), state);
|
||||
}
|
||||
} else {
|
||||
set.inactive_overlays.arc_rm(name);
|
||||
}
|
||||
}
|
||||
|
||||
let state = self.global_set.overlays.remove(id);
|
||||
let hidden_state = self.global_set.hidden_overlays.arc_rm(name);
|
||||
if remember_position {
|
||||
if let Some(state) = state.or(hidden_state) {
|
||||
self.global_set
|
||||
.inactive_overlays
|
||||
.arc_set(name.clone(), state);
|
||||
}
|
||||
|
||||
if let Some(state) = &overlay.config.active_state {
|
||||
let set = if overlay.config.global {
|
||||
&mut self.global_set
|
||||
} else {
|
||||
&mut self.sets[self.current_set.unwrap_or(self.restore_set)]
|
||||
};
|
||||
set.inactive_overlays.arc_set(name.clone(), state.clone());
|
||||
}
|
||||
} else {
|
||||
// transient overlays (toasts, etc)
|
||||
self.global_set.inactive_overlays.arc_rm(name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the name of an existing window, restoring the state associated with the new name
|
||||
fn set_window_name(
|
||||
&mut self,
|
||||
app: &mut AppState,
|
||||
id: OverlayID,
|
||||
name: Arc<str>,
|
||||
persistent: bool,
|
||||
restore_on_spawn: bool,
|
||||
) {
|
||||
let Some(overlay) = self.overlays.get(id) else {
|
||||
return;
|
||||
};
|
||||
if overlay.config.category != OverlayCategory::WayVR
|
||||
|| (overlay.config.name == name
|
||||
&& overlay.config.persistent == persistent
|
||||
&& overlay.config.restore_on_spawn == restore_on_spawn)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if self
|
||||
.overlays
|
||||
.iter()
|
||||
.any(|(other_id, other)| other_id != id && other.config.name == name)
|
||||
{
|
||||
log::debug!("Ignoring duplicate WayVR window name {name}");
|
||||
return;
|
||||
}
|
||||
|
||||
let old_name = overlay.config.name.clone();
|
||||
let global = overlay.config.global;
|
||||
if old_name != name {
|
||||
for set in &mut self.sets {
|
||||
if let Some(state) = set.hidden_overlays.arc_rm(&old_name) {
|
||||
set.hidden_overlays.arc_set(name.clone(), state);
|
||||
}
|
||||
}
|
||||
if let Some(state) = self.global_set.hidden_overlays.arc_rm(&old_name) {
|
||||
self.global_set.hidden_overlays.arc_set(name.clone(), state);
|
||||
}
|
||||
}
|
||||
{
|
||||
let config = &mut self.overlays[id].config;
|
||||
config.name = name.clone();
|
||||
config.persistent = persistent;
|
||||
config.restore_on_spawn = restore_on_spawn;
|
||||
}
|
||||
if let Err(err) = self.overlays_changed(app) {
|
||||
log::error!("Unable to update overlay name: {err:?}");
|
||||
}
|
||||
if !persistent || !restore_on_spawn {
|
||||
return;
|
||||
}
|
||||
|
||||
// apply to all saved states
|
||||
if global {
|
||||
if let Some(state) = self.global_set.inactive_overlays.arc_rm(&name) {
|
||||
let o = &mut self.overlays[id];
|
||||
o.config.active_state = Some(state);
|
||||
o.config.reset(app, false);
|
||||
o.config.dirty = true;
|
||||
}
|
||||
} else {
|
||||
for (i, set) in self.sets.iter_mut().enumerate() {
|
||||
let Some(state) = set.inactive_overlays.arc_rm(&name) else {
|
||||
continue;
|
||||
};
|
||||
if self.current_set == Some(i) {
|
||||
let o = &mut self.overlays[id];
|
||||
o.config.active_state = Some(state);
|
||||
o.config.reset(app, false);
|
||||
o.config.dirty = true;
|
||||
} else {
|
||||
set.overlays.insert(id, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn freeze_window_state(&mut self, id: OverlayID, name: &Arc<str>) {
|
||||
let Some(overlay) = self.overlays.get(id) else {
|
||||
return;
|
||||
};
|
||||
if let Some(state) = overlay.config.active_state.clone() {
|
||||
let set = if overlay.config.global {
|
||||
&mut self.global_set
|
||||
} else {
|
||||
&mut self.sets[self.current_set.unwrap_or(self.restore_set)]
|
||||
};
|
||||
set.inactive_overlays.arc_set(name.clone(), state);
|
||||
} else {
|
||||
for set in &mut self.sets {
|
||||
if let Some(state) = set.overlays.get(id) {
|
||||
set.inactive_overlays.arc_set(name.clone(), state.clone());
|
||||
}
|
||||
}
|
||||
if let Some(state) = self.global_set.overlays.get(id) {
|
||||
self.global_set
|
||||
.inactive_overlays
|
||||
.arc_set(name.clone(), state.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rename_window_state(&mut self, app: &mut AppState, old: &Arc<str>, new: &Arc<str>) {
|
||||
if old == new {
|
||||
return;
|
||||
}
|
||||
for set in &mut self.sets {
|
||||
if let Some(state) = set.inactive_overlays.arc_rm(old) {
|
||||
set.inactive_overlays.arc_set(new.clone(), state);
|
||||
}
|
||||
if let Some(state) = set.hidden_overlays.arc_rm(old) {
|
||||
set.hidden_overlays.arc_set(new.clone(), state);
|
||||
}
|
||||
}
|
||||
if let Some(state) = self.global_set.inactive_overlays.arc_rm(old) {
|
||||
self.global_set
|
||||
.inactive_overlays
|
||||
.arc_set(new.clone(), state);
|
||||
}
|
||||
if let Some(state) = self.global_set.hidden_overlays.arc_rm(old) {
|
||||
self.global_set.hidden_overlays.arc_set(new.clone(), state);
|
||||
}
|
||||
if let Some(attrs) = app.session.config.attribs.arc_rm(old) {
|
||||
app.session.config.attribs.arc_set(new.clone(), attrs);
|
||||
}
|
||||
for set in &mut app.session.config.sets {
|
||||
if let Some(state) = set.overlays.remove(old.as_ref()) {
|
||||
set.overlays.insert(new.clone(), state);
|
||||
}
|
||||
if let Some(state) = set.hidden_overlays.remove(old.as_ref()) {
|
||||
set.hidden_overlays.insert(new.clone(), state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_window_state(&mut self, app: &mut AppState, name: &str) {
|
||||
fn remove_saved_state(&mut self, id: OverlayID, name: &str) {
|
||||
for set in &mut self.sets {
|
||||
set.overlays.retain(|k, _| k != id);
|
||||
set.inactive_overlays.arc_rm(name);
|
||||
set.hidden_overlays.arc_rm(name);
|
||||
}
|
||||
self.global_set.inactive_overlays.arc_rm(name);
|
||||
self.global_set.hidden_overlays.arc_rm(name);
|
||||
app.session.config.attribs.arc_rm(name);
|
||||
for set in &mut app.session.config.sets {
|
||||
set.overlays.remove(name);
|
||||
set.hidden_overlays.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn persist_layout(&mut self, app: &mut AppState) {
|
||||
@@ -749,11 +561,7 @@ impl<T> OverlayWindowManager<T> {
|
||||
.overlays
|
||||
.iter()
|
||||
.filter_map(|(k, v)| {
|
||||
let o = self.overlays.get(k)?;
|
||||
if !o.config.can_persist() {
|
||||
return None;
|
||||
}
|
||||
let n = o.config.name.clone();
|
||||
let n = self.overlays.get(k).map(|o| o.config.name.clone())?;
|
||||
Some((n, v.clone()))
|
||||
})
|
||||
.collect();
|
||||
@@ -796,7 +604,6 @@ impl<T> OverlayWindowManager<T> {
|
||||
for o in self.overlays.values() {
|
||||
if o.config.global
|
||||
&& let Some(state) = &o.config.active_state
|
||||
&& o.config.can_persist()
|
||||
{
|
||||
global_overlays.insert(o.config.name.clone(), state.clone());
|
||||
}
|
||||
@@ -821,7 +628,7 @@ impl<T> OverlayWindowManager<T> {
|
||||
.filter_map(|a| o.config.backend.get_attrib(*a))
|
||||
.filter(|val| !val.is_default())
|
||||
.collect();
|
||||
if !attrs.is_empty() && o.config.can_persist() {
|
||||
if !attrs.is_empty() {
|
||||
app.session
|
||||
.config
|
||||
.attribs
|
||||
@@ -840,29 +647,6 @@ impl<T> OverlayWindowManager<T> {
|
||||
return;
|
||||
}
|
||||
|
||||
// floating transforms in older states are relative to hmd, not anchor
|
||||
if app.session.config.state_version < 1 {
|
||||
let clear_legacy_floating = |state: &mut OverlayWindowState| {
|
||||
if matches!(state.positioning, Positioning::Floating) {
|
||||
state.saved_transform = None;
|
||||
}
|
||||
};
|
||||
|
||||
for set in &mut app.session.config.sets {
|
||||
for state in set
|
||||
.overlays
|
||||
.values_mut()
|
||||
.chain(set.hidden_overlays.values_mut())
|
||||
{
|
||||
clear_legacy_floating(state);
|
||||
}
|
||||
}
|
||||
for state in app.session.config.global_set.values_mut() {
|
||||
clear_legacy_floating(state);
|
||||
}
|
||||
app.session.config.state_version = 1;
|
||||
}
|
||||
|
||||
// only safe to load when current_set is None
|
||||
if self.current_set.is_some() {
|
||||
self.switch_to_set(app, None, false);
|
||||
@@ -1189,13 +973,9 @@ impl<T> OverlayWindowManager<T> {
|
||||
overlay.config.name
|
||||
);
|
||||
overlay.config.name = format!("{}_2", overlay.config.name).into();
|
||||
if overlay.config.category == OverlayCategory::WayVR {
|
||||
overlay.config.persistent = false;
|
||||
}
|
||||
}
|
||||
|
||||
let name = overlay.config.name.clone();
|
||||
let restore = overlay.config.can_persist() && overlay.config.restore_on_spawn;
|
||||
let global = overlay.config.global;
|
||||
let internal = matches!(overlay.config.category, OverlayCategory::Internal);
|
||||
let show_on_spawn = overlay.config.show_on_spawn;
|
||||
@@ -1205,14 +985,6 @@ impl<T> OverlayWindowManager<T> {
|
||||
|
||||
if !global {
|
||||
for (i, set) in self.sets.iter_mut().enumerate() {
|
||||
if !restore {
|
||||
// Explicitly new session window: replace any old slot instead of
|
||||
// accidentally restoring it on a later relaunch.
|
||||
if self.overlays[oid].config.can_persist() {
|
||||
set.inactive_overlays.arc_rm(&name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let Some(state) = set.inactive_overlays.arc_rm(&name) else {
|
||||
continue;
|
||||
};
|
||||
@@ -1227,7 +999,7 @@ impl<T> OverlayWindowManager<T> {
|
||||
log::debug!("loaded state for {name} to set {i}");
|
||||
}
|
||||
}
|
||||
} else if restore && let Some(state) = self.global_set.inactive_overlays.arc_rm(&name) {
|
||||
} else if let Some(state) = self.global_set.inactive_overlays.arc_rm(&name) {
|
||||
let o = &mut self.overlays[oid];
|
||||
o.config.active_state = Some(state);
|
||||
o.config.reset(app, false);
|
||||
@@ -1235,11 +1007,7 @@ impl<T> OverlayWindowManager<T> {
|
||||
log::debug!("loaded state for {name} from global_set!");
|
||||
}
|
||||
|
||||
let saved_attribs = if restore {
|
||||
app.session.config.attribs.arc_get(&name).cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let saved_attribs = app.session.config.attribs.arc_get(&name).cloned();
|
||||
if let Some(attribs) = saved_attribs {
|
||||
for value in attribs {
|
||||
self.overlays[oid].config.backend.set_attrib(app, value);
|
||||
@@ -1281,38 +1049,6 @@ impl<T> OverlayWindowManager<T> {
|
||||
SpawnPos::Parent(parent) => {
|
||||
let _ = self.offset_spawn_from_parent(app, oid, &parent);
|
||||
}
|
||||
SpawnPos::WlDefault(window, fallback_pid) => {
|
||||
let Some((parent, has_app_id)) = app.wvr_server.as_ref().and_then(|wvr| {
|
||||
wvr.wm.windows.get(window).map(|w| {
|
||||
(
|
||||
w.parent.and_then(|p| wvr.get_overlay_id(p)),
|
||||
w.app_id.is_some(),
|
||||
)
|
||||
})
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let parent_id = parent.or_else(|| {
|
||||
if has_app_id {
|
||||
None
|
||||
} else {
|
||||
// legacy pid-based matching
|
||||
fallback_pid.filter(|candidate| *candidate != oid)
|
||||
}
|
||||
});
|
||||
if let Some(parent_id) = parent_id
|
||||
&& self
|
||||
.offset_spawn_from_parent(app, oid, &OverlaySelector::Id(parent_id))
|
||||
.is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Some(parent_id) = self.spread_parent_for(oid) {
|
||||
let _ =
|
||||
self.offset_spawn_from_parent(app, oid, &OverlaySelector::Id(parent_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1477,7 +1213,6 @@ impl<T> OverlayWindowManager<T> {
|
||||
meta.push(OverlayMeta {
|
||||
id,
|
||||
name: data.config.name.clone(),
|
||||
title: data.config.title.clone(),
|
||||
category: data.config.category,
|
||||
visible: data.config.is_active(),
|
||||
icon,
|
||||
|
||||
@@ -108,10 +108,6 @@ impl OverlayLifetime {
|
||||
|
||||
pub struct OverlayWindowConfig {
|
||||
pub name: Arc<str>,
|
||||
/// Optional presentation title
|
||||
pub title: Option<Arc<str>>,
|
||||
pub persistent: bool,
|
||||
pub restore_on_spawn: bool,
|
||||
pub backend: Box<dyn OverlayBackend>,
|
||||
/// The default state to show when the overlay is newly spawned.
|
||||
pub default_state: OverlayWindowState,
|
||||
@@ -140,16 +136,9 @@ pub struct OverlayWindowConfig {
|
||||
}
|
||||
|
||||
impl OverlayWindowConfig {
|
||||
pub const fn can_persist(&self) -> bool {
|
||||
self.persistent
|
||||
}
|
||||
|
||||
pub fn from_backend(backend: Box<dyn OverlayBackend>) -> Self {
|
||||
Self {
|
||||
name: "".into(),
|
||||
title: None,
|
||||
persistent: true,
|
||||
restore_on_spawn: true,
|
||||
backend,
|
||||
default_state: OverlayWindowState {
|
||||
transform: Affine3A::from_translation(Vec3::NEG_Z),
|
||||
@@ -286,8 +275,8 @@ impl OverlayWindowConfig {
|
||||
app.input_state.pointers[hand as usize].pose * cur_transform,
|
||||
lerp,
|
||||
),
|
||||
Positioning::Anchored | Positioning::Floating => (app.anchor * cur_transform, 1.0),
|
||||
Positioning::Static => (state.transform, 1.0), // STAGE space
|
||||
Positioning::Anchored => (app.anchor * cur_transform, 1.0),
|
||||
Positioning::Static | Positioning::Floating => (state.transform, 1.0), // STAGE space
|
||||
};
|
||||
|
||||
state.transform = match lerp {
|
||||
@@ -358,9 +347,9 @@ impl OverlayWindowConfig {
|
||||
.unwrap_or(self.default_state.transform);
|
||||
|
||||
let parent_transform = match state.positioning {
|
||||
Positioning::FollowHead { .. } => app.input_state.hmd,
|
||||
Positioning::Floating | Positioning::FollowHead { .. } => app.input_state.hmd,
|
||||
Positioning::FollowHand { hand, .. } => app.input_state.pointers[hand as usize].pose,
|
||||
Positioning::Anchored | Positioning::Floating => app.anchor,
|
||||
Positioning::Anchored => app.anchor,
|
||||
Positioning::Static => {
|
||||
if hard_reset {
|
||||
app.input_state.hmd
|
||||
@@ -448,7 +437,7 @@ pub fn scalar_scale(a: &Affine3A) -> f32 {
|
||||
|
||||
pub fn save_transform(state: &mut OverlayWindowState, app: &mut AppState) {
|
||||
let parent_transform = match state.positioning {
|
||||
Positioning::Floating => app.anchor,
|
||||
Positioning::Floating => snap_upright(app.input_state.hmd, Vec3A::Y),
|
||||
Positioning::FollowHead { .. } => app.input_state.hmd,
|
||||
Positioning::FollowHand { hand, .. } => app.input_state.pointers[hand as usize].pose,
|
||||
Positioning::Anchored => snap_upright(app.anchor, Vec3A::Y),
|
||||
|
||||
+11
-6
@@ -24,12 +24,17 @@ raw-window-handle = "0.6"
|
||||
cosmic-text = "0.19.0"
|
||||
etagere = "0.3.0"
|
||||
flate2 = "1.1.9"
|
||||
image = { workspace = true, features = [
|
||||
"gif",
|
||||
"jpeg",
|
||||
"rayon",
|
||||
"webp",
|
||||
] }
|
||||
image = {
|
||||
version = "0.25.10",
|
||||
default-features = false,
|
||||
features = [
|
||||
"gif",
|
||||
"jpeg",
|
||||
"png",
|
||||
"rayon",
|
||||
"webp",
|
||||
]
|
||||
}
|
||||
lru = "0.18.1"
|
||||
ouroboros = "0.18.5"
|
||||
parking_lot = "0.12.5"
|
||||
|
||||
+1
-1
@@ -218,7 +218,7 @@ impl WGfx {
|
||||
|
||||
/// Submit a non-blocking completion marker to the queue
|
||||
///
|
||||
/// It uses an `ALL_COMMANDS` to `ALL_COMMANDS` barrier, so its fence doesn't
|
||||
/// It uses an ALL_COMMANDS to ALL_COMMANDS barrier, so its fence doesn't
|
||||
/// complete until all earlier work is finished.
|
||||
pub fn submit_graphics_completion_marker(&self) -> anyhow::Result<GpuCompletionMarker> {
|
||||
Ok(GpuCompletionMarker {
|
||||
|
||||
@@ -16,6 +16,7 @@ idmap = { workspace = true }
|
||||
libc.workspace = true
|
||||
log = { workspace = true }
|
||||
pipewire = { workspace = true, optional = true }
|
||||
timeline_syncobj = { git = "https://github.com/Schmarni-Dev/timeline_syncobj", rev = "7f67e49ebf466487cf35138fc108618092acb9e8", optional = true }
|
||||
slotmap.workspace = true
|
||||
wayland-client = { workspace = true, optional = true }
|
||||
wayland-protocols = {
|
||||
@@ -46,7 +47,7 @@ smithay-client-toolkit = { version = "0.20.0", optional = true }
|
||||
[features]
|
||||
default = ["pipewire", "wayland"]
|
||||
egl = []
|
||||
pipewire = ["dep:dbus", "dep:pipewire", "wayland"]
|
||||
pipewire = ["dep:dbus", "dep:pipewire", "dep:timeline_syncobj", "wayland"]
|
||||
wayland = [
|
||||
"dep:smithay-client-toolkit",
|
||||
"dep:wayland-client",
|
||||
|
||||
@@ -30,17 +30,6 @@ let capture = PipewireCapture::new(
|
||||
);
|
||||
```
|
||||
|
||||
### Wlr-Dmabuf Setup
|
||||
```rust
|
||||
let wl = WlxClient::new();
|
||||
|
||||
// select desired screen
|
||||
let output_id = wl.outputs.keys().first().unwrap();
|
||||
|
||||
let mut capture = WlrDmabufCapture::new(wl, *output_id).unwrap();
|
||||
```
|
||||
|
||||
|
||||
### XSHM Setup
|
||||
```rust
|
||||
let monitors = XshmCapture::get_monitors();
|
||||
|
||||
+110
-1
@@ -1,4 +1,10 @@
|
||||
use std::os::fd::RawFd;
|
||||
use std::{
|
||||
os::fd::{AsRawFd, OwnedFd, RawFd},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[cfg(feature = "pipewire")]
|
||||
use timeline_syncobj::timeline_syncobj::TimelineSyncObj;
|
||||
|
||||
use drm_fourcc::{DrmFormat, DrmFourcc, DrmModifier};
|
||||
|
||||
@@ -69,6 +75,8 @@ pub struct DmabufFrame {
|
||||
pub num_planes: usize,
|
||||
pub planes: [FramePlane; 4],
|
||||
pub mouse: Option<MouseMeta>,
|
||||
pub acquire_sync: FrameAcquireSync,
|
||||
pub lease: Option<FrameLease>,
|
||||
}
|
||||
|
||||
impl DmabufFrame {
|
||||
@@ -139,6 +147,107 @@ pub struct MouseMeta {
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FrameLease(Arc<FrameLeaseInner>);
|
||||
|
||||
struct FrameLeaseInner(Box<dyn Fn() + Send + Sync>);
|
||||
|
||||
impl FrameLease {
|
||||
pub fn new(f: impl Fn() + Send + Sync + 'static) -> Self {
|
||||
Self(Arc::new(FrameLeaseInner(Box::new(f))))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FrameLeaseInner {
|
||||
fn drop(&mut self) {
|
||||
(self.0)();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub enum FrameAcquireSync {
|
||||
#[default]
|
||||
/// Frame is ready, no sync needed
|
||||
Ready,
|
||||
/// Implicit sync using polled FDs
|
||||
Implicit { fds: Arc<[OwnedFd]> },
|
||||
#[cfg(feature = "pipewire")]
|
||||
/// Explicit sync using DRM SyncObj
|
||||
Explicit {
|
||||
timeline: Arc<TimelineSyncObj>,
|
||||
point: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl FrameAcquireSync {
|
||||
pub fn check_ready(&mut self) -> std::io::Result<bool> {
|
||||
match self {
|
||||
FrameAcquireSync::Ready => Ok(true),
|
||||
FrameAcquireSync::Implicit { fds } => {
|
||||
if fds.is_empty() {
|
||||
return Err(std::io::Error::other("implicit DMA-buf without planes"));
|
||||
}
|
||||
for plane_fd in fds.iter() {
|
||||
let fd = plane_fd.as_raw_fd();
|
||||
let mut pfd = libc::pollfd {
|
||||
fd,
|
||||
events: libc::POLLIN,
|
||||
revents: 0,
|
||||
};
|
||||
let result = unsafe { libc::poll(&mut pfd, 1, 0) };
|
||||
if result < 0 {
|
||||
let error = std::io::Error::last_os_error();
|
||||
if error.kind() == std::io::ErrorKind::Interrupted {
|
||||
return Ok(false);
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
if result == 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
if pfd.revents & (libc::POLLNVAL | libc::POLLHUP) != 0 {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"DMA-buf plane fd {fd} poll returned with {:#x}",
|
||||
pfd.revents
|
||||
)));
|
||||
}
|
||||
if pfd.revents & libc::POLLERR != 0 {
|
||||
static WARNED: std::sync::atomic::AtomicBool =
|
||||
std::sync::atomic::AtomicBool::new(false);
|
||||
if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
|
||||
log::error!(
|
||||
"Could not poll implicit acquire fence. Falling back to unsynchronized behavior. Visual artifacts may happen."
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if pfd.revents & libc::POLLIN == 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
*self = FrameAcquireSync::Ready;
|
||||
Ok(true)
|
||||
}
|
||||
#[cfg(feature = "pipewire")]
|
||||
FrameAcquireSync::Explicit { timeline, point } => {
|
||||
if *point == 0 {
|
||||
return Ok(true);
|
||||
}
|
||||
if timeline
|
||||
.query_signaled_point()
|
||||
.map(|signaled| signaled >= *point)
|
||||
.map_err(|e| std::io::Error::from_raw_os_error(e.raw_os_error()))?
|
||||
{
|
||||
*self = FrameAcquireSync::Ready;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DmaExporter {
|
||||
fn next_frame(
|
||||
&mut self,
|
||||
|
||||
@@ -6,12 +6,12 @@ use frame::WlxFrame;
|
||||
|
||||
pub mod frame;
|
||||
|
||||
#[cfg(feature = "pipewire")]
|
||||
pub mod syncobj;
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
pub mod wayland;
|
||||
|
||||
#[cfg(feature = "wlr")]
|
||||
pub mod wlr_dmabuf;
|
||||
|
||||
#[cfg(feature = "wlr")]
|
||||
pub mod wlr_screencopy;
|
||||
|
||||
|
||||
+346
-163
@@ -1,5 +1,9 @@
|
||||
use std::any::Any;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::os::fd::{FromRawFd, OwnedFd, RawFd};
|
||||
use std::ptr::NonNull;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc;
|
||||
use std::thread::JoinHandle;
|
||||
@@ -10,8 +14,9 @@ use drm_fourcc::DrmModifier;
|
||||
use pipewire as pw;
|
||||
use pipewire::context::ContextRc;
|
||||
use pipewire::main_loop::MainLoopRc;
|
||||
use pipewire::spa::buffer::meta::MetaVideoTransformValue;
|
||||
use pipewire::spa::buffer::meta::{MetaCursor, MetaHeader, MetaHeaderFlags, MetaVideoTransform};
|
||||
use pipewire::spa::buffer::meta::{
|
||||
MetaCursor, MetaHeader, MetaHeaderFlags, MetaVideoTransform, MetaVideoTransformValue,
|
||||
};
|
||||
use pipewire::stream::StreamRc;
|
||||
use pw::spa;
|
||||
|
||||
@@ -21,7 +26,7 @@ use pw::stream::{Stream, StreamFlags};
|
||||
use spa::buffer::DataType;
|
||||
use spa::param::ParamType;
|
||||
use spa::param::video::VideoFormat;
|
||||
use spa::param::video::VideoInfoRaw;
|
||||
use spa::param::video::{VideoFlags, VideoInfoRaw};
|
||||
use spa::pod::ChoiceValue;
|
||||
use spa::pod::Pod;
|
||||
use spa::pod::serialize::GenError;
|
||||
@@ -29,13 +34,24 @@ use spa::pod::{Object, Property, PropertyFlags, Value};
|
||||
use spa::utils::Choice;
|
||||
use spa::utils::ChoiceEnum;
|
||||
use spa::utils::ChoiceFlags;
|
||||
use timeline_syncobj::render_node::DrmRenderNode;
|
||||
use wayland_client::backend::smallvec::SmallVec;
|
||||
|
||||
use super::raw_buffer::DequeuedBuffer;
|
||||
use crate::WlxCapture;
|
||||
use crate::frame::FrameFormat;
|
||||
use crate::frame::MouseMeta;
|
||||
use crate::frame::Transform;
|
||||
use crate::frame::WlxFrame;
|
||||
use crate::frame::{DmabufFrame, FramePlane, MemFdFrame, MemPtrFrame};
|
||||
use crate::frame::{
|
||||
DmabufFrame, FrameAcquireSync, FrameLease, FramePlane, MemFdFrame, MemPtrFrame,
|
||||
};
|
||||
use crate::pipewire::raw_buffer::{HeldBuffers, MAX_PW_BUFFERS};
|
||||
use crate::syncobj::import_timeline;
|
||||
|
||||
const SPA_DATA_SYNCOBJ: u32 = 5;
|
||||
const SPA_META_SYNC_TIMELINE: u32 = 9;
|
||||
const SPA_PARAM_BUFFERS_META_TYPE: u32 = 7;
|
||||
|
||||
#[derive(Default)]
|
||||
struct StreamData {
|
||||
@@ -48,6 +64,7 @@ pub enum PwChangeRequest {
|
||||
Pause,
|
||||
Resume,
|
||||
Stop,
|
||||
ReleaseBuffer(usize),
|
||||
}
|
||||
|
||||
struct CaptureData<R>
|
||||
@@ -66,18 +83,20 @@ where
|
||||
data: Option<CaptureData<R>>,
|
||||
node_id: u32,
|
||||
handle: Option<JoinHandle<Result<(), Error>>>,
|
||||
drm_render_node: Option<DrmRenderNode>,
|
||||
}
|
||||
|
||||
impl<R> PipewireCapture<R>
|
||||
where
|
||||
R: Any + Send,
|
||||
{
|
||||
pub fn new(name: Arc<str>, node_id: u32) -> Self {
|
||||
pub fn new(name: Arc<str>, node_id: u32, drm_render_node: Option<DrmRenderNode>) -> Self {
|
||||
PipewireCapture {
|
||||
name,
|
||||
data: None,
|
||||
node_id,
|
||||
handle: None,
|
||||
drm_render_node,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +106,8 @@ where
|
||||
R: Any + Send,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
if let Some(data) = &self.data {
|
||||
if let Some(data) = self.data.take() {
|
||||
drop(data.rx_frame);
|
||||
let _ = data.tx_ctrl.send(PwChangeRequest::Stop);
|
||||
}
|
||||
if let Some(handle) = self.handle.take() {
|
||||
@@ -107,10 +127,15 @@ where
|
||||
user_data: U,
|
||||
receive_callback: fn(&U, WlxFrame) -> Option<R>,
|
||||
) {
|
||||
let (tx_frame, rx_frame) = mpsc::sync_channel(2);
|
||||
let (tx_frame, rx_frame) = mpsc::sync_channel(4);
|
||||
let (tx_ctrl, rx_ctrl) = pw::channel::channel();
|
||||
|
||||
self.data = Some(CaptureData { tx_ctrl, rx_frame });
|
||||
self.data = Some(CaptureData {
|
||||
tx_ctrl: tx_ctrl.clone(),
|
||||
rx_frame,
|
||||
});
|
||||
|
||||
let drm_render_node = self.drm_render_node.clone();
|
||||
|
||||
self.handle = Some(std::thread::spawn({
|
||||
let name = self.name.clone();
|
||||
@@ -123,9 +148,11 @@ where
|
||||
node_id,
|
||||
formats,
|
||||
tx_frame,
|
||||
tx_ctrl,
|
||||
rx_ctrl,
|
||||
user_data,
|
||||
receive_callback,
|
||||
drm_render_node,
|
||||
)
|
||||
}
|
||||
}));
|
||||
@@ -175,10 +202,12 @@ fn main_loop<U, R>(
|
||||
name: Arc<str>,
|
||||
node_id: u32,
|
||||
dmabuf_formats: Vec<DrmFormat>,
|
||||
sender: mpsc::SyncSender<R>,
|
||||
receiver: pw::channel::Receiver<PwChangeRequest>,
|
||||
tx_frame: mpsc::SyncSender<R>,
|
||||
tx_ctrl: pw::channel::Sender<PwChangeRequest>,
|
||||
rx_ctrl: pw::channel::Receiver<PwChangeRequest>,
|
||||
user_data: U,
|
||||
receive_callback: fn(&U, WlxFrame) -> Option<R>,
|
||||
drm_render_node: Option<DrmRenderNode>,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
U: Any,
|
||||
@@ -189,6 +218,8 @@ where
|
||||
let context = ContextRc::new(&main_loop, None)?;
|
||||
let core = context.connect_rc(None)?;
|
||||
|
||||
let held_buffers = Rc::new(RefCell::new(HeldBuffers::default()));
|
||||
|
||||
let stream = StreamRc::new(
|
||||
core,
|
||||
&name,
|
||||
@@ -217,6 +248,7 @@ where
|
||||
})
|
||||
.param_changed({
|
||||
let name = name.clone();
|
||||
let support_explicit_sync = drm_render_node.is_some();
|
||||
move |stream, format, id, param| {
|
||||
let Some(param) = param else {
|
||||
return;
|
||||
@@ -234,186 +266,278 @@ where
|
||||
format.drm_format.code = spa_to_fourcc(info.format());
|
||||
format.drm_format.modifier = DrmModifier::from(info.modifier());
|
||||
|
||||
let kind = if info.modifier() != 0 {
|
||||
"DMA-buf"
|
||||
} else {
|
||||
"SHM"
|
||||
};
|
||||
let dmabuf = info.flags().contains(VideoFlags::MODIFIER);
|
||||
let kind = if dmabuf { "DMA-buf" } else { "SHM" };
|
||||
|
||||
log::info!("{}: got {} video format:", name, kind);
|
||||
log::info!(" format: {} ({:?})", info.format().as_raw(), info.format());
|
||||
log::info!(" size: {}x{}", info.size().width, info.size().height);
|
||||
log::info!(" modifier: {}", info.modifier());
|
||||
let Ok(params_bytes) = obj_to_bytes(get_buffer_params()) else {
|
||||
log::warn!("{}: failed to serialize buffer params", name);
|
||||
return;
|
||||
};
|
||||
let Some(params_pod) = Pod::from_bytes(¶ms_bytes) else {
|
||||
log::warn!("{}: failed to deserialize buffer params", name);
|
||||
return;
|
||||
};
|
||||
|
||||
let header_bytes = obj_to_bytes(get_meta_object(
|
||||
spa::sys::SPA_META_Header,
|
||||
std::mem::size_of::<spa::sys::spa_meta_header>(),
|
||||
))
|
||||
.unwrap(); // want panic
|
||||
let header_pod = Pod::from_bytes(&header_bytes).unwrap(); // want panic
|
||||
let mut param_bytes = Vec::new();
|
||||
if dmabuf && support_explicit_sync {
|
||||
param_bytes.push(obj_to_bytes(get_sync_buffer_params()).unwrap());
|
||||
// order defines preference, we prefer explicit sync
|
||||
param_bytes.push(
|
||||
obj_to_bytes(get_meta_object(
|
||||
SPA_META_SYNC_TIMELINE,
|
||||
std::mem::size_of::<MetaSyncTimeline>(),
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
param_bytes.push(obj_to_bytes(get_buffer_params()).unwrap());
|
||||
param_bytes.push(
|
||||
obj_to_bytes(get_meta_object(
|
||||
spa::sys::SPA_META_Header,
|
||||
std::mem::size_of::<spa::sys::spa_meta_header>(),
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
param_bytes.push(
|
||||
obj_to_bytes(get_meta_object(
|
||||
spa::sys::SPA_META_VideoTransform,
|
||||
std::mem::size_of::<spa::sys::spa_meta_videotransform>(),
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let xform_bytes = obj_to_bytes(get_meta_object(
|
||||
spa::sys::SPA_META_VideoTransform,
|
||||
std::mem::size_of::<spa::sys::spa_meta_videotransform>(),
|
||||
))
|
||||
.unwrap(); // want panic
|
||||
let xform_pod = Pod::from_bytes(&xform_bytes).unwrap(); // want panic
|
||||
|
||||
let mut pods = [params_pod, header_pod, xform_pod];
|
||||
let mut pods: Vec<&Pod> = param_bytes
|
||||
.iter()
|
||||
.map(|bytes| Pod::from_bytes(bytes).unwrap())
|
||||
.collect();
|
||||
if let Err(e) = stream.update_params(&mut pods) {
|
||||
log::error!("{}: failed to update params: {}", name, e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.remove_buffer({
|
||||
let held_buffers = held_buffers.clone();
|
||||
let name = name.clone();
|
||||
move |_, _, ptr| {
|
||||
if let Some(buffer) = NonNull::new(ptr)
|
||||
&& held_buffers.borrow_mut().on_removed(buffer)
|
||||
{
|
||||
log::warn!("{name}: PipeWire removed a still-leased buffer");
|
||||
}
|
||||
}
|
||||
})
|
||||
.process({
|
||||
let name = name.clone();
|
||||
let sync_device = drm_render_node.clone();
|
||||
let held_buffers = held_buffers.clone();
|
||||
let tx_ctrl = tx_ctrl.clone();
|
||||
let u = user_data;
|
||||
move |stream, format| {
|
||||
let mut maybe_buffer = None;
|
||||
// discard all but the newest frame
|
||||
while let Some(buffer) = stream.dequeue_buffer() {
|
||||
maybe_buffer = Some(buffer);
|
||||
let mut new_buffer = None;
|
||||
while let Some(buffer) = DequeuedBuffer::dequeue(stream) {
|
||||
new_buffer = Some(buffer);
|
||||
}
|
||||
let Some(buffer) = new_buffer else { return };
|
||||
|
||||
if let Some(header) = buffer.find_meta::<MetaHeader>()
|
||||
&& header.flags().contains(MetaHeaderFlags::CORRUPTED)
|
||||
{
|
||||
log::warn!("{name}: PipeWire buffer is corrupt");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(mut buffer) = maybe_buffer {
|
||||
if let Some(header) = buffer.find_meta::<MetaHeader>()
|
||||
&& header.flags().contains(MetaHeaderFlags::CORRUPTED)
|
||||
{
|
||||
log::warn!("{}: PipeWire buffer is corrupt.", name);
|
||||
return;
|
||||
}
|
||||
if let Some(transform) = buffer.find_meta::<MetaVideoTransform>() {
|
||||
format.transform = match transform.transform() {
|
||||
MetaVideoTransformValue::NONE => Transform::Normal,
|
||||
MetaVideoTransformValue::ROTATED90 => Transform::Rotated90,
|
||||
MetaVideoTransformValue::ROTATED180 => Transform::Rotated180,
|
||||
MetaVideoTransformValue::ROTATED270 => Transform::Rotated270,
|
||||
MetaVideoTransformValue::FLIPPED => Transform::Flipped,
|
||||
MetaVideoTransformValue::FLIPPED90 => Transform::Flipped90,
|
||||
MetaVideoTransformValue::FLIPPED180 => Transform::Flipped180,
|
||||
MetaVideoTransformValue::FLIPPED270 => Transform::Flipped270,
|
||||
_ => Transform::Undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(transform) = buffer.find_meta::<MetaVideoTransform>() {
|
||||
format.transform = match transform.transform() {
|
||||
MetaVideoTransformValue::NONE => Transform::Normal,
|
||||
MetaVideoTransformValue::ROTATED90 => Transform::Rotated90,
|
||||
MetaVideoTransformValue::ROTATED180 => Transform::Rotated180,
|
||||
MetaVideoTransformValue::ROTATED270 => Transform::Rotated270,
|
||||
MetaVideoTransformValue::FLIPPED => Transform::Flipped,
|
||||
MetaVideoTransformValue::FLIPPED90 => Transform::Flipped90,
|
||||
MetaVideoTransformValue::FLIPPED180 => Transform::Flipped180,
|
||||
MetaVideoTransformValue::FLIPPED270 => Transform::Flipped270,
|
||||
_ => Transform::Undefined,
|
||||
let mouse_meta = buffer
|
||||
.find_meta::<MetaCursor>()
|
||||
.filter(|c| c.is_valid())
|
||||
.map(|c| MouseMeta {
|
||||
x: c.position().x as f32 / format.width as f32,
|
||||
y: c.position().y as f32 / format.height as f32,
|
||||
});
|
||||
|
||||
let acquire_point = buffer
|
||||
.find_meta::<MetaSyncTimeline>()
|
||||
.map(|meta| meta.acquire_point);
|
||||
|
||||
let datas = buffer.datas_mut();
|
||||
if datas.is_empty() {
|
||||
log::debug!("{name}: empty PipeWire frame");
|
||||
return;
|
||||
}
|
||||
let is_explicit = datas.len() >= 3
|
||||
&& datas[datas.len() - 2].type_().as_raw() == SPA_DATA_SYNCOBJ
|
||||
&& datas[datas.len() - 1].type_().as_raw() == SPA_DATA_SYNCOBJ;
|
||||
if is_explicit != acquire_point.is_some() {
|
||||
log::warn!("{name}: inconsistent SyncTimeline metadata / SyncObj FD layout");
|
||||
return;
|
||||
}
|
||||
let acquire_fd = is_explicit.then(|| datas[datas.len() - 2].fd());
|
||||
let datas = if is_explicit {
|
||||
&datas[..datas.len() - 2]
|
||||
} else {
|
||||
datas
|
||||
};
|
||||
if datas.is_empty()
|
||||
|| datas.len() > 4
|
||||
|| datas
|
||||
.iter()
|
||||
.any(|d| d.type_().as_raw() == SPA_DATA_SYNCOBJ)
|
||||
{
|
||||
log::warn!("{name}: invalid PipeWire plane layout");
|
||||
return;
|
||||
}
|
||||
let data_type = datas[0].type_();
|
||||
if datas.iter().any(|d| d.type_() != data_type) {
|
||||
log::warn!("{name}: mixed PipeWire data types");
|
||||
return;
|
||||
}
|
||||
|
||||
match datas[0].type_() {
|
||||
DataType::DmaBuf => {
|
||||
let mut planes: SmallVec<[FramePlane; 4]> = datas
|
||||
.iter()
|
||||
.map(|p| FramePlane {
|
||||
fd: Some(p.as_raw().fd as _),
|
||||
offset: p.chunk().offset(),
|
||||
stride: p.chunk().stride(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let acquire_sync = if is_explicit {
|
||||
let Some(device) = sync_device.as_ref() else {
|
||||
log::error!("{name}: explicit sync without DRM timeline support");
|
||||
return;
|
||||
};
|
||||
let acquire_point = acquire_point.unwrap();
|
||||
let fd = acquire_fd.unwrap();
|
||||
let acquire = match import_timeline(device, fd as RawFd) {
|
||||
Ok(obj) => obj,
|
||||
Err(e) => {
|
||||
log::error!("{name}: acquire SyncObj import failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
FrameAcquireSync::Explicit {
|
||||
timeline: acquire,
|
||||
point: acquire_point,
|
||||
}
|
||||
} else {
|
||||
let mut fds = Vec::with_capacity(planes.len());
|
||||
for (index, plane) in planes.iter_mut().enumerate() {
|
||||
let fd = plane.fd.unwrap(); // always Some
|
||||
// duplicate the fd so that we can poll it for acquire sync
|
||||
let duplicate = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) };
|
||||
if duplicate < 0 {
|
||||
log::error!(
|
||||
"{name}: failed to duplicate DMA-buf plane {index} FD {fd}: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
return;
|
||||
}
|
||||
fds.push(unsafe { OwnedFd::from_raw_fd(duplicate) });
|
||||
plane.fd = Some(duplicate);
|
||||
}
|
||||
FrameAcquireSync::Implicit { fds: fds.into() }
|
||||
};
|
||||
log::debug!("{}: Transform: {:?}", name, format.transform);
|
||||
}
|
||||
|
||||
let mouse_meta = buffer
|
||||
.find_meta::<MetaCursor>()
|
||||
.filter(|c| c.is_valid())
|
||||
.map(|c| MouseMeta {
|
||||
x: c.position().x as f32 / format.width as f32,
|
||||
y: c.position().y as f32 / format.height as f32,
|
||||
let lease_id = held_buffers.borrow_mut().insert(buffer);
|
||||
let tx = tx_ctrl.clone();
|
||||
let lease = FrameLease::new(move || {
|
||||
let _ = tx.send(PwChangeRequest::ReleaseBuffer(lease_id));
|
||||
});
|
||||
|
||||
let datas = buffer.datas_mut();
|
||||
if datas.is_empty() {
|
||||
log::debug!("{}: no data", name);
|
||||
return;
|
||||
}
|
||||
let mut dmabuf = DmabufFrame {
|
||||
format: *format,
|
||||
num_planes: planes.len(),
|
||||
planes: Default::default(),
|
||||
mouse: mouse_meta,
|
||||
acquire_sync,
|
||||
lease: Some(lease),
|
||||
};
|
||||
|
||||
let planes: Vec<FramePlane> = datas
|
||||
.iter()
|
||||
.map(|p| FramePlane {
|
||||
fd: Some(p.as_raw().fd as _),
|
||||
offset: p.chunk().offset(),
|
||||
stride: p.chunk().stride(),
|
||||
})
|
||||
.collect();
|
||||
dmabuf.planes[..planes.len()].copy_from_slice(&planes[..planes.len()]);
|
||||
|
||||
match datas[0].type_() {
|
||||
DataType::DmaBuf => {
|
||||
let mut dmabuf = DmabufFrame {
|
||||
format: *format,
|
||||
num_planes: planes.len(),
|
||||
mouse: mouse_meta,
|
||||
planes: Default::default(),
|
||||
};
|
||||
dmabuf.planes[..planes.len()].copy_from_slice(&planes[..planes.len()]);
|
||||
|
||||
let frame = WlxFrame::Dmabuf(dmabuf);
|
||||
|
||||
if let Some(r) = receive_callback(&u, frame) {
|
||||
match sender.try_send(r) {
|
||||
Ok(_) => (),
|
||||
Err(mpsc::TrySendError::Full(_)) => (),
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => {
|
||||
log::warn!("{}: disconnected, stopping stream", name);
|
||||
let _ = stream.disconnect();
|
||||
}
|
||||
if let Some(result) = receive_callback(&u, WlxFrame::Dmabuf(dmabuf)) {
|
||||
match tx_frame.try_send(result) {
|
||||
Ok(()) | Err(mpsc::TrySendError::Full(_)) => (),
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => {
|
||||
log::warn!("{name}: frame receiver disconnected");
|
||||
let _ = stream.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
DataType::MemFd => {
|
||||
let memfd = MemFdFrame {
|
||||
format: *format,
|
||||
plane: FramePlane {
|
||||
fd: Some(datas[0].as_raw().fd as _),
|
||||
offset: datas[0].chunk().offset(),
|
||||
stride: datas[0].chunk().stride(),
|
||||
},
|
||||
mouse: mouse_meta,
|
||||
};
|
||||
|
||||
let frame = WlxFrame::MemFd(memfd);
|
||||
if let Some(r) = receive_callback(&u, frame) {
|
||||
match sender.try_send(r) {
|
||||
Ok(_) => (),
|
||||
Err(mpsc::TrySendError::Full(_)) => (),
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => {
|
||||
log::warn!("{}: disconnected, stopping stream", name);
|
||||
let _ = stream.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DataType::MemPtr => {
|
||||
let raw = datas[0].as_raw();
|
||||
let chunk = datas[0].chunk();
|
||||
let offset = chunk.offset() as usize;
|
||||
let chunk_size = chunk.size() as usize;
|
||||
let Some(size) = offset.checked_add(chunk_size) else {
|
||||
log::error!("{name}: PipeWire chunk range overflow");
|
||||
return;
|
||||
};
|
||||
if size > raw.maxsize as usize {
|
||||
log::error!("{name}: PipeWire chunk extends beyond allocation");
|
||||
return;
|
||||
}
|
||||
let memptr = MemPtrFrame {
|
||||
format: *format,
|
||||
ptr: raw.data as _,
|
||||
size,
|
||||
offset,
|
||||
stride: chunk.stride() as _,
|
||||
mouse: mouse_meta,
|
||||
};
|
||||
|
||||
let frame = WlxFrame::MemPtr(memptr);
|
||||
if let Some(r) = receive_callback(&u, frame) {
|
||||
match sender.try_send(r) {
|
||||
Ok(_) => (),
|
||||
Err(mpsc::TrySendError::Full(_)) => (),
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => {
|
||||
log::warn!("{}: disconnected, stopping stream", name);
|
||||
let _ = stream.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::error!("Received invalid frame data type ({:?})", datas[0].type_())
|
||||
}
|
||||
}
|
||||
DataType::MemFd => {
|
||||
let memfd = MemFdFrame {
|
||||
format: *format,
|
||||
plane: FramePlane {
|
||||
fd: Some(datas[0].as_raw().fd as _),
|
||||
offset: datas[0].chunk().offset(),
|
||||
stride: datas[0].chunk().stride(),
|
||||
},
|
||||
mouse: mouse_meta,
|
||||
};
|
||||
|
||||
let frame = WlxFrame::MemFd(memfd);
|
||||
if let Some(r) = receive_callback(&u, frame) {
|
||||
match tx_frame.try_send(r) {
|
||||
Ok(_) => (),
|
||||
Err(mpsc::TrySendError::Full(_)) => (),
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => {
|
||||
log::warn!("{}: disconnected, stopping stream", name);
|
||||
let _ = stream.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DataType::MemPtr => {
|
||||
let raw = datas[0].as_raw();
|
||||
let chunk = datas[0].chunk();
|
||||
let offset = chunk.offset() as usize;
|
||||
let chunk_size = chunk.size() as usize;
|
||||
let Some(size) = offset.checked_add(chunk_size) else {
|
||||
log::error!("{name}: PipeWire chunk range overflow");
|
||||
return;
|
||||
};
|
||||
if size > raw.maxsize as usize {
|
||||
log::error!("{name}: PipeWire chunk extends beyond allocation");
|
||||
return;
|
||||
}
|
||||
let memptr = MemPtrFrame {
|
||||
format: *format,
|
||||
ptr: raw.data as _,
|
||||
size,
|
||||
offset,
|
||||
stride: chunk.stride() as _,
|
||||
mouse: mouse_meta,
|
||||
};
|
||||
|
||||
let frame = WlxFrame::MemPtr(memptr);
|
||||
if let Some(r) = receive_callback(&u, frame) {
|
||||
match tx_frame.try_send(r) {
|
||||
Ok(_) => (),
|
||||
Err(mpsc::TrySendError::Full(_)) => (),
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => {
|
||||
log::warn!("{}: disconnected, stopping stream", name);
|
||||
let _ = stream.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => log::error!("Received invalid frame data type ({:?})", datas[0].type_()),
|
||||
}
|
||||
|
||||
// DMA-buf frames transfer ownership to held_buffers,
|
||||
// requeued when their lease expires
|
||||
}
|
||||
})
|
||||
.register()?;
|
||||
@@ -448,9 +572,10 @@ where
|
||||
params.as_mut_slice(),
|
||||
)?;
|
||||
|
||||
let _receiver = receiver.attach(main_loop.loop_(), {
|
||||
let _receiver = rx_ctrl.attach(main_loop.loop_(), {
|
||||
let name = name.clone();
|
||||
let main_loop = main_loop.clone();
|
||||
let held_buffers = held_buffers.clone();
|
||||
|
||||
move |req| {
|
||||
log::debug!("{name}: request pipewire stream to {req:?}");
|
||||
@@ -465,6 +590,9 @@ where
|
||||
.set_active(true)
|
||||
.inspect_err(|e| log::warn!("Could not {req:?} pipewire stream: {e:?}"));
|
||||
}
|
||||
PwChangeRequest::ReleaseBuffer(id) => {
|
||||
held_buffers.borrow_mut().requeue(&stream, id);
|
||||
}
|
||||
PwChangeRequest::Stop => {
|
||||
main_loop.quit();
|
||||
}
|
||||
@@ -491,7 +619,25 @@ fn get_buffer_params() -> Object {
|
||||
| (1 << DataType::MemPtr.as_raw())
|
||||
| (1 << DataType::DmaBuf.as_raw());
|
||||
|
||||
let property = Property {
|
||||
get_buffer_params_with_data_types(data_types)
|
||||
}
|
||||
|
||||
fn get_buffer_params_with_data_types(data_types: i32) -> Object {
|
||||
// since we hold onto buffers until rendering is done,
|
||||
// fewer than 4 buffers may produce stalls
|
||||
let buffers = Property {
|
||||
key: spa::sys::SPA_PARAM_BUFFERS_buffers,
|
||||
flags: PropertyFlags::empty(),
|
||||
value: Value::Choice(ChoiceValue::Int(Choice(
|
||||
ChoiceFlags::empty(),
|
||||
ChoiceEnum::Range {
|
||||
default: 6,
|
||||
min: 4,
|
||||
max: MAX_PW_BUFFERS,
|
||||
},
|
||||
))),
|
||||
};
|
||||
let data_types = Property {
|
||||
key: spa::sys::SPA_PARAM_BUFFERS_dataType,
|
||||
flags: PropertyFlags::empty(),
|
||||
value: Value::Int(data_types),
|
||||
@@ -500,10 +646,35 @@ fn get_buffer_params() -> Object {
|
||||
spa::pod::object!(
|
||||
spa::utils::SpaTypes::ObjectParamBuffers,
|
||||
spa::param::ParamType::Buffers,
|
||||
property,
|
||||
buffers,
|
||||
data_types,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_sync_buffer_params() -> Object {
|
||||
let mut obj = get_buffer_params_with_data_types(1 << DataType::DmaBuf.as_raw());
|
||||
obj.properties.push(Property {
|
||||
key: spa::sys::SPA_PARAM_BUFFERS_blocks,
|
||||
flags: PropertyFlags::empty(),
|
||||
value: Value::Choice(ChoiceValue::Int(Choice(
|
||||
ChoiceFlags::empty(),
|
||||
ChoiceEnum::Range {
|
||||
default: 3,
|
||||
// when using sync timeline, we want at least 1 image plane + 2 syncobj fds
|
||||
min: 3,
|
||||
// up to 4 image planes
|
||||
max: 6,
|
||||
},
|
||||
))),
|
||||
});
|
||||
obj.properties.push(Property {
|
||||
key: SPA_PARAM_BUFFERS_META_TYPE,
|
||||
flags: PropertyFlags::MANDATORY,
|
||||
value: Value::Int(1_i32 << SPA_META_SYNC_TIMELINE),
|
||||
});
|
||||
obj
|
||||
}
|
||||
|
||||
fn get_meta_object(key: u32, size: usize) -> Object {
|
||||
let meta_type_property = Property {
|
||||
key: spa::sys::SPA_PARAM_META_type,
|
||||
@@ -641,3 +812,15 @@ fn spa_to_fourcc(spa: VideoFormat) -> DrmFourcc {
|
||||
_ => panic!("Unsupported format"),
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct MetaSyncTimeline {
|
||||
_flags: u32,
|
||||
_padding: u32,
|
||||
acquire_point: u64,
|
||||
_release_point: u64,
|
||||
}
|
||||
|
||||
impl spa::buffer::meta::Metadata for MetaSyncTimeline {
|
||||
const META_TYPE: u32 = SPA_META_SYNC_TIMELINE;
|
||||
}
|
||||
@@ -26,6 +26,7 @@ slotmap::new_key_type! {
|
||||
|
||||
pub mod capture;
|
||||
mod dbus_screencast;
|
||||
mod raw_buffer;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PipewireStream {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
use std::{marker::PhantomData, mem, ptr::NonNull};
|
||||
|
||||
use pipewire::{
|
||||
self as pw,
|
||||
spa::{self, buffer::meta::Metadata},
|
||||
stream::Stream,
|
||||
};
|
||||
|
||||
pub(super) struct DequeuedBuffer<'a> {
|
||||
stream: &'a Stream,
|
||||
ptr: NonNull<pw::sys::pw_buffer>,
|
||||
_not_send: PhantomData<std::rc::Rc<()>>,
|
||||
}
|
||||
|
||||
impl<'a> DequeuedBuffer<'a> {
|
||||
pub(super) fn dequeue(stream: &'a Stream) -> Option<Self> {
|
||||
let ptr = NonNull::new(unsafe { stream.dequeue_raw_buffer() })?;
|
||||
Some(Self {
|
||||
stream,
|
||||
ptr,
|
||||
_not_send: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
fn spa(&self) -> Option<NonNull<spa::sys::spa_buffer>> {
|
||||
NonNull::new(unsafe { self.ptr.read().buffer })
|
||||
}
|
||||
|
||||
pub(super) fn datas_mut(&self) -> &'a mut [pw::spa::buffer::Data] {
|
||||
let Some(buffer) = self.spa() else {
|
||||
return &mut [];
|
||||
};
|
||||
unsafe {
|
||||
let n_datas = buffer.read().n_datas;
|
||||
if n_datas > 0
|
||||
&& let Some(datas) = NonNull::new(buffer.read().datas)
|
||||
{
|
||||
let datas = datas.as_ptr() as *mut pw::spa::buffer::Data;
|
||||
std::slice::from_raw_parts_mut(datas, usize::try_from(n_datas).unwrap())
|
||||
} else {
|
||||
&mut []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn find_meta<T>(&self) -> Option<&'a T>
|
||||
where
|
||||
T: Metadata,
|
||||
{
|
||||
let buffer = self.spa()?;
|
||||
unsafe {
|
||||
if buffer.read().n_metas != 0 {
|
||||
match T::META_TYPE {
|
||||
pw::spa::sys::SPA_META_VideoDamage => {
|
||||
let meta_data =
|
||||
pw::spa::sys::spa_buffer_find_meta(buffer.as_ptr(), T::META_TYPE)
|
||||
as *const T;
|
||||
if !meta_data.is_null() {
|
||||
return Some(&*meta_data);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let meta_data = pw::spa::sys::spa_buffer_find_meta_data(
|
||||
buffer.as_ptr(),
|
||||
T::META_TYPE,
|
||||
std::mem::size_of::<T>(),
|
||||
) as *const T;
|
||||
if !meta_data.is_null() {
|
||||
return Some(&*meta_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) const MAX_PW_BUFFERS: i32 = 8;
|
||||
|
||||
impl Drop for DequeuedBuffer<'_> {
|
||||
fn drop(&mut self) {
|
||||
unsafe { self.stream.queue_raw_buffer(self.ptr.as_ptr()) };
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct HeldBuffers {
|
||||
buffers: Vec<NonNull<pw::sys::pw_buffer>>,
|
||||
}
|
||||
|
||||
impl HeldBuffers {
|
||||
pub(super) fn insert<'a>(&mut self, buffer: DequeuedBuffer<'a>) -> usize {
|
||||
self.buffers.push(buffer.ptr);
|
||||
let ret_val = buffer.ptr.as_ptr() as usize;
|
||||
mem::forget(buffer);
|
||||
ret_val
|
||||
}
|
||||
|
||||
pub(super) fn on_removed(&mut self, buffer: NonNull<pw::sys::pw_buffer>) -> bool {
|
||||
let before_count = self.buffers.len();
|
||||
self.buffers.retain(|ptr| ptr.as_ptr() != buffer.as_ptr());
|
||||
before_count != self.buffers.len()
|
||||
}
|
||||
|
||||
pub(super) fn requeue(&mut self, stream: &pw::stream::Stream, id: usize) {
|
||||
self.buffers.retain(|ptr| {
|
||||
if ptr.as_ptr() as usize == id {
|
||||
unsafe { stream.queue_raw_buffer(ptr.as_ptr()) };
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HeldBuffers {
|
||||
fn drop(&mut self) {
|
||||
log::warn!("{} checked-out buffers at shutdown", self.buffers.len());
|
||||
self.buffers.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use std::{
|
||||
fs, io,
|
||||
os::{
|
||||
fd::{AsFd, AsRawFd, BorrowedFd, RawFd},
|
||||
unix::fs::{FileTypeExt, MetadataExt},
|
||||
},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub use timeline_syncobj::render_node::DrmRenderNode;
|
||||
use timeline_syncobj::timeline_syncobj::TimelineSyncObj;
|
||||
|
||||
const DRM_CAP_SYNCOBJ: u64 = 0x13;
|
||||
const DRM_CAP_SYNCOBJ_TIMELINE: u64 = 0x14;
|
||||
|
||||
#[repr(C)]
|
||||
struct DrmGetCap {
|
||||
capability: u64,
|
||||
value: u64,
|
||||
}
|
||||
|
||||
const DRM_IOCTL_GET_CAP: libc::c_ulong = ((3_u64 << 30)
|
||||
| ((std::mem::size_of::<DrmGetCap>() as u64) << 16)
|
||||
| ((b'd' as u64) << 8)
|
||||
| 0x0c) as libc::c_ulong;
|
||||
|
||||
fn get_cap(node: &DrmRenderNode, capability: u64) -> io::Result<u64> {
|
||||
let mut arg = DrmGetCap {
|
||||
capability,
|
||||
value: 0,
|
||||
};
|
||||
let ret = unsafe { libc::ioctl(node.as_fd().as_raw_fd(), DRM_IOCTL_GET_CAP, &mut arg) };
|
||||
if ret < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(arg.value)
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_timeline(node: &DrmRenderNode) -> bool {
|
||||
[DRM_CAP_SYNCOBJ, DRM_CAP_SYNCOBJ_TIMELINE]
|
||||
.into_iter()
|
||||
.all(|cap| get_cap(node, cap).is_ok_and(|value| value != 0))
|
||||
}
|
||||
|
||||
pub fn open_render_node(render_major: u32, render_minor: u32) -> io::Result<DrmRenderNode> {
|
||||
if !(128..=255).contains(&render_minor) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
format!("Unsupported DRM render minor: {render_minor}"),
|
||||
));
|
||||
}
|
||||
|
||||
let path = format!("/dev/dri/renderD{render_minor}");
|
||||
let metadata = fs::metadata(&path)?;
|
||||
|
||||
// verify that device actually matches the provided minor/major
|
||||
if !metadata.file_type().is_char_device()
|
||||
|| metadata.rdev() != libc::makedev(render_major, render_minor)
|
||||
{
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("{path} does not match DRM device {render_major}:{render_minor}"),
|
||||
));
|
||||
}
|
||||
|
||||
let node = DrmRenderNode::new(render_minor.into())
|
||||
.map_err(|e| io::Error::from_raw_os_error(e.raw_os_error()))?;
|
||||
|
||||
if !supports_timeline(&node) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
format!("{path} does not support DRM syncobj timelines"),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
pub(crate) fn import_timeline(node: &DrmRenderNode, fd: RawFd) -> io::Result<Arc<TimelineSyncObj>> {
|
||||
if fd < 0 {
|
||||
return Err(io::Error::from_raw_os_error(libc::EBADF));
|
||||
}
|
||||
let fd = unsafe { BorrowedFd::borrow_raw(fd) };
|
||||
TimelineSyncObj::import(node, fd)
|
||||
.map(Arc::new)
|
||||
.map_err(|e| io::Error::from_raw_os_error(e.raw_os_error()))
|
||||
}
|
||||
@@ -11,10 +11,7 @@ use smithay_client_toolkit::reexports::{
|
||||
zxdg_output_manager_v1::ZxdgOutputManagerV1,
|
||||
zxdg_output_v1::{self, ZxdgOutputV1},
|
||||
},
|
||||
protocols_wlr::{
|
||||
export_dmabuf::v1::client::zwlr_export_dmabuf_manager_v1::ZwlrExportDmabufManagerV1,
|
||||
screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1,
|
||||
},
|
||||
protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1,
|
||||
};
|
||||
|
||||
use wayland_client::{
|
||||
@@ -61,7 +58,6 @@ pub struct WlxOutput {
|
||||
pub struct WlxClient {
|
||||
pub connection: Arc<Connection>,
|
||||
pub xdg_output_mgr: ZxdgOutputManagerV1,
|
||||
pub maybe_wlr_dmabuf_mgr: Option<ZwlrExportDmabufManagerV1>,
|
||||
pub maybe_wlr_screencopy_mgr: Option<ZwlrScreencopyManagerV1>,
|
||||
pub maybe_zwp_linux_dmabuf: Option<ZwpLinuxDmabufV1>,
|
||||
pub wl_seat: WlSeat,
|
||||
@@ -93,7 +89,6 @@ impl WlxClient {
|
||||
.bind(&qh, 4..=9, ())
|
||||
.expect(WlSeat::interface().name),
|
||||
wl_shm: globals.bind(&qh, 1..=1, ()).expect(WlShm::interface().name),
|
||||
maybe_wlr_dmabuf_mgr: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
maybe_wlr_screencopy_mgr: globals.bind(&qh, 3..=3, ()).ok(),
|
||||
maybe_zwp_linux_dmabuf: globals.bind(&qh, 4..=4, ()).ok(),
|
||||
outputs: IdMap::new(),
|
||||
@@ -398,18 +393,6 @@ impl Dispatch<ZxdgOutputManagerV1, ()> for WlxClient {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZwlrExportDmabufManagerV1, ()> for WlxClient {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &ZwlrExportDmabufManagerV1,
|
||||
_event: <ZwlrExportDmabufManagerV1 as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZwlrScreencopyManagerV1, ()> for WlxClient {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
use std::{
|
||||
any::Any,
|
||||
collections::VecDeque,
|
||||
os::fd::{FromRawFd, IntoRawFd, OwnedFd, RawFd},
|
||||
sync::mpsc,
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
use drm_fourcc::{DrmFormat, DrmFourcc, DrmModifier};
|
||||
use smithay_client_toolkit::reexports::protocols_wlr::export_dmabuf::v1::client::zwlr_export_dmabuf_frame_v1::{self, ZwlrExportDmabufFrameV1};
|
||||
use wayland_client::{Connection, QueueHandle, Dispatch, Proxy};
|
||||
|
||||
use crate::{
|
||||
WlxCapture,
|
||||
frame::{DmabufFrame, FrameFormat, FramePlane, WlxFrame},
|
||||
wayland::WlxClient,
|
||||
};
|
||||
|
||||
use log::{debug, warn};
|
||||
|
||||
struct CaptureData<U, R>
|
||||
where
|
||||
U: Any,
|
||||
R: Any,
|
||||
{
|
||||
sender: mpsc::SyncSender<WlxFrame>,
|
||||
receiver: mpsc::Receiver<WlxFrame>,
|
||||
user_data: U,
|
||||
receive_callback: fn(&U, WlxFrame) -> Option<R>,
|
||||
}
|
||||
|
||||
pub struct WlrDmabufCapture<U, R>
|
||||
where
|
||||
U: Any + Send,
|
||||
R: Any + Send,
|
||||
{
|
||||
output_id: u32,
|
||||
wl: Option<Box<WlxClient>>,
|
||||
handle: Option<JoinHandle<Box<WlxClient>>>,
|
||||
data: Option<CaptureData<U, R>>,
|
||||
fds: VecDeque<RawFd>,
|
||||
}
|
||||
|
||||
impl<U, R> WlrDmabufCapture<U, R>
|
||||
where
|
||||
U: Any + Send,
|
||||
R: Any + Send,
|
||||
{
|
||||
pub fn new(wl: WlxClient, output_id: u32) -> Self {
|
||||
Self {
|
||||
output_id,
|
||||
wl: Some(Box::new(wl)),
|
||||
handle: None,
|
||||
data: None,
|
||||
fds: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<U, R> WlxCapture<U, R> for WlrDmabufCapture<U, R>
|
||||
where
|
||||
U: Any + Send,
|
||||
R: Any + Send,
|
||||
{
|
||||
fn init(
|
||||
&mut self,
|
||||
_: &[DrmFormat],
|
||||
user_data: U,
|
||||
receive_callback: fn(&U, WlxFrame) -> Option<R>,
|
||||
) {
|
||||
debug_assert!(self.wl.is_some());
|
||||
|
||||
let (sender, receiver) = std::sync::mpsc::sync_channel::<WlxFrame>(2);
|
||||
self.data = Some(CaptureData {
|
||||
sender,
|
||||
receiver,
|
||||
user_data,
|
||||
receive_callback,
|
||||
});
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.data.is_some()
|
||||
}
|
||||
fn supports_dmbuf(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn receive(&mut self) -> Option<R> {
|
||||
if let Some(data) = self.data.as_ref()
|
||||
&& let Some(WlxFrame::Dmabuf(last)) = data.receiver.try_iter().last()
|
||||
{
|
||||
// this is the only protocol that requires us to manually close the FD
|
||||
while self.fds.len() > 6 * last.num_planes {
|
||||
// safe unwrap
|
||||
let _ = unsafe { OwnedFd::from_raw_fd(self.fds.pop_back().unwrap()) };
|
||||
}
|
||||
for p in 0..last.num_planes {
|
||||
if let Some(fd) = last.planes[p].fd {
|
||||
self.fds.push_front(fd);
|
||||
}
|
||||
}
|
||||
return (data.receive_callback)(&data.user_data, WlxFrame::Dmabuf(last));
|
||||
}
|
||||
None
|
||||
}
|
||||
fn pause(&mut self) {}
|
||||
fn resume(&mut self) {
|
||||
self.receive(); // clear old frames
|
||||
}
|
||||
fn request_new_frame(&mut self) {
|
||||
if let Some(handle) = self.handle.take() {
|
||||
if handle.is_finished() {
|
||||
self.wl = Some(handle.join().unwrap()); // safe to unwrap because we checked is_finished
|
||||
} else {
|
||||
self.handle = Some(handle);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(wl) = self.wl.take() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.handle = Some(std::thread::spawn({
|
||||
let sender = self
|
||||
.data
|
||||
.as_ref()
|
||||
.expect("must call init once before request_new_frame")
|
||||
.sender
|
||||
.clone();
|
||||
let output_id = self.output_id;
|
||||
move || request_dmabuf_frame(wl, output_id, sender)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// Request a new DMA-Buf frame using the wlr-export-dmabuf protocol.
|
||||
fn request_dmabuf_frame(
|
||||
client: Box<WlxClient>,
|
||||
output_id: u32,
|
||||
sender: mpsc::SyncSender<WlxFrame>,
|
||||
) -> Box<WlxClient> {
|
||||
let Some(dmabuf_manager) = client.maybe_wlr_dmabuf_mgr.as_ref() else {
|
||||
return client;
|
||||
};
|
||||
|
||||
let Some(output) = client.outputs.get(output_id) else {
|
||||
return client;
|
||||
};
|
||||
|
||||
let transform = output.transform;
|
||||
|
||||
let (tx, rx) = mpsc::sync_channel::<zwlr_export_dmabuf_frame_v1::Event>(16);
|
||||
let name = output.name.clone();
|
||||
|
||||
let _ = dmabuf_manager.capture_output(1, &output.wl_output, &client.queue_handle, tx.clone());
|
||||
|
||||
let mut client = client;
|
||||
client.dispatch();
|
||||
|
||||
let mut frame = None;
|
||||
|
||||
rx.try_iter().for_each(|event| match event {
|
||||
zwlr_export_dmabuf_frame_v1::Event::Frame {
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
mod_high,
|
||||
mod_low,
|
||||
num_objects,
|
||||
..
|
||||
} => {
|
||||
let mut new_frame = DmabufFrame {
|
||||
format: FrameFormat {
|
||||
width,
|
||||
height,
|
||||
drm_format: DrmFormat {
|
||||
code: match DrmFourcc::try_from(format) {
|
||||
Ok(code) => code,
|
||||
Err(e) => {
|
||||
log::error!("Unrecognized fourcc: {e:?}");
|
||||
return;
|
||||
}
|
||||
},
|
||||
modifier: DrmModifier::Invalid,
|
||||
},
|
||||
transform,
|
||||
},
|
||||
mouse: None,
|
||||
num_planes: num_objects as _,
|
||||
planes: Default::default(),
|
||||
};
|
||||
new_frame.format.set_mod(mod_high, mod_low);
|
||||
frame = Some(new_frame);
|
||||
}
|
||||
zwlr_export_dmabuf_frame_v1::Event::Object {
|
||||
index,
|
||||
fd,
|
||||
offset,
|
||||
stride,
|
||||
..
|
||||
} => {
|
||||
let Some(ref mut frame) = frame else {
|
||||
return;
|
||||
};
|
||||
frame.planes[index as usize] = FramePlane {
|
||||
fd: Some(fd.into_raw_fd()),
|
||||
offset,
|
||||
stride: stride as _,
|
||||
};
|
||||
}
|
||||
zwlr_export_dmabuf_frame_v1::Event::Ready { .. } => {
|
||||
let Some(frame) = frame.take() else {
|
||||
return;
|
||||
};
|
||||
debug!("DMA-Buf frame captured");
|
||||
let frame = WlxFrame::Dmabuf(frame);
|
||||
match sender.try_send(frame) {
|
||||
Ok(_) => (),
|
||||
Err(mpsc::TrySendError::Full(_)) => (),
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => {
|
||||
log::warn!("{}: disconnected", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
zwlr_export_dmabuf_frame_v1::Event::Cancel { .. } => {
|
||||
warn!("DMA-Buf frame capture cancelled");
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
impl Dispatch<ZwlrExportDmabufFrameV1, mpsc::SyncSender<zwlr_export_dmabuf_frame_v1::Event>>
|
||||
for WlxClient
|
||||
{
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
proxy: &ZwlrExportDmabufFrameV1,
|
||||
event: <ZwlrExportDmabufFrameV1 as Proxy>::Event,
|
||||
data: &mpsc::SyncSender<zwlr_export_dmabuf_frame_v1::Event>,
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
zwlr_export_dmabuf_frame_v1::Event::Ready { .. }
|
||||
| zwlr_export_dmabuf_frame_v1::Event::Cancel { .. } => {
|
||||
proxy.destroy();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = data.send(event).or_else(|err| {
|
||||
warn!("Failed to send DMA-Buf frame event: {}", err);
|
||||
Ok::<(), mpsc::SendError<zwlr_export_dmabuf_frame_v1::Event>>(())
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ chrono.workspace = true
|
||||
glam.workspace = true
|
||||
idmap = { workspace = true, features = ["serde"] }
|
||||
idmap-derive.workspace = true
|
||||
libc.workspace = true
|
||||
log.workspace = true
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json.workspace = true
|
||||
@@ -35,4 +34,3 @@ rodio = {
|
||||
}
|
||||
walkdir = "2.5.0"
|
||||
wgui = { path = "../wgui/" }
|
||||
inotify = { version = "0.11.5", default-features = false }
|
||||
@@ -14,8 +14,6 @@ use crate::{
|
||||
windowing::OverlayWindowState,
|
||||
};
|
||||
|
||||
pub const CONFIG_VERSION: u32 = 1;
|
||||
|
||||
pub type PwTokenMap = AStrMap<String>;
|
||||
pub type SerializedWindowStates = HashMap<Arc<str>, OverlayWindowState>;
|
||||
|
||||
@@ -564,10 +562,4 @@ pub struct GeneralConfig {
|
||||
|
||||
#[serde(default)]
|
||||
pub middle_grabs_single: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub state_version: u32,
|
||||
|
||||
#[serde(default)]
|
||||
pub config_version: u32,
|
||||
}
|
||||
+116
-204
@@ -3,14 +3,14 @@ use std::{
|
||||
ffi::OsStr,
|
||||
fmt::Debug,
|
||||
fs::exists,
|
||||
path::{Path, PathBuf},
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
thread::JoinHandle,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use crate::{cache_dir, desktop_watcher::DesktopWatcher, locale::Language};
|
||||
use crate::{cache_dir, locale::Language};
|
||||
use anyhow::Context;
|
||||
use freedesktop_desktop_entry::DesktopEntry as FreedesktopDesktopEntry;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -57,32 +57,14 @@ const CATEGORY_TYPE_BLOCKLIST: [&str; 1] = ["ConsoleOnly"];
|
||||
struct DesktopFinderParams {
|
||||
size_preferences: Vec<&'static OsStr>,
|
||||
icon_folders: Vec<String>,
|
||||
app_folders: Vec<PathBuf>,
|
||||
app_folders: Vec<String>,
|
||||
langs: Vec<String>,
|
||||
}
|
||||
|
||||
struct CachedFileOwned {
|
||||
root_index: usize,
|
||||
entry: DesktopEntryOwned,
|
||||
}
|
||||
|
||||
struct CachedFile {
|
||||
root_index: usize,
|
||||
entry: Rc<DesktopEntry>,
|
||||
}
|
||||
|
||||
struct CacheBuild {
|
||||
files: HashMap<PathBuf, CachedFileOwned>,
|
||||
watcher: Option<DesktopWatcher>,
|
||||
}
|
||||
|
||||
pub struct DesktopFinder {
|
||||
params: Arc<DesktopFinderParams>,
|
||||
// include all desktop files, so that removing a higher-prio one will reveal the lower-prio underneath
|
||||
files: HashMap<PathBuf, CachedFile>,
|
||||
entry_cache: HashMap<String, Rc<DesktopEntry>>,
|
||||
watcher: Option<DesktopWatcher>,
|
||||
bg_task: Option<JoinHandle<CacheBuild>>,
|
||||
bg_task: Option<JoinHandle<HashMap<String, DesktopEntryOwned>>>,
|
||||
}
|
||||
|
||||
impl DesktopFinder {
|
||||
@@ -94,8 +76,13 @@ impl DesktopFinder {
|
||||
let mut icon_folders = vec![];
|
||||
|
||||
if let Some(data_home) = xdg.get_data_home() {
|
||||
app_folders.push(data_home.join("applications"));
|
||||
app_folders.push(data_home.join("flatpak/exports/share/applications"));
|
||||
app_folders.push(data_home.join("applications").to_string_lossy().to_string());
|
||||
app_folders.push(
|
||||
data_home
|
||||
.join("flatpak/exports/share/applications")
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
icon_folders.push(data_home.join("icons").to_string_lossy().to_string());
|
||||
icon_folders.push(
|
||||
data_home
|
||||
@@ -110,7 +97,7 @@ impl DesktopFinder {
|
||||
|
||||
// /usr/share and such
|
||||
for data_dir in xdg.get_data_dirs() {
|
||||
app_folders.push(data_dir.join("applications"));
|
||||
app_folders.push(data_dir.join("applications").to_string_lossy().to_string());
|
||||
icon_folders.push(data_dir.join("icons").to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
@@ -135,117 +122,126 @@ impl DesktopFinder {
|
||||
size_preferences,
|
||||
langs,
|
||||
}),
|
||||
files: HashMap::new(),
|
||||
entry_cache: HashMap::new(),
|
||||
watcher: None,
|
||||
bg_task: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_single(params: &DesktopFinderParams, path: &Path) -> Option<DesktopEntryOwned> {
|
||||
let file_path = path.display();
|
||||
let file_name = path.file_name()?.to_string_lossy();
|
||||
let desktop = match FreedesktopDesktopEntry::from_path(path, Some(¶ms.langs)) {
|
||||
Ok(desktop) => desktop,
|
||||
Err(e) => {
|
||||
log::debug!("Failed to read .desktop file {}: {:?}, skipping", file_path, e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
if desktop.groups.desktop_entry().is_none() {
|
||||
log::debug!("Failed to get [Desktop Entry] section for file {}, skipping", file_path);
|
||||
return None;
|
||||
}
|
||||
if desktop.only_show_in().is_some() || desktop.terminal() || desktop.no_display() || desktop.hidden() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let exec = desktop.exec()?;
|
||||
if CMD_BLOCKLIST.iter().any(|blocked| exec.contains(blocked)) {
|
||||
return None;
|
||||
}
|
||||
let exec_argv = desktop
|
||||
.parse_exec_with_uris(&[], ¶ms.langs)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|x| x.into_boxed_str())
|
||||
.collect::<Box<[_]>>();
|
||||
if exec_argv.is_empty() {
|
||||
log::debug!("Invalid Exec in desktop file {}, skipping", file_path);
|
||||
return None;
|
||||
}
|
||||
let app_name = desktop.name(¶ms.langs)?;
|
||||
let mut vec_categories = Vec::<Box<str>>::new();
|
||||
for cat in desktop.categories().unwrap_or_default() {
|
||||
if CATEGORY_TYPE_BLOCKLIST.contains(&cat) {
|
||||
return None;
|
||||
}
|
||||
if !cat.is_empty() {
|
||||
vec_categories.push(cat.into());
|
||||
}
|
||||
}
|
||||
|
||||
let icon_path = desktop
|
||||
.icon()
|
||||
.and_then(|icon_name| Self::find_icon(params, icon_name))
|
||||
.or_else(|| Self::create_icon(&file_name).ok());
|
||||
|
||||
Some(DesktopEntryOwned {
|
||||
app_id: desktop.id().into(),
|
||||
app_name: app_name.into(),
|
||||
exec_argv,
|
||||
icon_path: icon_path.map(|i| i.into_boxed_str()),
|
||||
categories: vec_categories.into_boxed_slice(),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_cache_full(params: Arc<DesktopFinderParams>) -> CacheBuild {
|
||||
fn build_cache(params: Arc<DesktopFinderParams>) -> HashMap<String, DesktopEntryOwned> {
|
||||
let start = Instant::now();
|
||||
// install watch before scan so edits during scan remain in inotify event queue
|
||||
let watcher = match DesktopWatcher::new(¶ms.app_folders) {
|
||||
Ok(watcher) => Some(watcher),
|
||||
Err(err) => {
|
||||
log::warn!("Could not watch .desktop files: {err}");
|
||||
None
|
||||
}
|
||||
};
|
||||
let mut files = HashMap::new();
|
||||
|
||||
let mut known_files = HashSet::new();
|
||||
let mut entries = HashMap::<String, DesktopEntryOwned>::new();
|
||||
|
||||
let icons_folder = cache_dir::get_path("icons");
|
||||
if !std::fs::exists(&icons_folder).unwrap_or(false) {
|
||||
let _ = std::fs::create_dir(&icons_folder);
|
||||
}
|
||||
|
||||
for (root_index, root) in params.app_folders.iter().enumerate() {
|
||||
log::debug!("Searching desktop entries in path {}", root.display());
|
||||
for entry in WalkDir::new(root)
|
||||
for path in ¶ms.app_folders {
|
||||
log::debug!("Searching desktop entries in path {}", path);
|
||||
|
||||
'entries: for entry in WalkDir::new(path)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| !entry.file_type().is_dir())
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| !e.file_type().is_dir())
|
||||
{
|
||||
let path = entry.path();
|
||||
if path.extension() != Some(OsStr::new("desktop")) || files.contains_key(path) {
|
||||
let Some(extension) = Path::new(entry.file_name()).extension() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if extension != "desktop" {
|
||||
continue; // ignore, go on
|
||||
}
|
||||
|
||||
let file_name = entry.file_name().to_string_lossy();
|
||||
let file_path = entry.path().display().to_string();
|
||||
|
||||
let desktop = match FreedesktopDesktopEntry::from_path(entry.path(), Some(¶ms.langs)) {
|
||||
Ok(desktop) => desktop,
|
||||
Err(e) => {
|
||||
log::debug!("Failed to read .desktop file {}: {:?}, skipping", file_path, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if desktop.groups.desktop_entry().is_none() {
|
||||
log::debug!("Failed to get [Desktop Entry] section for file {}, skipping", file_path);
|
||||
continue;
|
||||
}
|
||||
if let Some(parsed) = Self::read_single(¶ms, path) {
|
||||
files.insert(
|
||||
path.to_path_buf(),
|
||||
CachedFileOwned {
|
||||
root_index,
|
||||
entry: parsed,
|
||||
},
|
||||
);
|
||||
|
||||
// prefer entries from earlier data directories
|
||||
let app_id = desktop.id();
|
||||
if known_files.contains(app_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if desktop.only_show_in().is_some() || desktop.terminal() || desktop.no_display() || desktop.hidden() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(exec) = desktop.exec() else {
|
||||
log::debug!("Failed to get desktop entry Exec for file {}, skipping", file_path);
|
||||
continue;
|
||||
};
|
||||
|
||||
if CMD_BLOCKLIST.iter().any(|blocked| exec.contains(blocked)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let exec_argv = desktop
|
||||
.parse_exec_with_uris(&[], ¶ms.langs)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|x| x.into_boxed_str())
|
||||
.collect::<Box<[_]>>();
|
||||
|
||||
if exec_argv.is_empty() {
|
||||
log::debug!("Invalid Exec in desktop file {}, skipping", file_path);
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(app_name) = desktop.name(¶ms.langs) else {
|
||||
log::debug!(
|
||||
"Failed to get desktop entry application name for file {}, skipping",
|
||||
file_path
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let icon_path = desktop
|
||||
.icon()
|
||||
.and_then(|icon_name| Self::find_icon(¶ms, icon_name))
|
||||
.or_else(|| Self::create_icon(&file_name).ok());
|
||||
|
||||
let mut vec_categories = Vec::<Box<str>>::new();
|
||||
for cat in desktop.categories().unwrap_or_default() {
|
||||
if CATEGORY_TYPE_BLOCKLIST.contains(&cat) {
|
||||
continue 'entries;
|
||||
}
|
||||
if !cat.is_empty() {
|
||||
vec_categories.push(cat.into());
|
||||
}
|
||||
}
|
||||
|
||||
known_files.insert(app_id.to_owned());
|
||||
|
||||
entries.insert(
|
||||
app_id.to_owned(),
|
||||
DesktopEntryOwned {
|
||||
app_id: app_id.into(),
|
||||
app_name: app_name.into(),
|
||||
exec_argv,
|
||||
icon_path: icon_path.map(|i| i.into_boxed_str()),
|
||||
categories: vec_categories.into_boxed_slice(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("Desktop entry & icon scan took {:?}", start.elapsed());
|
||||
CacheBuild { files, watcher }
|
||||
}
|
||||
|
||||
pub fn resolve_icon(&self, icon_name: &str) -> Option<String> {
|
||||
Self::find_icon(&self.params, icon_name)
|
||||
entries
|
||||
}
|
||||
|
||||
fn find_icon(params: &DesktopFinderParams, icon_name: &str) -> Option<String> {
|
||||
@@ -310,113 +306,29 @@ impl DesktopFinder {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(CacheBuild { files, watcher }) = bg_task.join() else {
|
||||
log::warn!("Desktop entry scan thread panicked");
|
||||
let Ok(entries) = bg_task.join() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.files = files
|
||||
.into_iter()
|
||||
.map(|(path, file)| {
|
||||
(
|
||||
path,
|
||||
CachedFile {
|
||||
root_index: file.root_index,
|
||||
entry: Rc::new(file.entry.into()),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
self.watcher = watcher;
|
||||
|
||||
let mut files: Vec<_> = self.files.iter().collect();
|
||||
files.sort_by(|(path_a, a), (path_b, b)| a.root_index.cmp(&b.root_index).then_with(|| path_a.cmp(path_b)));
|
||||
self.entry_cache.clear();
|
||||
for (_, file) in files {
|
||||
self
|
||||
.entry_cache
|
||||
.entry(file.entry.app_id.to_string())
|
||||
.or_insert_with(|| Rc::clone(&file.entry));
|
||||
for (app_id, entry) in entries {
|
||||
self.entry_cache.insert(app_id, Rc::new(entry.into()));
|
||||
}
|
||||
}
|
||||
|
||||
fn update_app_id(&mut self, app_id: &str) {
|
||||
let best = self
|
||||
.files
|
||||
.iter()
|
||||
.filter(|(_, file)| file.entry.app_id.as_ref() == app_id)
|
||||
.min_by(|(path_a, a), (path_b, b)| a.root_index.cmp(&b.root_index).then_with(|| path_a.cmp(path_b)))
|
||||
.map(|(_, file)| Rc::clone(&file.entry));
|
||||
if let Some(best) = best {
|
||||
self.entry_cache.insert(app_id.to_owned(), best);
|
||||
} else {
|
||||
self.entry_cache.remove(app_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_file_changes(&mut self) {
|
||||
self.wait_for_entries();
|
||||
let Some(watcher) = self.watcher.as_mut() else {
|
||||
return;
|
||||
};
|
||||
let changes = watcher.collect_changes();
|
||||
if changes.overflowed {
|
||||
log::warn!("Desktop file inotify event queue overflowed; rebuilding cache");
|
||||
self.refresh();
|
||||
self.wait_for_entries();
|
||||
return;
|
||||
}
|
||||
|
||||
let mut affected_ids = HashSet::new();
|
||||
if !changes.removed_directories.is_empty() {
|
||||
self.files.retain(|path, file| {
|
||||
let removed = changes
|
||||
.removed_directories
|
||||
.iter()
|
||||
.any(|directory| path.starts_with(directory));
|
||||
if removed {
|
||||
affected_ids.insert(file.entry.app_id.to_string());
|
||||
}
|
||||
!removed
|
||||
});
|
||||
}
|
||||
for path in changes.files {
|
||||
if let Some(previous) = self.files.remove(&path) {
|
||||
affected_ids.insert(previous.entry.app_id.to_string());
|
||||
}
|
||||
let root_index = self.params.app_folders.iter().position(|root| path.starts_with(root));
|
||||
if let Some(root_index) = root_index
|
||||
&& let Some(entry) = Self::read_single(&self.params, &path)
|
||||
{
|
||||
affected_ids.insert(entry.app_id.to_string());
|
||||
self.files.insert(
|
||||
path,
|
||||
CachedFile {
|
||||
root_index,
|
||||
entry: Rc::new(entry.into()),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
for app_id in affected_ids {
|
||||
self.update_app_id(&app_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_cached_entry(&mut self, app_id: &str) -> Option<&Rc<DesktopEntry>> {
|
||||
self.sync_file_changes();
|
||||
pub fn get_cached_entry(&self, app_id: &str) -> Option<&Rc<DesktopEntry>> {
|
||||
self.entry_cache.get(app_id)
|
||||
}
|
||||
|
||||
pub fn find_entries(&mut self) -> HashMap<String, Rc<DesktopEntry>> {
|
||||
self.sync_file_changes();
|
||||
self.wait_for_entries();
|
||||
self.entry_cache.clone()
|
||||
}
|
||||
|
||||
pub fn refresh(&mut self) {
|
||||
let bg_task = std::thread::spawn({
|
||||
let params = self.params.clone();
|
||||
move || Self::build_cache_full(params)
|
||||
move || Self::build_cache(params)
|
||||
});
|
||||
self.bg_task = Some(bg_task);
|
||||
}
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::OsStr,
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use inotify::{EventMask, Inotify, WatchDescriptor, WatchMask};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct DesktopChanges {
|
||||
pub files: HashSet<PathBuf>,
|
||||
pub removed_directories: HashSet<PathBuf>,
|
||||
pub overflowed: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct DesktopWatcher {
|
||||
inotify: Inotify,
|
||||
roots: Vec<PathBuf>,
|
||||
by_wd: HashMap<WatchDescriptor, HashSet<PathBuf>>,
|
||||
by_path: HashMap<PathBuf, WatchDescriptor>,
|
||||
}
|
||||
|
||||
impl DesktopWatcher {
|
||||
pub fn new(roots: &[PathBuf]) -> io::Result<Self> {
|
||||
let mut watcher = Self {
|
||||
inotify: Inotify::init()?,
|
||||
roots: roots.to_vec(),
|
||||
by_wd: HashMap::new(),
|
||||
by_path: HashMap::new(),
|
||||
};
|
||||
|
||||
for root in roots {
|
||||
for ancestor in root.ancestors().skip(1) {
|
||||
if ancestor.is_dir() {
|
||||
watcher.watch_directory(ancestor);
|
||||
}
|
||||
}
|
||||
watcher.watch_tree(root, None);
|
||||
}
|
||||
|
||||
Ok(watcher)
|
||||
}
|
||||
|
||||
fn is_related_directory(&self, path: &Path) -> bool {
|
||||
self
|
||||
.roots
|
||||
.iter()
|
||||
.any(|root| path.starts_with(root) || root.starts_with(path))
|
||||
}
|
||||
|
||||
fn is_application_file(&self, path: &Path) -> bool {
|
||||
path.extension() == Some(OsStr::new("desktop")) && self.roots.iter().any(|root| path.starts_with(root))
|
||||
}
|
||||
|
||||
fn watch_directory(&mut self, path: &Path) {
|
||||
if self.by_path.contains_key(path) || !path.is_dir() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mask = WatchMask::CREATE
|
||||
| WatchMask::DELETE
|
||||
| WatchMask::MOVED_FROM
|
||||
| WatchMask::MOVED_TO
|
||||
| WatchMask::MODIFY
|
||||
| WatchMask::CLOSE_WRITE
|
||||
| WatchMask::ATTRIB
|
||||
| WatchMask::DELETE_SELF
|
||||
| WatchMask::MOVE_SELF
|
||||
| WatchMask::ONLYDIR;
|
||||
|
||||
let wd = match self.inotify.watches().add(path, mask) {
|
||||
Ok(wd) => wd,
|
||||
Err(err) => {
|
||||
log::warn!("Unable to watch desktop directory {}: {err}", path.display());
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.by_wd.entry(wd.clone()).or_default().insert(path.to_path_buf());
|
||||
self.by_path.insert(path.to_path_buf(), wd);
|
||||
}
|
||||
|
||||
fn watch_tree(&mut self, directory: &Path, mut existing_files: Option<&mut HashSet<PathBuf>>) {
|
||||
let mut pending = vec![directory.to_path_buf()];
|
||||
|
||||
while let Some(dir) = pending.pop() {
|
||||
if !self.is_related_directory(&dir) || !dir.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.watch_directory(&dir);
|
||||
|
||||
let children = match std::fs::read_dir(&dir) {
|
||||
Ok(children) => children,
|
||||
Err(err) => {
|
||||
log::debug!("Cannot read desktop directory {}: {err}", dir.display());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
for child in children.flatten() {
|
||||
let path = child.path();
|
||||
|
||||
match child.file_type() {
|
||||
Ok(kind) if kind.is_dir() => {
|
||||
if self.is_related_directory(&path) {
|
||||
pending.push(path);
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
if self.is_application_file(&path)
|
||||
&& let Some(files) = existing_files.as_deref_mut()
|
||||
{
|
||||
files.insert(path);
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unwatch_tree(&mut self, directory: &Path) {
|
||||
let descendants: Vec<_> = self
|
||||
.by_path
|
||||
.iter()
|
||||
.filter(|(path, _)| path.starts_with(directory))
|
||||
.map(|(path, wd)| (path.clone(), wd.clone()))
|
||||
.collect();
|
||||
|
||||
for (path, wd) in descendants {
|
||||
self.by_path.remove(&path);
|
||||
|
||||
if let Some(paths) = self.by_wd.get_mut(&wd) {
|
||||
paths.remove(&path);
|
||||
|
||||
if paths.is_empty() {
|
||||
self.by_wd.remove(&wd);
|
||||
let _ = self.inotify.watches().remove(wd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_changes(&mut self) -> DesktopChanges {
|
||||
let mut changes = DesktopChanges::default();
|
||||
let mut buffer = [0u8; 64 * 1024];
|
||||
|
||||
loop {
|
||||
let events = match self.inotify.read_events(&mut buffer) {
|
||||
Ok(events) => events,
|
||||
Err(err) if err.kind() == io::ErrorKind::WouldBlock => break,
|
||||
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(err) => {
|
||||
log::warn!("Unable to read desktop directory inotify events: {err}");
|
||||
changes.overflowed = true;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
for event in events {
|
||||
if event.mask.contains(EventMask::Q_OVERFLOW) {
|
||||
changes.overflowed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if event.mask.contains(EventMask::IGNORED) {
|
||||
if let Some(paths) = self.by_wd.remove(&event.wd) {
|
||||
for path in paths {
|
||||
self.by_path.remove(&path);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(directories) = self.by_wd.get(&event.wd).cloned() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for directory in directories {
|
||||
let path = match event.name {
|
||||
Some(name) => directory.join(name),
|
||||
None => directory,
|
||||
};
|
||||
|
||||
if event.mask.intersects(EventMask::DELETE_SELF | EventMask::MOVE_SELF) {
|
||||
changes.removed_directories.insert(path.clone());
|
||||
self.unwatch_tree(&path);
|
||||
} else if event.mask.contains(EventMask::ISDIR) {
|
||||
if event.mask.intersects(EventMask::DELETE | EventMask::MOVED_FROM) {
|
||||
changes.removed_directories.insert(path.clone());
|
||||
self.unwatch_tree(&path);
|
||||
}
|
||||
|
||||
if event.mask.intersects(EventMask::CREATE | EventMask::MOVED_TO) {
|
||||
self.watch_tree(&path, Some(&mut changes.files));
|
||||
}
|
||||
} else if self.is_application_file(&path) {
|
||||
changes.files.insert(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changes
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ pub mod dash_interface;
|
||||
pub mod dash_interface_emulated;
|
||||
pub mod data_dir;
|
||||
pub mod desktop_finder;
|
||||
mod desktop_watcher;
|
||||
pub mod locale;
|
||||
pub mod openxr_actions;
|
||||
pub mod openxr_bindings_schema;
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{common::LeftRight, config::DefaultPositioning};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
|
||||
pub enum Positioning {
|
||||
/// Freely grabbed, but stored relative to the anchor and moves with it.
|
||||
/// Stays in place, recenters relative to HMD
|
||||
#[default]
|
||||
Floating,
|
||||
/// Stays in place, recenters relative to anchor. Follows anchor during anchor grab.
|
||||
|
||||
Reference in new issue
Block a user