mirror of
https://github.com/wayvr-org/wayvr.git
synced 2026-10-28 17:06:46 +01:00
Compare commits
72
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b34595e489 | ||
|
|
330cf1bdeb | ||
|
|
19b7d85682 | ||
|
|
346c6391ef | ||
|
|
e850c2c8b6 | ||
|
|
5360697828 | ||
|
|
3a869e3c35 | ||
|
|
b3ac55474f | ||
|
|
a4986a3d9f | ||
|
|
f3f7d97ed6 | ||
|
|
10857739a4 | ||
|
|
42e9deb54c | ||
|
|
d6515b3c2a | ||
|
|
a101c79892 | ||
|
|
82565e975f | ||
|
|
959f66b271 | ||
|
|
745728d90d | ||
|
|
68944184de | ||
|
|
4590371a4c | ||
|
|
d1255e5f71 | ||
|
|
537f2f5397 | ||
|
|
77730432b1 | ||
|
|
fd3e9069e4 | ||
|
|
2226d95834 | ||
|
|
a0d36d7257 | ||
|
|
38382275ec | ||
|
|
5517c3a809 | ||
|
|
f0d96802ba | ||
|
|
3f43003a0b | ||
|
|
28842d6d69 | ||
|
|
573bd5da53 | ||
|
|
da3b19a495 | ||
|
|
ad0d1aa8a2 | ||
|
|
b85976bdf0 | ||
|
|
2ea97e88bb | ||
|
|
b7b93131c9 | ||
|
|
e7c07c7789 | ||
|
|
0a158c102c | ||
|
|
11a5c8305b | ||
|
|
d5f0306aab | ||
|
|
6d6e7033ac | ||
|
|
16bc4383b5 | ||
|
|
0e6526836e | ||
|
|
2e8d1589f5 | ||
|
|
54713b95f4 | ||
|
|
6f761a99dc | ||
|
|
c87bcee358 | ||
|
|
0eab390702 | ||
|
|
9dcdf852c7 | ||
|
|
b3261dbe4f | ||
|
|
329a89ca08 | ||
|
|
c284162068 | ||
|
|
f2beabd39a | ||
|
|
126120e52e | ||
|
|
e986c3591e | ||
|
|
a0f096b9dc | ||
|
|
7b95f01ff4 | ||
|
|
d9cc73be8b | ||
|
|
548ea36a84 | ||
|
|
d4a5d4c81f | ||
|
|
443bc01361 | ||
|
|
9b9d1b9f8a | ||
|
|
40811f219e | ||
|
|
a244c90d05 | ||
|
|
f0a1b82db5 | ||
|
|
b45c68cb59 | ||
|
|
234c4a4021 | ||
|
|
10e54728fa | ||
|
|
e4daef209b | ||
|
|
d318831ecc | ||
|
|
48be76c443 | ||
|
|
9d3546b0c4 |
No files matched your search
@@ -9,7 +9,9 @@ assignees: ''
|
||||
|
||||
## Description
|
||||
<!--
|
||||
If this is a regression, please mention which version was working previously.
|
||||
*** Do NOT use AI to generate a bug report! ***
|
||||
Simply write down what's wrong and maybe provide screenshots or a video.
|
||||
If this has worked before, please mention which version was last working.
|
||||
-->
|
||||
|
||||
## System Info
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: Build AppImage
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
@@ -12,10 +13,17 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
|
||||
|
||||
jobs:
|
||||
build_appimage:
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
arch: x86_64
|
||||
- runner: ubuntu-24.04-arm
|
||||
arch: aarch64
|
||||
runs-on: ${{ matrix.runner }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./wayvr
|
||||
@@ -38,5 +46,10 @@ jobs:
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: WayVR-${{ github.ref_name }}-x86_64.AppImage
|
||||
path: ./wayvr/WayVR-x86_64.AppImage
|
||||
name: WayVR-${{ github.ref_name }}-${{ matrix.arch }}.AppImage
|
||||
path: ./wayvr/WayVR-${{ matrix.arch }}.AppImage
|
||||
- name: Upload AppImage zsync
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: WayVR-${{ github.ref_name }}-${{ matrix.arch }}.AppImage.zsync
|
||||
path: ./wayvr/WayVR-${{ matrix.arch }}.AppImage.zsync
|
||||
@@ -83,6 +83,16 @@ jobs:
|
||||
asset_name: WayVR-${{ github.ref_name }}-x86_64.AppImage
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload AppImage zsync
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_KEY }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./wayvr/WayVR-x86_64.AppImage.zsync
|
||||
asset_name: WayVR-${{ github.ref_name }}-x86_64.AppImage.zsync
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload vendor tarball
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
sudo apt install \
|
||||
sudo apt-get install -y \
|
||||
xwayland \
|
||||
libxcb1-dev \
|
||||
libxcb-cursor-dev \
|
||||
@@ -11,7 +12,7 @@ git clone https://github.com/Supreeeme/xwayland-satellite.git
|
||||
cd xwayland-satellite
|
||||
|
||||
cargo build --release
|
||||
chmod +x ../target/release/xwayland-satellite
|
||||
chmod +x target/release/xwayland-satellite
|
||||
cd ..
|
||||
|
||||
cp xwayland-satellite/target/release/xwayland-satellite ${APPDIR}/usr/bin
|
||||
cp xwayland-satellite/target/release/xwayland-satellite "${APPDIR}/usr/bin"
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
cargo build --release
|
||||
chmod +x ../target/release/wayvr
|
||||
cp ../target/release/wayvr ${APPDIR}/usr/bin
|
||||
cp ../target/release/wayvr "${APPDIR}/usr/bin"
|
||||
@@ -1,4 +1,32 @@
|
||||
#!/bin/sh
|
||||
export VERSION=$GITHUB_REF_NAME
|
||||
./linuxdeploy-x86_64.AppImage -dwayvr.desktop -iwayvr.png --appdir=${APPDIR} --output appimage --exclude-library '*libpipewire*'
|
||||
mv WayVR-$VERSION-x86_64.AppImage WayVR-x86_64.AppImage
|
||||
set -eu
|
||||
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64)
|
||||
APPIMAGE_ARCH=x86_64
|
||||
;;
|
||||
aarch64|arm64)
|
||||
APPIMAGE_ARCH=aarch64
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture: $(uname -m)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
LINUXDEPLOY="linuxdeploy-${APPIMAGE_ARCH}.AppImage"
|
||||
VERSION="${GITHUB_REF_NAME:-dev}"
|
||||
export VERSION
|
||||
export LDAI_UPDATE_INFORMATION="gh-releases-zsync|wayvr-org|wayvr|latest|WayVR-*-${APPIMAGE_ARCH}.AppImage.zsync"
|
||||
|
||||
echo "Packaging AppImage for ${APPIMAGE_ARCH}"
|
||||
|
||||
"./${LINUXDEPLOY}" \
|
||||
-dwayvr.desktop \
|
||||
-iwayvr.png \
|
||||
--appdir="${APPDIR}" \
|
||||
--output appimage \
|
||||
--exclude-library '*libpipewire*'
|
||||
|
||||
mv "WayVR-${VERSION}-${APPIMAGE_ARCH}.AppImage" "WayVR-${APPIMAGE_ARCH}.AppImage"
|
||||
mv "WayVR-${VERSION}-${APPIMAGE_ARCH}.AppImage.zsync" "WayVR-${APPIMAGE_ARCH}.AppImage.zsync"
|
||||
@@ -1,17 +1,39 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64)
|
||||
APPIMAGE_ARCH=x86_64
|
||||
;;
|
||||
aarch64|arm64)
|
||||
APPIMAGE_ARCH=aarch64
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture: $(uname -m)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
LINUXDEPLOY="linuxdeploy-${APPIMAGE_ARCH}.AppImage"
|
||||
|
||||
echo "Preparing AppImage environment for ${APPIMAGE_ARCH}"
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y fuse cmake pkg-config fontconfig libasound2-dev libxkbcommon-dev libxkbcommon-x11-0 libxkbcommon-x11-dev \
|
||||
libopenxr-dev libfontconfig-dev libdbus-1-dev libpipewire-0.3-0 libpipewire-0.3-dev libspa-0.2-dev libx11-6 libxext6 libxrandr2 \
|
||||
libx11-dev libxext-dev libxrandr-dev libopenvr-dev libopenvr-api1 libwayland-dev libegl-dev libxcb-glx0 libxcb-glx0-dev \
|
||||
libx11-dev libxext-dev libxrandr-dev libopenvr-dev libopenvr-api1t64 libwayland-dev libegl-dev libxcb-glx0 libxcb-glx0-dev \
|
||||
dav1d libdav1d-dev libvulkan-dev glslc vulkan-tools libudev-dev libudev1 libinput-dev libinput10
|
||||
|
||||
rustup update
|
||||
|
||||
if [ "$APPDIR" != "" ]; then
|
||||
test -f linuxdeploy-x86_64.AppImage || wget -q "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
|
||||
chmod +x linuxdeploy-x86_64.AppImage
|
||||
if [ -n "${APPDIR:-}" ]; then
|
||||
if [ ! -f "$LINUXDEPLOY" ]; then
|
||||
wget -q "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/${LINUXDEPLOY}"
|
||||
fi
|
||||
chmod +x "$LINUXDEPLOY"
|
||||
|
||||
test -d ${APPDIR} && rm -rf ${APPDIR}
|
||||
mkdir -p ${APPDIR}/usr/bin
|
||||
if [ -d "$APPDIR" ]; then
|
||||
rm -rf "$APPDIR"
|
||||
fi
|
||||
mkdir -p "$APPDIR/usr/bin"
|
||||
fi
|
||||
Generated
+17
-141
@@ -1250,15 +1250,6 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
@@ -1735,7 +1726,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3149,7 +3140,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3268,7 +3259,7 @@ dependencies = [
|
||||
"objc2-core-data",
|
||||
"objc2-core-image",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-quartz-core 0.2.2",
|
||||
"objc2-quartz-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3377,7 +3368,7 @@ dependencies = [
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-metal 0.2.2",
|
||||
"objc2-metal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3458,17 +3449,6 @@ dependencies = [
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-metal"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"objc2 0.6.4",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-open-directory"
|
||||
version = "0.3.2"
|
||||
@@ -3490,20 +3470,7 @@ dependencies = [
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-metal 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-metal 0.3.2",
|
||||
"objc2-metal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3531,7 +3498,7 @@ dependencies = [
|
||||
"objc2-core-location",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-link-presentation",
|
||||
"objc2-quartz-core 0.2.2",
|
||||
"objc2-quartz-core",
|
||||
"objc2-symbols",
|
||||
"objc2-uniform-type-identifiers",
|
||||
"objc2-user-notifications",
|
||||
@@ -3651,7 +3618,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ovr_overlay"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/galister/ovr_overlay_oyasumi?rev=e477bd2a9e04293ea68c1e7529ef2cb131f32acc#e477bd2a9e04293ea68c1e7529ef2cb131f32acc"
|
||||
source = "git+https://github.com/galister/ovr_overlay_oyasumi?rev=3b7d24cee24aa2b1ad7dffc5d5c8568508049bdd#3b7d24cee24aa2b1ad7dffc5d5c8568508049bdd"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"derive_more",
|
||||
@@ -3666,7 +3633,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ovr_overlay_sys"
|
||||
version = "0.0.0"
|
||||
source = "git+https://github.com/galister/ovr_overlay_oyasumi?rev=e477bd2a9e04293ea68c1e7529ef2cb131f32acc#e477bd2a9e04293ea68c1e7529ef2cb131f32acc"
|
||||
source = "git+https://github.com/galister/ovr_overlay_oyasumi?rev=3b7d24cee24aa2b1ad7dffc5d5c8568508049bdd#3b7d24cee24aa2b1ad7dffc5d5c8568508049bdd"
|
||||
dependencies = [
|
||||
"autocxx",
|
||||
"autocxx-build",
|
||||
@@ -4054,7 +4021,7 @@ version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"heck 0.4.1",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"multimap",
|
||||
@@ -4357,18 +4324,6 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-metal"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135"
|
||||
dependencies = [
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"objc2-quartz-core 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.12.0"
|
||||
@@ -4667,7 +4622,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5026,12 +4981,6 @@ version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "slabbin"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9db491c0d4152a069911a0fbdaca959691bf0b9d7110d98a7ed1c8e59b79ab30"
|
||||
|
||||
[[package]]
|
||||
name = "slice-of-array"
|
||||
version = "0.3.2"
|
||||
@@ -5401,10 +5350,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.3",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5739,8 +5688,6 @@ dependencies = [
|
||||
"rust-embed",
|
||||
"tracing-subscriber",
|
||||
"ureq",
|
||||
"vulkano",
|
||||
"vulkano-shaders",
|
||||
"wgui",
|
||||
"winit",
|
||||
"wlx-common",
|
||||
@@ -5905,70 +5852,6 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vk-parse"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3859da4d7b98bec73e68fb65815d47a263819c415c90eed42b80440a02cbce8c"
|
||||
dependencies = [
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vulkano"
|
||||
version = "0.35.2"
|
||||
source = "git+https://github.com/galister/vulkano.git?rev=cf7f92867928a56ce16b376037c1120f2b167678#cf7f92867928a56ce16b376037c1120f2b167678"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"bytemuck",
|
||||
"crossbeam-queue",
|
||||
"foldhash 0.1.5",
|
||||
"half",
|
||||
"heck 0.4.1",
|
||||
"indexmap 2.14.0",
|
||||
"libloading 0.8.9",
|
||||
"nom 7.1.3",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"raw-window-handle",
|
||||
"raw-window-metal",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"slabbin",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"vk-parse",
|
||||
"vulkano-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vulkano-macros"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/galister/vulkano.git?rev=cf7f92867928a56ce16b376037c1120f2b167678#cf7f92867928a56ce16b376037c1120f2b167678"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vulkano-shaders"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bf501461be7cef2893c0e62c50945add9763cc482051d29053f6157089d5ea9"
|
||||
dependencies = [
|
||||
"foldhash 0.1.5",
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"shaderc",
|
||||
"syn 2.0.118",
|
||||
"vulkano",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
@@ -6211,7 +6094,6 @@ name = "wayvr"
|
||||
version = "26.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ash",
|
||||
"bytes",
|
||||
"calloop 0.14.4",
|
||||
"calloop-wayland-source 0.4.1",
|
||||
@@ -6249,6 +6131,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_json5",
|
||||
"serde_yaml",
|
||||
"shaderc",
|
||||
"signal-hook",
|
||||
"slotmap",
|
||||
"smallvec",
|
||||
@@ -6261,8 +6144,6 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
"vulkano",
|
||||
"vulkano-shaders",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-misc",
|
||||
@@ -6349,6 +6230,7 @@ name = "wgui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ash",
|
||||
"bytes",
|
||||
"cosmic-text",
|
||||
"dav1d-sys",
|
||||
@@ -6360,18 +6242,18 @@ dependencies = [
|
||||
"lru",
|
||||
"ouroboros",
|
||||
"parking_lot",
|
||||
"raw-window-handle",
|
||||
"regex",
|
||||
"resvg",
|
||||
"roxmltree 0.21.1",
|
||||
"rust-embed",
|
||||
"rustc-hash",
|
||||
"serde_json",
|
||||
"shaderc",
|
||||
"slotmap",
|
||||
"smallvec",
|
||||
"strum",
|
||||
"taffy",
|
||||
"vulkano",
|
||||
"vulkano-shaders",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6425,7 +6307,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6982,12 +6864,6 @@ dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
||||
|
||||
[[package]]
|
||||
name = "xmlparser"
|
||||
version = "0.13.6"
|
||||
|
||||
-13
@@ -38,25 +38,12 @@ smol = "2.0.2"
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||
uuid = { version = "1.19.0", features = ["fast-rng", "serde", "v4"] }
|
||||
vulkano = {
|
||||
version = "0.35.2",
|
||||
default-features = false,
|
||||
features = [
|
||||
"macros",
|
||||
]
|
||||
}
|
||||
vulkano-shaders = "0.35.0"
|
||||
wayland-client = { version = "0.31" }
|
||||
wayland-protocols = { version = "0.32" }
|
||||
wayland-protocols-misc = { version = "0.3.12" }
|
||||
winit = "0.30.12"
|
||||
xdg = "3.0.0"
|
||||
|
||||
[patch.crates-io]
|
||||
vulkano = {
|
||||
git = "https://github.com/galister/vulkano.git",
|
||||
rev = "cf7f92867928a56ce16b376037c1120f2b167678"
|
||||
}
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
@@ -37,7 +37,7 @@ There are multiple ways to install WayVR:
|
||||
|
||||
**Note:** If you are using Monado or WiVRn, no additional setup steps are required for Flatpak Steam compatibility—most people use WayVR seamlessly with Monado/WiVRn.
|
||||
|
||||
### SteamVR via Steam Flatpak
|
||||
### SteamVR (PC) via Steam Flatpak
|
||||
|
||||
For users specifically running **SteamVR via Steam Flatpak**, follow these steps:
|
||||
|
||||
@@ -50,6 +50,20 @@ For users specifically running **SteamVR via Steam Flatpak**, follow these steps
|
||||
1. Start SteamVR.
|
||||
1. `flatpak run --command='/path/to/squashfs-root/AppRun' com.valvesoftware.Steam`
|
||||
|
||||
### Installing on Steam Frame
|
||||
|
||||
Steam Frame support is experimental. Please report issues.
|
||||
|
||||
It's possible to install and run WayVR on the Steam Frame in headless mode. You will not have access to your KDE desktop, but you will be able to run applications in 3D space around you.
|
||||
|
||||
Steps to install on Steam Frame:
|
||||
1. Grab the latest aarch64 AppImage
|
||||
- For nightly AppImages go [here](https://github.com/wayvr-org/wayvr/actions/workflows/build-appimage.yml) and select the topmost list item a ✅️
|
||||
- The downloads will be on the bottom of the page (GitHub login required).
|
||||
1. Install the AppImage using GearLever
|
||||
1. (Recommended) In Frame's SteamVR settings, set overlay quality to High.
|
||||
1. Start WayVR from the taskbar (same place where you'd start Destkop)
|
||||
|
||||
## First Start
|
||||
|
||||
**When the screen share pop-up appears, check your notifications or the terminal and select the screens in the order it requests.**
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="m12 13.4l-4.9 4.9q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7l4.9-4.9l-4.9-4.9q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l4.9 4.9l4.9-4.9q.275-.275.7-.275t.7.275t.275.7t-.275.7L13.4 12l4.9 4.9q.275.275.275.7t-.275.7t-.7.275t-.7-.275z"/></svg>
|
||||
|
After Width: | Height: | Size: 468 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M9 22q-.825 0-1.412-.587T7 20v-3H4q-.825 0-1.412-.587T2 15v-2h6.45l1.7 2.55q.125.2.35.325t.475.125q.325 0 .6-.2t.375-.5l1.35-4.05l.85 1.3q.15.2.375.325T15 13h7v2q0 .825-.588 1.413T20 17h-3v3q0 .825-.587 1.413T15 22zm1.7-9.25l-.875-1.3q-.125-.2-.35-.325T9 11H2V9q0-.825.588-1.412T4 7h3V4q0-.825.588-1.412T9 2h6q.825 0 1.413.588T17 4v3h3q.825 0 1.413.588T22 9v2h-6.475l-1.7-2.55q-.125-.2-.35-.325T13 8q-.325 0-.587.2t-.363.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 660 B |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 24 24"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs id="defs1" />
|
||||
<path fill="currentColor" d="m 10,12 c 0,-1.104569 0.895431,-2 2,-2 1.104569,0 2,0.895431 2,2 0,1.104569 -0.895431,2 -2,2 -1.104569,0 -2,-0.895431 -2,-2" id="path1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 422 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M2 17h18a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H2m0 16V3m5 14v3a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-3"/><circle cx="16" cy="11" r="2"/><circle cx="8" cy="11" r="2"/></g></svg>
|
||||
|
After Width: | Height: | Size: 462 B |
@@ -6,6 +6,16 @@
|
||||
<var key="side_button_size" value="48" />
|
||||
</vars>
|
||||
|
||||
<blueprint name="context_menu_power_options">
|
||||
<context_menu>
|
||||
<cell translation="APP_SETTINGS.RESTART_WAYVR" action="restart" sprite_src_builtin="@/dashboard/refresh.svg"/>
|
||||
<cell translation="APP_SETTINGS.CLOSE_WAYVR" action="close" sprite_src_builtin="@/dashboard/close.svg"/>
|
||||
<separator/>
|
||||
<cell translation="POWER_OPTIONS.SUSPEND" action="suspend" sprite_src_builtin="@/dashboard/sleep.svg"/>
|
||||
<cell translation="POWER_OPTIONS.SHUTDOWN" action="shutdown" sprite_src_builtin="@/dashboard/power.svg"/>
|
||||
</context_menu>
|
||||
</blueprint>
|
||||
|
||||
<template name="SideButton">
|
||||
<Button
|
||||
id="${id}"
|
||||
@@ -136,6 +146,9 @@
|
||||
|
||||
<!-- Left bottom side -->
|
||||
<div margin_left="8">
|
||||
<Button id="btn_power" color="background(transparent)" border_color="background(transparent)" tooltip="POWER_OPTIONS.TITLE" tooltip_side="top">
|
||||
<sprite src_builtin="@/dashboard/power.svg" width="24" height="24" margin="8" />
|
||||
</Button>
|
||||
<Button id="btn_audio" color="background(transparent)" border_color="background(transparent)" tooltip="AUDIO.VOLUME" tooltip_side="top">
|
||||
<sprite src_builtin="@/dashboard/volume.svg" width="24" height="24" margin="8" />
|
||||
</Button>
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
gap="8"
|
||||
round="8"/>
|
||||
|
||||
<!-- id -->
|
||||
<template name="GroupBox">
|
||||
<rectangle macro="group_box">
|
||||
<rectangle macro="group_box" id="rect">
|
||||
<CHILDREN/>
|
||||
</rectangle>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
<layout version="2">
|
||||
<include src="@/gui/t_menu_button.xml" />
|
||||
|
||||
<template name="MiscButton">
|
||||
<Button
|
||||
id="${id}" macro="home_button"
|
||||
min_width="64" height="36"
|
||||
padding_left="8" padding_right="8">
|
||||
<div gap="8"
|
||||
align_items="center"
|
||||
justify_content="center"
|
||||
flex_direction="row">
|
||||
<sprite src_builtin="${icon}" width="24" height="24" />
|
||||
<label weight="bold" size="18" text="${text}" translation="${translation}" />
|
||||
</div>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<elements>
|
||||
<div
|
||||
flex_direction="column"
|
||||
@@ -33,17 +48,9 @@
|
||||
</div>
|
||||
|
||||
<div gap="8">
|
||||
<Button id="btn_welcome_screen" macro="home_button" tooltip="APP_SETTINGS.SHOW_WELCOME_SCREEN">
|
||||
<sprite src_builtin="@/dashboard/welcome.svg" width="32" height="32" margin="16" />
|
||||
</Button>
|
||||
|
||||
<Button id="btn_website" macro="home_button" tooltip_str="wayvr.org">
|
||||
<sprite src_builtin="@/dashboard/globe.svg" width="32" height="32" margin="16" />
|
||||
</Button>
|
||||
|
||||
<Button id="btn_donate" macro="home_button" tooltip="DONATE.SUPPORT_US">
|
||||
<sprite src_builtin="@/dashboard/opencollective.svg" width="32" height="32" margin="16" />
|
||||
</Button>
|
||||
<MiscButton id="btn_welcome_screen" icon="@/dashboard/welcome.svg" translation="APP_SETTINGS.TUTORIAL"/>
|
||||
<MiscButton id="btn_website" icon="@/dashboard/globe.svg" text="wayvr.org"/>
|
||||
<MiscButton id="btn_donate" icon="@/dashboard/opencollective.svg" translation="DONATE.SUPPORT_US"/>
|
||||
</div>
|
||||
</div>
|
||||
</elements>
|
||||
|
||||
@@ -31,6 +31,27 @@
|
||||
<RadioBox text="${text}" translation="${translation}" value="${value}" tooltip="${tooltip}" />
|
||||
</template>
|
||||
|
||||
<!-- title, sprite, color, tooltip_str -->
|
||||
<template name="DiagnosticsRow">
|
||||
<div align_items="center">
|
||||
<label text="${title}" weight="bold" min_width="140"/>
|
||||
<Button tooltip_str="${tooltip_str}" color="background(transparent)">
|
||||
<sprite width="24" height="24" color="${color}" src_builtin="${sprite}" parent_color="ignore"/>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- name, info -->
|
||||
<template name="DiagnosticsDrmCard">
|
||||
<GroupBox>
|
||||
<div gap="8">
|
||||
<sprite width="24" height="24" src_builtin="@/dashboard/gpu.svg"/>
|
||||
<label text="${name}"/>
|
||||
</div>
|
||||
<label text="${info}"/>
|
||||
</GroupBox>
|
||||
</template>
|
||||
|
||||
<template name="StatRow">
|
||||
<div flex_direction="row" justify_content="space_between" align_items="center" gap="12" width="100%">
|
||||
<label weight="bold" translation="${translation}" />
|
||||
@@ -77,6 +98,7 @@
|
||||
<Tab macro="tab_style" name="misc" translation="APP_SETTINGS.MISC" sprite_src_builtin="@/dashboard/blocks.svg" />
|
||||
<Tab macro="tab_style" name="autostart_apps" translation="APP_SETTINGS.AUTOSTART_APPS" sprite_src_builtin="@/dashboard/apps.svg" />
|
||||
<Tab macro="tab_style" name="statistics" translation="APP_SETTINGS.STATISTICS" sprite_src_builtin="@/dashboard/barchart.svg" />
|
||||
<Tab macro="tab_style" name="diagnostics" translation="APP_SETTINGS.DIAGNOSTICS.TITLE" sprite_src_builtin="@/dashboard/diagnostics.svg" />
|
||||
<Tab macro="tab_style" name="troubleshooting" translation="APP_SETTINGS.TROUBLESHOOTING" sprite_src_builtin="@/dashboard/cpu.svg" />
|
||||
</Tabs>
|
||||
<div flex_wrap="wrap" justify_content="stretch" gap="4" id="settings_root" width="100%">
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
|
||||
<!-- device_name, device_icon -->
|
||||
<template name="DeviceSlider">
|
||||
<GroupBox>
|
||||
<GroupBox id="rect">
|
||||
<div width="100%" align_items="center" justify_content="center" gap="8">
|
||||
<sprite src_builtin="${device_icon}" width="16" height="16" color="on_background" />
|
||||
<sprite src_builtin="${device_icon}" width="24" height="24" color="on_background" />
|
||||
<label text="${device_name}" margin_right="8" size="12" weight="bold" color="on_background" />
|
||||
</div>
|
||||
<div width="100%" align_items="center">
|
||||
<CheckBox id="checkbox" />
|
||||
<Button sprite_src_builtin="${volume_icon}" id="btn_mute" width="32" />
|
||||
<Slider id="slider" flex_grow="1" height="16" min_value="0" max_value="150" margin_left="8" />
|
||||
<CheckBox id="checkbox" tooltip="AUDIO.SWITCH_TO_THIS_DEVICE" />
|
||||
<Slider id="slider" flex_grow="1" height="16" min_value="0" max_value="150" margin_left="8" margin_right="8" />
|
||||
<Button sprite_src_builtin="${volume_icon}" id="btn_mute" width="32" height="32" />
|
||||
</div>
|
||||
</GroupBox>
|
||||
</template>
|
||||
@@ -47,8 +47,17 @@
|
||||
|
||||
<elements>
|
||||
<div flex_direction="column" gap="4">
|
||||
<div id="devices" flex_direction="column" gap="4" max_height="400" overflow_y="scroll">
|
||||
<!--
|
||||
this div shouldn't be technically needed, but there's a bug in
|
||||
the newest taffy version (or we're doing something wrong),
|
||||
causing the scrollbar to show up, even if it's not overflowing.
|
||||
OVERFLOW_START_THRESHOLD_UNITS is already set to 3, but it's not
|
||||
sufficient enough in this case?? (margin_bottom="4" fixes that).
|
||||
-->
|
||||
<div width="100%" flex_direction="column" max_height="400" overflow_y="scroll">
|
||||
<div id="devices" width="100%" flex_direction="column" gap="4" margin_bottom="4">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- bottom buttons -->
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</template>
|
||||
|
||||
<elements>
|
||||
<div flex_direction="column" gap="8" width="100%" align_items="center">
|
||||
<div flex_direction="column" gap="8" width="100%" align_items="center" overflow_y="scroll">
|
||||
<div id="list_parent" flex_grow="1" flex_wrap="wrap" flex_direction="row" gap="4" width="100%" justify_content="center"></div>
|
||||
</div>
|
||||
</elements>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<layout version="2">
|
||||
<template name="DialogBoxButton">
|
||||
<Button id="btn" translation="CLOSE_WINDOW" align_self="start" sprite_src_builtin="${icon}"/>
|
||||
<Button id="btn" translation="CLOSE_WINDOW" align_self="start" sprite_src_builtin="${icon}"
|
||||
text_size="16"
|
||||
padding_left="16" padding_right="16"
|
||||
padding_top="8" padding_bottom="8"/>
|
||||
</template>
|
||||
|
||||
<elements>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<rectangle position="absolute" width="100%" height="3" round="4" color="highlight(opacity-0.1)" />
|
||||
|
||||
<!-- Top bar contents -->
|
||||
<div gap="16" align_items="center">
|
||||
<div gap="16" align_items="center" id="top_bar_contents">
|
||||
<!-- Back button -->
|
||||
<Button id="but_back" width="48" height="48" color="on_background_contrast(transparent)" border_color="on_background_contrast(transparent)">
|
||||
<sprite src_builtin="@/dashboard/back.svg" width="24" height="24" color="on_background_contrast" />
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<layout version="2">
|
||||
<elements>
|
||||
<div id="list" gap="8" flex_wrap="wrap" align_self="baseline">
|
||||
<!-- filled-in at runtime -->
|
||||
<div flex_direction="column" overflow_y="scroll">
|
||||
<div id="list" gap="8" flex_direction="row" flex_wrap="wrap">
|
||||
<!-- filled-in at runtime -->
|
||||
</div>
|
||||
</div>
|
||||
</elements>
|
||||
</layout>
|
||||
@@ -1,11 +1,11 @@
|
||||
<layout version="2">
|
||||
<elements>
|
||||
<div flex_direction="column" gap="8" width="100%" align_items="center">
|
||||
<div flex_direction="column" gap="8" width="100%" >
|
||||
<div flex_direction="row" gap="4" align_self="end">
|
||||
<Button id="btn_refresh" tooltip="RELOAD_FROM_DISK" width="32" height="32" sprite_src_builtin="@/dashboard/refresh.svg" />
|
||||
<Button id="btn_download_skymaps" height="32" translation="APP_SETTINGS.BROWSE_ONLINE_CATALOG" sprite_src_builtin="@/dashboard/download.svg"/>
|
||||
</div>
|
||||
<div id="list_parent" gap="8" flex_direction="row" flex_wrap="wrap" />
|
||||
<div id="list_parent" gap="8" flex_direction="row" flex_wrap="wrap" overflow_y="scroll" justify_content="center"/>
|
||||
</div>
|
||||
</elements>
|
||||
</layout>
|
||||
@@ -15,9 +15,12 @@
|
||||
flex_direction="column"
|
||||
gap="4"
|
||||
width="256"
|
||||
max_width="450"
|
||||
height="200"
|
||||
align_items="center"
|
||||
flex_grow="1"
|
||||
align_self="start">
|
||||
<image id="image_preview" width="100%" height="128" round="6">
|
||||
<image id="image_preview" width="100%" aspect_ratio="2.0" round="6">
|
||||
<!-- new_pass is required, because we need to render rectangles at the top of the image. Sorry. -->
|
||||
<div new_pass="1" id="resolution_pips" gap="4" margin="6"/>
|
||||
</image>
|
||||
@@ -34,7 +37,7 @@
|
||||
-->
|
||||
<template name="ResolutionPip">
|
||||
<rectangle color="${color}" padding_left="4" padding_right="4" padding_top="2" padding_bottom="2" round="3" align_self="start">
|
||||
<label text="${text}" weight="bold" size="12" color="#FFFFFF" shadow="#000000" shadow_x="2" shadow_y="2"/>
|
||||
<label text="${text}" weight="bold" size="12" color="#FFFFFF" shadow="#000000" shadow_x="2" shadow_y="2" parent_color="ignore"/>
|
||||
</rectangle>
|
||||
</template>
|
||||
</layout>
|
||||
@@ -97,7 +97,10 @@
|
||||
"RIGHT": "Rechts",
|
||||
"COMPONENT": "Aktivierungsart",
|
||||
"SUBPATH": "Bedienelement",
|
||||
"THRESHOLD": "Aktivieren bei Überschreiten des oberen Wertes\nDeaktivieren bei Unterschreiten des unteren Wertes"
|
||||
"THRESHOLD": "Aktivieren bei Überschreiten des oberen Wertes\nDeaktivieren bei Unterschreiten des unteren Wertes",
|
||||
"CHORD": "Chord",
|
||||
"CHORD_ADD_INPUT": "Chord-Input hinzufügen",
|
||||
"CHORD_INPUT_HAND": "Eingabehand"
|
||||
},
|
||||
"BROWSE_ONLINE_CATALOG": "Online-Katalog durchsuchen...",
|
||||
"BROWSE_SKYMAPS": "Skymaps durchsuchen",
|
||||
@@ -194,8 +197,6 @@
|
||||
"SAVE_PLAYSPACE_CENTER_HELP": "Nutze aktuellen Playspace für die Playspace-Reset-Belegung.",
|
||||
"RESET_PLAYSPACE_CENTER": "Playspace-Zentrum zurücksetzen",
|
||||
"RESET_PLAYSPACE_CENTER_HELP": "Vorheriges Playspace-Zentrum zurücksetzen.",
|
||||
"RESTART_SOFTWARE": "Software neu starten",
|
||||
"RESTART_SOFTWARE_HELP": "Einstellungen übernehmen, die einen Neustart erfordern",
|
||||
"ROUND_MULTIPLIER": "Abrundung der UI-Kanten",
|
||||
"SCREEN_RENDER_DOWN": "Bildschirm mit niedrigerer Auflösung rendern",
|
||||
"SCREEN_RENDER_DOWN_HELP": "Hilft gegen Treppeneffekte bei hochauflösenden Bildschirmen",
|
||||
@@ -205,7 +206,6 @@
|
||||
"SNAP_ANGLE_DEG_HELP": "Gegriffene Fenster rasten auf der Nick- und Rollachse in diesem Winkel ein.",
|
||||
"ENABLE_WATCH": "Handgelenk-Uhr aktivieren",
|
||||
"SETS_ON_WATCH": "Sets auf Handgelenk-Uhr",
|
||||
"SHOW_WELCOME_SCREEN": "Willkommensbildschirm anzeigen",
|
||||
"SKYBOX": "Skymap",
|
||||
"SKYMAP_ALREADY_DOWNLOADED": "Diese Skymap ist bereits heruntergeladen. Wähle eine Aktion aus.",
|
||||
"SPACE_DRAG": "Playspace",
|
||||
@@ -247,7 +247,23 @@
|
||||
"STATISTICS": "Statistiken",
|
||||
"ROTATIONS": "HMD-Rotationen",
|
||||
"SESSION_TIME": "Sitzungszeit",
|
||||
"LENS_SEPARATION_MM": "Linsenabstand (mm)"
|
||||
"LENS_SEPARATION_MM": "Linsenabstand (mm)",
|
||||
"MIDDLE_GRABS_SINGLE": "Mittelklick-Modifier greift einzeln",
|
||||
"MIDDLE_GRABS_SINGLE_HELP": "Greifen mit dem Mittelklick-Modifier (lila Laser) \ngreift einzelne Overlays statt des gesamten Sets",
|
||||
"DIAGNOSTICS_REASON": {
|
||||
"PACTL": "Erforderlich für Lautsprecher-/Mikrofonsteuerung und das Wechseln der Soundkarte",
|
||||
"CAGE": "Empfohlen bei Inkompatibilitäten mit einigen X11-Anwendungen (Kompatibilitätsmodus)",
|
||||
"XWAYLAND_SATELLITE": "Empfohlen, wenn X11 (Xwayland) Anwendungen im nativen Modus ausgeführt werden sollen",
|
||||
"XDG_OPEN": "Erforderlich zum Öffnen von URLs oder Steam-Spielen"
|
||||
},
|
||||
"DIAGNOSTICS": {
|
||||
"CPU_ACCESSIBLE_VRAM": "CPU-zugänglicher VRAM: {NUM} MiB",
|
||||
"TITLE": "Diagnose",
|
||||
"ENABLE_REBAR": "Aktiviere resizable bar in den BIOS-Einstellungen für optimale VR-Performance"
|
||||
},
|
||||
"TUTORIAL": "Tutorial",
|
||||
"RESTART_WAYVR": "WayVR neu starten",
|
||||
"CLOSE_WAYVR": "WayVR schließen"
|
||||
},
|
||||
"APPLICATION_LAUNCHER": "Programmstarter",
|
||||
"APPLICATION_STARTED": "Anwendung gestartet",
|
||||
@@ -264,7 +280,8 @@
|
||||
"SETTINGS": "Audio-Einstellungen",
|
||||
"SPEAKERS": "Lautsprecher",
|
||||
"SPEAKERS_SET_SUCCESSFULLY": "Lautsprecher erfolgreich eingestellt",
|
||||
"VOLUME": "Lautstärke"
|
||||
"VOLUME": "Lautstärke",
|
||||
"SWITCH_TO_THIS_DEVICE": "Zu diesem Gerät wechseln"
|
||||
},
|
||||
"CLOSE_WINDOW": "Fenster schließen",
|
||||
"CREATION_DATE": "Erstellungsdatum",
|
||||
@@ -349,5 +366,15 @@
|
||||
"ADD_TO_FAVOURITES": "Zu angehefteten Anwendungen hinzufügen",
|
||||
"ADD_TO_FAVOURITES_HINT": "Voreinstellung speichern und an die App-Schublade anheften",
|
||||
"REMOVE_FROM_FAVOURITES": "Aus angehefteten Anwendungen entfernen",
|
||||
"EMPTY": "Leer"
|
||||
"EMPTY": "Leer",
|
||||
"INSTALLED": "Installiert",
|
||||
"NOT_INSTALLED": "Nicht installiert",
|
||||
"POWER_OPTIONS": {
|
||||
"TITLE": "Energieoptionen",
|
||||
"SHUTDOWN": "Herunterfahren",
|
||||
"SUSPEND": "Ruhezustand",
|
||||
"SHUTDOWN_ARE_YOU_SURE": "Bist du sicher, dass du den Computer ausschalten willst?"
|
||||
},
|
||||
"YES": "Ja",
|
||||
"NO": "Nein"
|
||||
}
|
||||
@@ -74,6 +74,9 @@ APP_SETTINGS.BINDINGS.ACTION.MOVE_MOUSE,Move mouse (if off by default),Label for
|
||||
APP_SETTINGS.BINDINGS.ACTION.SCROLL,Scroll*,Label for the scroll action row. Maps controller input to scrolling. Leave the '*' at the end.
|
||||
APP_SETTINGS.BINDINGS.LEFT,Left,Label in the bindings editor. Refers to the left controller/hand configuration.
|
||||
APP_SETTINGS.BINDINGS.RIGHT,Right,Label in the bindings editor. Refers to the right controller/hand configuration.
|
||||
APP_SETTINGS.BINDINGS.CHORD,Chord,Label for a group of controller inputs that must all be active together. As in Input Chord
|
||||
APP_SETTINGS.BINDINGS.CHORD_ADD_INPUT,Add chord input,"Button in the controller bindings editor that adds another physical input to a chord. ""Chord"" was previously translated as ""${APP_SETTINGS.BINDINGS.CHORD}""."
|
||||
APP_SETTINGS.BINDINGS.CHORD_INPUT_HAND,Input hand,"Selector for the hand to which we will be binding the input action to."
|
||||
APP_SETTINGS.BINDINGS.COMPONENT,Actuation type,"Tooltip for the component dropdown in the bindings editor. Asks user to select what type of actuation (click, touch, axis, etc.)."
|
||||
APP_SETTINGS.BINDINGS.SUBPATH,Actuating control,Tooltip for the subpath dropdown in the bindings editor. Asks user to select which controller button/stick/trackpad to bind.
|
||||
APP_SETTINGS.BINDINGS.THRESHOLD,"Activate when above upper value
|
||||
@@ -119,6 +122,13 @@ controllers are unavailable.
|
||||
Left pinch is grab, right is click.",Tooltip for the handsfree mode dropdown.
|
||||
APP_SETTINGS.HANDSFREE_ALT_TAB,App switcher aim,Dropdown/setting in Settings > Features tab. Controls how the alt-tab app switcher is aimed.
|
||||
APP_SETTINGS.HANDSFREE_ALT_TAB_HELP,"Hold Super+X, aim at the desired window, then release to switch focus.",Tooltip explaining the handsfree alt-tab behavior.
|
||||
APP_SETTINGS.DIAGNOSTICS.CPU_ACCESSIBLE_VRAM,"CPU-accessible VRAM: {NUM} MiB","Amount of VRAM accessible by the CPU. For example: 256/4096"
|
||||
APP_SETTINGS.DIAGNOSTICS.TITLE,"Diagnostics","Tab name for the hardware/software check panel"
|
||||
APP_SETTINGS.DIAGNOSTICS.ENABLE_REBAR,"Enable Resizable BAR in the BIOS settings for optimal VR performance","Info shown if resizable bar is disabled in the BIOS. Leave [resizable bar] translation as-is."
|
||||
APP_SETTINGS.DIAGNOSTICS_REASON.PACTL,"Required for speaker/microphone control and audio card switching","Reason for why the given software is recommended"
|
||||
APP_SETTINGS.DIAGNOSTICS_REASON.CAGE,"Recommended in case if there's an incompatibility with some X11 applications (Compatibility mode)","Reason for why the given software is required or recommended"
|
||||
APP_SETTINGS.DIAGNOSTICS_REASON.XWAYLAND_SATELLITE,"Recommended in case if you want to run X11 (Xwayland) applications in native mode","Reason for why the given software is required or recommended"
|
||||
APP_SETTINGS.DIAGNOSTICS_REASON.XDG_OPEN,"Required for opening URLs or Steam games","Reason for why the given software is required or recommended"
|
||||
APP_SETTINGS.HIDE_GRAB_HELP,Hide grab help,Checkbox label in Settings > Look & Feel tab. Hides the grab help overlay/text.
|
||||
APP_SETTINGS.HIDE_USERNAME,Hide username,Checkbox label in Settings > Look & Feel tab. Hides the user's username from the UI.
|
||||
APP_SETTINGS.INPUT_EMULATION_METHOD,Input emulation method,"Dropdown label in Settings > Miscellaneous tab. Selects how WayVR emulates mouse/keyboard input (uinput, Wayland virtual, none)."
|
||||
@@ -143,6 +153,9 @@ APP_SETTINGS.LEFT_HANDED_MOUSE_HELP,Use this if mouse buttons are swapped,Toolti
|
||||
APP_SETTINGS.LONG_PRESS_DURATION,Long press duration,Setting in Settings > Features tab. Controls how long a press must be held to register as a long press.
|
||||
APP_SETTINGS.LOOK_AND_FEEL,Look & Feel,"Category header in Settings. Groups visual/customization settings like language, opacity, animation speed, color palettes."
|
||||
APP_SETTINGS.MISC,Miscellaneous,"Category header in Settings. Groups troubleshooting and system-level settings like capture method, input emulation, xwayland."
|
||||
APP_SETTINGS.MIDDLE_GRABS_SINGLE,"Middle-click modifier grabs individual","Checkbox label in Settings > Controls tab. When enabled, grabbing with the middle modifier will always grab individual objects, never the entire set of objects. ""Middle-click modifier"" was previously translated as ""${APP_SETTINGS.BINDINGS.ACTION.CLICK_MODIFIER_MIDDLE}"""
|
||||
APP_SETTINGS.MIDDLE_GRABS_SINGLE_HELP,"Grabbing with the middle-click modifier (purple laser)
|
||||
will grab individual overlays, not the entire set","Tooltip for the middle modifier grabs single overlays checkbox. ""Middle-click modifier"" was previously translated as ""${APP_SETTINGS.BINDINGS.ACTION.CLICK_MODIFIER_MIDDLE}"""
|
||||
APP_SETTINGS.NOTIFICATIONS_ENABLED,Enable notifications,Checkbox label in Settings > Features tab. Enables or disables system notifications within WayVR.
|
||||
APP_SETTINGS.NOTIFICATIONS_SOUND_ENABLED,Notification sounds,Checkbox label in Settings > Features tab. Enables or disables sound effects for notifications.
|
||||
APP_SETTINGS.NOT_SUPPORTED,Not supported,Label shown in the space drag/physics settings when a feature (like gravity) is not supported by the current backend.
|
||||
@@ -195,8 +208,8 @@ APP_SETTINGS.SAVE_PLAYSPACE_CENTER,Save playspace center,"Button in Settings > S
|
||||
APP_SETTINGS.SAVE_PLAYSPACE_CENTER_HELP,Use current playspace for reset playspace binding.,"Tooltip for the Save playspace center button. ""Playspace"" was previously translated as ""${DEFINITIONS.PLAYSPACE}""."
|
||||
APP_SETTINGS.RESET_PLAYSPACE_CENTER,Reset playspace center,"Button in Settings > Space Drag tab. Clears the previously saved playspace center. ""Playspace"" was previously translated as ""${DEFINITIONS.PLAYSPACE}""."
|
||||
APP_SETTINGS.RESET_PLAYSPACE_CENTER_HELP,Clear previously set playspace center.,"Tooltip for the Reset playspace center button. ""Playspace"" was previously translated as ""${DEFINITIONS.PLAYSPACE}""."
|
||||
APP_SETTINGS.RESTART_SOFTWARE,Restart software,Button in Settings > Troubleshooting tab. Restarts the WayVR software to apply settings that require a restart.
|
||||
APP_SETTINGS.RESTART_SOFTWARE_HELP,Apply settings that require a restart,Tooltip for the Restart software button.
|
||||
APP_SETTINGS.RESTART_WAYVR,Restart WayVR,Button in Settings which restarts WayVR
|
||||
APP_SETTINGS.CLOSE_WAYVR,Close WayVR,Button in Settings which closes WayVR
|
||||
APP_SETTINGS.ROUND_MULTIPLIER,UI Edge roundness,Slider in Settings > Look & Feel tab. Controls how rounded the corners of UI elements appear (range 0.1-5.0).
|
||||
APP_SETTINGS.SCREEN_RENDER_DOWN,Render screen at lower resolution,Checkbox in Settings > Miscellaneous tab. Renders the captured desktop screen at a lower resolution to improve performance or reduce aliasing.
|
||||
APP_SETTINGS.SCREEN_RENDER_DOWN_HELP,Helps with aliasing on high-res screens,Tooltip for the lower-resolution screen render setting.
|
||||
@@ -206,7 +219,6 @@ APP_SETTINGS.SNAP_ANGLE_DEG,Window pitch & roll snap angle,Slider in Settings >
|
||||
APP_SETTINGS.SNAP_ANGLE_DEG_HELP,Grabbed windows snap to this angle on pitch and roll axis.,Tooltip for the snap angle setting.
|
||||
APP_SETTINGS.ENABLE_WATCH,Enable watch,"Checkbox in Settings > Look & Feel tab. Enables the wrist watch UI element. ""Watch"" was previously translated as ""${DEFINITIONS.WATCH}""."
|
||||
APP_SETTINGS.SETS_ON_WATCH,Sets on watch,"Checkbox in Settings > Look & Feel tab. Shows overlay sets on the wrist watch display. ""Watch"" was previously translated as ""${DEFINITIONS.WATCH}"". ""Set"" was previously translated as ""${DEFINITIONS.SET}""."
|
||||
APP_SETTINGS.SHOW_WELCOME_SCREEN,Show welcome screen,Checkbox in Settings > Features tab. Controls whether the welcome/tutorial screen is shown on first launch.
|
||||
APP_SETTINGS.SKYBOX,Skymap,"Category header in Settings > Skymap tab. Groups skymap-related settings. ""Skymap"" was previously translated as ""${DEFINITIONS.SKYMAP}""."
|
||||
APP_SETTINGS.SKYMAP_ALREADY_DOWNLOADED,This skymap is already downloaded. Select desired action.,"Message shown in a dialog when attempting to download a skymap that is already present locally. ""Skymap"" was previously translated as ""${DEFINITIONS.SKYMAP}""."
|
||||
APP_SETTINGS.SPACE_DRAG,Playspace,"Category header in Settings. Groups playspace drag/physics settings including reset buttons and gravity. ""Playspace"" was previously translated as ""${DEFINITIONS.PLAYSPACE}""."
|
||||
@@ -247,6 +259,7 @@ APP_SETTINGS.WHISPER_MODEL,Speech-to-text model,Dropdown label in Settings > Fea
|
||||
APP_SETTINGS.WHISPER_MODEL_HELP,"Whisper model to use.
|
||||
Larger models produce more accurate
|
||||
results, but consume more VRAM.",Tooltip for the speech-to-text model dropdown.
|
||||
APP_SETTINGS.TUTORIAL,Tutorial,Tooltip for tutorial button in the main screen.
|
||||
APP_SETTINGS.GRID_OPACITY,Floor grid opacity,Slider in Settings > Skybox tab. Controls the opacity of the floor grid when skybox is enabled.
|
||||
APP_SETTINGS.GRID_OPACITY_HELP,Opacity of the floor grid when the skymap is enabled,"Tooltip for the floor grid opacity slider. ""Skymap"" was previously translated as ""${DEFINITIONS.SKYMAP}""."
|
||||
APP_SETTINGS.XWAYLAND_BY_DEFAULT,Run apps in Compatibility mode by default,Checkbox in Settings > Miscellaneous tab. Launches apps in Cage/compatibility mode by default.
|
||||
@@ -255,6 +268,7 @@ APP_SETTINGS.CANCEL,Cancel,Button label in the controller bindings editor popup
|
||||
APPLICATION_LAUNCHER,Application launcher,App launcher configuration popup title - appears as the label for the app launch/options dialog.
|
||||
APPLICATION_STARTED,Application started,Toast notification shown after a successful app launch.
|
||||
APPLICATIONS,Applications,Tab label in the dashboard sidebar for the Apps tab.
|
||||
AUDIO.SWITCH_TO_THIS_DEVICE,Switch to this device,Button in the audio settings dialog
|
||||
AUDIO.AUTO_SWITCH_TO_VR_AUDIO,Auto-switch to VR audio,Tooltip on the auto-switch audio button in the audio settings popup.
|
||||
AUDIO.CARDS,Cards,Button label in the audio settings popup for selecting audio card profiles.
|
||||
AUDIO.FAILED_TO_SWITCH_MICROPHONE,Failed to switch microphone,Error toast shown when switching to a VR microphone fails.
|
||||
@@ -322,6 +336,10 @@ MODIFICATION_DATE,Modification date,Column label in the remote skymap downloader
|
||||
MONADO_RUNTIME,Monado runtime,Tab label in the dashboard sidebar for the Monado runtime tab.
|
||||
LOADING,Loading...,Label in the loading spinner template used across the UI.
|
||||
POPUP_ADD_DISPLAY.RESOLUTION,Resolution,Label for the resolution field in the add display popup.
|
||||
POWER_OPTIONS.TITLE,"Power options","A button which allows the user to perform a specific power action"
|
||||
POWER_OPTIONS.SHUTDOWN,"Shut down","One of the power options: shuts down the computer"
|
||||
POWER_OPTIONS.SUSPEND,"Suspend","One of the power options: suspends the computer"
|
||||
POWER_OPTIONS.SHUTDOWN_ARE_YOU_SURE,"Are you sure you want to turn off your computer?","Question dialog box before shutting down the computer"
|
||||
PROCESS.FORCE_KILL,Force-kill,Tooltip on the kill button in the running games list.
|
||||
PROCESS.STOP,Stop,Tooltip on the stop button in the running games list.
|
||||
PROCESS_LIST,Process list,Tab name and group box title in the Monado runtime process list section.
|
||||
@@ -341,3 +359,7 @@ ADD_TO_FAVOURITES,Add to pinned apps,"Label on the pin button in the app launche
|
||||
ADD_TO_FAVOURITES_HINT,Save preset and pin it to the app drawer,"Tooltip on the add to favourites button in the app launcher popup. ""Pinned apps"" was previously translated as ""${PINNED_APPS}""."
|
||||
REMOVE_FROM_FAVOURITES,Remove from pinned apps,"Label on the unpin section in the app launcher popup. ""Pinned apps"" was previously translated as ""${PINNED_APPS}""."
|
||||
EMPTY,Empty,Label shown in the pinned apps section when there are no pinned apps.
|
||||
INSTALLED,"Installed","Installation flag (installed/not installed)"
|
||||
NOT_INSTALLED,"Not installed","Installation flag (installed/not installed)"
|
||||
YES,"Yes","Just a simple 'Yes' message, button in the dialog box"
|
||||
NO,"No","Just a simple 'No' message, button in the dialog box"
|
||||
|
@@ -95,6 +95,9 @@
|
||||
},
|
||||
"LEFT": "Left",
|
||||
"RIGHT": "Right",
|
||||
"CHORD": "Chord",
|
||||
"CHORD_ADD_INPUT": "Add chord input",
|
||||
"CHORD_INPUT_HAND": "Input hand",
|
||||
"COMPONENT": "Actuation type",
|
||||
"SUBPATH": "Actuating control",
|
||||
"THRESHOLD": "Activate when above upper value\nDeactivate when below lower value"
|
||||
@@ -136,6 +139,17 @@
|
||||
"HANDSFREE_POINTER_HELP": "Input to use when motion\ncontrollers are unavailable.\nLeft pinch is grab, right is click.",
|
||||
"HANDSFREE_ALT_TAB": "App switcher aim",
|
||||
"HANDSFREE_ALT_TAB_HELP": "Hold Super+X, aim at the desired window, then release to switch focus.",
|
||||
"DIAGNOSTICS": {
|
||||
"CPU_ACCESSIBLE_VRAM": "CPU-accessible VRAM: {NUM} MiB",
|
||||
"TITLE": "Diagnostics",
|
||||
"ENABLE_REBAR": "Enable Resizable BAR in the BIOS settings for optimal VR performance"
|
||||
},
|
||||
"DIAGNOSTICS_REASON": {
|
||||
"PACTL": "Required for speaker/microphone control and audio card switching",
|
||||
"CAGE": "Recommended in case if there's an incompatibility with some X11 applications (Compatibility mode)",
|
||||
"XWAYLAND_SATELLITE": "Recommended in case if you want to run X11 (Xwayland) applications in native mode",
|
||||
"XDG_OPEN": "Required for opening URLs or Steam games"
|
||||
},
|
||||
"HIDE_GRAB_HELP": "Hide grab help",
|
||||
"HIDE_USERNAME": "Hide username",
|
||||
"INPUT_EMULATION_METHOD": "Input emulation method",
|
||||
@@ -156,6 +170,8 @@
|
||||
"LONG_PRESS_DURATION": "Long press duration",
|
||||
"LOOK_AND_FEEL": "Look & Feel",
|
||||
"MISC": "Miscellaneous",
|
||||
"MIDDLE_GRABS_SINGLE": "Middle-click modifier grabs individual",
|
||||
"MIDDLE_GRABS_SINGLE_HELP": "Grabbing with the middle-click modifier (purple laser)\nwill grab individual overlays, not the entire set",
|
||||
"NOTIFICATIONS_ENABLED": "Enable notifications",
|
||||
"NOTIFICATIONS_SOUND_ENABLED": "Notification sounds",
|
||||
"NOT_SUPPORTED": "Not supported",
|
||||
@@ -195,8 +211,8 @@
|
||||
"SAVE_PLAYSPACE_CENTER_HELP": "Use current playspace for reset playspace binding.",
|
||||
"RESET_PLAYSPACE_CENTER": "Reset playspace center",
|
||||
"RESET_PLAYSPACE_CENTER_HELP": "Clear previously set playspace center.",
|
||||
"RESTART_SOFTWARE": "Restart software",
|
||||
"RESTART_SOFTWARE_HELP": "Apply settings that require a restart",
|
||||
"RESTART_WAYVR": "Restart WayVR",
|
||||
"CLOSE_WAYVR": "Close WayVR",
|
||||
"ROUND_MULTIPLIER": "UI Edge roundness",
|
||||
"SCREEN_RENDER_DOWN": "Render screen at lower resolution",
|
||||
"SCREEN_RENDER_DOWN_HELP": "Helps with aliasing on high-res screens",
|
||||
@@ -206,7 +222,6 @@
|
||||
"SNAP_ANGLE_DEG_HELP": "Grabbed windows snap to this angle on pitch and roll axis.",
|
||||
"ENABLE_WATCH": "Enable watch",
|
||||
"SETS_ON_WATCH": "Sets on watch",
|
||||
"SHOW_WELCOME_SCREEN": "Show welcome screen",
|
||||
"SKYBOX": "Skymap",
|
||||
"SKYMAP_ALREADY_DOWNLOADED": "This skymap is already downloaded. Select desired action.",
|
||||
"SPACE_DRAG": "Playspace",
|
||||
@@ -243,6 +258,7 @@
|
||||
},
|
||||
"WHISPER_MODEL": "Speech-to-text model",
|
||||
"WHISPER_MODEL_HELP": "Whisper model to use.\nLarger models produce more accurate\nresults, but consume more VRAM.",
|
||||
"TUTORIAL": "Tutorial",
|
||||
"GRID_OPACITY": "Floor grid opacity",
|
||||
"GRID_OPACITY_HELP": "Opacity of the floor grid when the skymap is enabled",
|
||||
"XWAYLAND_BY_DEFAULT": "Run apps in Compatibility mode by default",
|
||||
@@ -253,6 +269,7 @@
|
||||
"APPLICATION_STARTED": "Application started",
|
||||
"APPLICATIONS": "Applications",
|
||||
"AUDIO": {
|
||||
"SWITCH_TO_THIS_DEVICE": "Switch to this device",
|
||||
"AUTO_SWITCH_TO_VR_AUDIO": "Auto-switch to VR audio",
|
||||
"CARDS": "Cards",
|
||||
"FAILED_TO_SWITCH_MICROPHONE": "Failed to switch microphone",
|
||||
@@ -329,6 +346,12 @@
|
||||
"POPUP_ADD_DISPLAY": {
|
||||
"RESOLUTION": "Resolution"
|
||||
},
|
||||
"POWER_OPTIONS": {
|
||||
"TITLE": "Power options",
|
||||
"SHUTDOWN": "Shut down",
|
||||
"SUSPEND": "Suspend",
|
||||
"SHUTDOWN_ARE_YOU_SURE": "Are you sure you want to turn off your computer?"
|
||||
},
|
||||
"PROCESS": {
|
||||
"FORCE_KILL": "Force-kill",
|
||||
"STOP": "Stop"
|
||||
@@ -349,5 +372,9 @@
|
||||
"ADD_TO_FAVOURITES": "Add to pinned apps",
|
||||
"ADD_TO_FAVOURITES_HINT": "Save preset and pin it to the app drawer",
|
||||
"REMOVE_FROM_FAVOURITES": "Remove from pinned apps",
|
||||
"EMPTY": "Empty"
|
||||
"EMPTY": "Empty",
|
||||
"INSTALLED": "Installed",
|
||||
"NOT_INSTALLED": "Not installed",
|
||||
"YES": "Yes",
|
||||
"NO": "No"
|
||||
}
|
||||
@@ -97,7 +97,10 @@
|
||||
"RIGHT": "Derecha",
|
||||
"COMPONENT": "Tipo de accionamiento",
|
||||
"SUBPATH": "Control a accionar",
|
||||
"THRESHOLD": "Activar al superar el valor superior\nDesactivar al caer bajo el valor inferior"
|
||||
"THRESHOLD": "Activar al superar el valor superior\nDesactivar al caer bajo el valor inferior",
|
||||
"CHORD": "Acorde",
|
||||
"CHORD_ADD_INPUT": "Añadir entrada al acorde",
|
||||
"CHORD_INPUT_HAND": "Mano de entrada"
|
||||
},
|
||||
"BROWSE_ONLINE_CATALOG": "Explorar catálogo en línea...",
|
||||
"BROWSE_SKYMAPS": "Explorar skymaps",
|
||||
@@ -194,8 +197,6 @@
|
||||
"SAVE_PLAYSPACE_CENTER_HELP": "Usa el espacio de juego actual para el comando de reiniciar espacio de juego.",
|
||||
"RESET_PLAYSPACE_CENTER": "Reiniciar centro del espacio de juego",
|
||||
"RESET_PLAYSPACE_CENTER_HELP": "Restablece el centro del espacio de juego definido previamente.",
|
||||
"RESTART_SOFTWARE": "Reiniciar software",
|
||||
"RESTART_SOFTWARE_HELP": "Aplicar ajustes que requieren reiniciar",
|
||||
"ROUND_MULTIPLIER": "Redondeo de bordes de la UI",
|
||||
"SCREEN_RENDER_DOWN": "Renderizar pantalla a resolución menor",
|
||||
"SCREEN_RENDER_DOWN_HELP": "Ayuda con el aliasing en pantallas de alta resolución",
|
||||
@@ -205,7 +206,6 @@
|
||||
"SNAP_ANGLE_DEG_HELP": "Las ventanas agarradas se ajustan a este ángulo en los ejes de cabeceo y balanceo.",
|
||||
"ENABLE_WATCH": "Habilitar reloj",
|
||||
"SETS_ON_WATCH": "Sets en el reloj",
|
||||
"SHOW_WELCOME_SCREEN": "Mostrar pantalla de bienvenida",
|
||||
"SKYBOX": "Skymap",
|
||||
"SKYMAP_ALREADY_DOWNLOADED": "Este skymap ya está descargado. Selecciona la acción deseada.",
|
||||
"SPACE_DRAG": "Espacio de juego",
|
||||
@@ -247,7 +247,23 @@
|
||||
"STATISTICS": "Estadísticas",
|
||||
"ROTATIONS": "Rotaciones del HMD",
|
||||
"SESSION_TIME": "Tiempo de sesión",
|
||||
"LENS_SEPARATION_MM": "Separación de lentes (mm)"
|
||||
"LENS_SEPARATION_MM": "Separación de lentes (mm)",
|
||||
"MIDDLE_GRABS_SINGLE": "El modificador de clic central agarra objetos individuales",
|
||||
"MIDDLE_GRABS_SINGLE_HELP": "Al agarrar con el modificador de clic central (láser púrpura),\nse agarrarán overlays individuales y no el set completo",
|
||||
"DIAGNOSTICS_REASON": {
|
||||
"PACTL": "Necesario para el control de altavoces/micrófono y el cambio de tarjeta de audio",
|
||||
"CAGE": "Recomendado en caso de incompatibilidad con algunas aplicaciones X11 (Modo de compatibilidad)",
|
||||
"XWAYLAND_SATELLITE": "Recomendado si quieres ejecutar aplicaciones X11 (Xwayland) en modo nativo",
|
||||
"XDG_OPEN": "Necesario para abrir URLs o juegos de Steam"
|
||||
},
|
||||
"DIAGNOSTICS": {
|
||||
"CPU_ACCESSIBLE_VRAM": "VRAM accesible por CPU: {NUM} MiB",
|
||||
"TITLE": "Diagnósticos",
|
||||
"ENABLE_REBAR": "Activa [resizable bar] en la configuración de la BIOS para un rendimiento óptimo de VR"
|
||||
},
|
||||
"TUTORIAL": "Tutorial",
|
||||
"RESTART_WAYVR": "Reiniciar WayVR",
|
||||
"CLOSE_WAYVR": "Cerrar WayVR"
|
||||
},
|
||||
"APPLICATION_LAUNCHER": "Lanzador de aplicaciones",
|
||||
"APPLICATION_STARTED": "Aplicación iniciada",
|
||||
@@ -264,7 +280,8 @@
|
||||
"SETTINGS": "Ajustes de audio",
|
||||
"SPEAKERS": "Altavoces",
|
||||
"SPEAKERS_SET_SUCCESSFULLY": "Altavoces configurados correctamente",
|
||||
"VOLUME": "Volumen"
|
||||
"VOLUME": "Volumen",
|
||||
"SWITCH_TO_THIS_DEVICE": "Cambiar a este dispositivo"
|
||||
},
|
||||
"CLOSE_WINDOW": "Cerrar ventana",
|
||||
"CREATION_DATE": "Fecha de creación",
|
||||
@@ -349,5 +366,15 @@
|
||||
"ADD_TO_FAVOURITES": "Añadir a aplicaciones fijadas",
|
||||
"ADD_TO_FAVOURITES_HINT": "Guardar ajuste y fijarlo al cajón de aplicaciones",
|
||||
"REMOVE_FROM_FAVOURITES": "Quitar de aplicaciones fijadas",
|
||||
"EMPTY": "Vacío"
|
||||
"EMPTY": "Vacío",
|
||||
"INSTALLED": "Instalado",
|
||||
"NOT_INSTALLED": "No instalado",
|
||||
"POWER_OPTIONS": {
|
||||
"TITLE": "Opciones de energía",
|
||||
"SHUTDOWN": "Apagar",
|
||||
"SUSPEND": "Suspender",
|
||||
"SHUTDOWN_ARE_YOU_SURE": "¿Seguro que quieres apagar el ordenador?"
|
||||
},
|
||||
"YES": "Sí",
|
||||
"NO": "No"
|
||||
}
|
||||
@@ -97,7 +97,10 @@
|
||||
"RIGHT": "Destro",
|
||||
"COMPONENT": "Tipo di attuazione",
|
||||
"SUBPATH": "Comando di attivazione",
|
||||
"THRESHOLD": "Attiva sopra il valore superiore\nDisattiva sotto il valore inferiore"
|
||||
"THRESHOLD": "Attiva sopra il valore superiore\nDisattiva sotto il valore inferiore",
|
||||
"CHORD": "Combinazione",
|
||||
"CHORD_ADD_INPUT": "Aggiungi input combinazione",
|
||||
"CHORD_INPUT_HAND": "Mano di input"
|
||||
},
|
||||
"BROWSE_ONLINE_CATALOG": "Sfoglia catalogo online...",
|
||||
"BROWSE_SKYMAPS": "Sfoglia skymap",
|
||||
@@ -194,8 +197,6 @@
|
||||
"SAVE_PLAYSPACE_CENTER_HELP": "Usa il playspace corrente per il comando di reset del playspace.",
|
||||
"RESET_PLAYSPACE_CENTER": "Resetta centro playspace",
|
||||
"RESET_PLAYSPACE_CENTER_HELP": "Rimuovi il centro del playspace impostato precedentemente.",
|
||||
"RESTART_SOFTWARE": "Riavvia software",
|
||||
"RESTART_SOFTWARE_HELP": "Applica le impostazioni che richiedono il riavvio",
|
||||
"ROUND_MULTIPLIER": "Arrotondamento bordi UI",
|
||||
"SCREEN_RENDER_DOWN": "Rendering schermo a risoluzione inferiore",
|
||||
"SCREEN_RENDER_DOWN_HELP": "Riduce l'effetto aliasing sugli schermi ad alta risoluzione",
|
||||
@@ -205,7 +206,6 @@
|
||||
"SNAP_ANGLE_DEG_HELP": "Le finestre afferrate si allineano a questo angolo sugli assi di beccheggio e rollio.",
|
||||
"ENABLE_WATCH": "Attiva orologio",
|
||||
"SETS_ON_WATCH": "Set sull'orologio",
|
||||
"SHOW_WELCOME_SCREEN": "Mostra schermata di benvenuto",
|
||||
"SKYBOX": "Skymap",
|
||||
"SKYMAP_ALREADY_DOWNLOADED": "Questo skymap è già stato scaricato. Seleziona l'azione desiderata.",
|
||||
"SPACE_DRAG": "Playspace",
|
||||
@@ -247,7 +247,23 @@
|
||||
"STATISTICS": "Statistiche",
|
||||
"ROTATIONS": "Rotazioni HMD",
|
||||
"SESSION_TIME": "Tempo sessione",
|
||||
"LENS_SEPARATION_MM": "Distanza lenti (mm)"
|
||||
"LENS_SEPARATION_MM": "Distanza lenti (mm)",
|
||||
"MIDDLE_GRABS_SINGLE": "Il modificatore click centrale afferra singoli oggetti",
|
||||
"MIDDLE_GRABS_SINGLE_HELP": "Afferrando con il modificatore click centrale (laser viola),\nsi afferrano i singoli overlay e non l'intero set",
|
||||
"DIAGNOSTICS_REASON": {
|
||||
"PACTL": "Necessario per il controllo di altoparlanti/microfono e il cambio della scheda audio",
|
||||
"CAGE": "Consigliato in caso di incompatibilità con alcune applicazioni X11 (Modalità compatibilità)",
|
||||
"XWAYLAND_SATELLITE": "Consigliato per eseguire applicazioni X11 (Xwayland) in modalità nativa",
|
||||
"XDG_OPEN": "Necessario per aprire URL o giochi Steam"
|
||||
},
|
||||
"DIAGNOSTICS": {
|
||||
"CPU_ACCESSIBLE_VRAM": "VRAM accessibile dalla CPU: {NUM} MiB",
|
||||
"TITLE": "Diagnostica",
|
||||
"ENABLE_REBAR": "Abilita Resizable BAR nelle impostazioni del BIOS per prestazioni VR ottimali"
|
||||
},
|
||||
"TUTORIAL": "Tutorial",
|
||||
"RESTART_WAYVR": "Riavvia WayVR",
|
||||
"CLOSE_WAYVR": "Chiudi WayVR"
|
||||
},
|
||||
"APPLICATION_LAUNCHER": "Avvio applicazioni",
|
||||
"APPLICATION_STARTED": "Applicazione avviata",
|
||||
@@ -264,7 +280,8 @@
|
||||
"SETTINGS": "Impostazioni audio",
|
||||
"SPEAKERS": "Altoparlanti",
|
||||
"SPEAKERS_SET_SUCCESSFULLY": "Altoparlanti impostati correttamente",
|
||||
"VOLUME": "Volume"
|
||||
"VOLUME": "Volume",
|
||||
"SWITCH_TO_THIS_DEVICE": "Passa a questo dispositivo"
|
||||
},
|
||||
"CLOSE_WINDOW": "Chiudi finestra",
|
||||
"CREATION_DATE": "Data di creazione",
|
||||
@@ -349,5 +366,15 @@
|
||||
"ADD_TO_FAVOURITES": "Aggiungi alle applicazioni fissate",
|
||||
"ADD_TO_FAVOURITES_HINT": "Salva il preset e fissalo al cassetto delle app",
|
||||
"REMOVE_FROM_FAVOURITES": "Rimuovi dalle app fissate",
|
||||
"EMPTY": "Vuoto"
|
||||
"EMPTY": "Vuoto",
|
||||
"INSTALLED": "Installato",
|
||||
"NOT_INSTALLED": "Non installato",
|
||||
"POWER_OPTIONS": {
|
||||
"TITLE": "Opzioni di alimentazione",
|
||||
"SHUTDOWN": "Spegni",
|
||||
"SUSPEND": "Sospendi",
|
||||
"SHUTDOWN_ARE_YOU_SURE": "Sei sicuro di voler spegnere il computer?"
|
||||
},
|
||||
"YES": "Sì",
|
||||
"NO": "No"
|
||||
}
|
||||
@@ -94,10 +94,13 @@
|
||||
"SCROLL": "スクロール*"
|
||||
},
|
||||
"LEFT": "左手",
|
||||
"RIGHT": "右",
|
||||
"RIGHT": "右手",
|
||||
"COMPONENT": "入力形式",
|
||||
"SUBPATH": "操作ボタン",
|
||||
"THRESHOLD": "上の値を超えると有効\n下の値を下回ると無効"
|
||||
"THRESHOLD": "上の値を超えると有効\n下の値を下回ると無効",
|
||||
"CHORD": "コード",
|
||||
"CHORD_ADD_INPUT": "コード入力の追加",
|
||||
"CHORD_INPUT_HAND": "入力側の手"
|
||||
},
|
||||
"BROWSE_ONLINE_CATALOG": "オンラインカタログを見る...",
|
||||
"BROWSE_SKYMAPS": "スカイマップを選択",
|
||||
@@ -194,8 +197,6 @@
|
||||
"SAVE_PLAYSPACE_CENTER_HELP": "現在のプレイスペースを、プレイスペースリセットのバインディングに使用する。",
|
||||
"RESET_PLAYSPACE_CENTER": "プレイスペースの中心をリセット",
|
||||
"RESET_PLAYSPACE_CENTER_HELP": "設定済みのプレイスペースの中心点をリセットする。",
|
||||
"RESTART_SOFTWARE": "ソフトウェアを再起動",
|
||||
"RESTART_SOFTWARE_HELP": "再起動が必要な設定を適用する",
|
||||
"ROUND_MULTIPLIER": "UIの角の丸み",
|
||||
"SCREEN_RENDER_DOWN": "低解像度で画面をレンダリング",
|
||||
"SCREEN_RENDER_DOWN_HELP": "高解像度ディスプレイでのジャギーを軽減する",
|
||||
@@ -205,7 +206,6 @@
|
||||
"SNAP_ANGLE_DEG_HELP": "掴んでいるウィンドウが、ピッチ軸とロール軸でこの角度にスナップします。",
|
||||
"ENABLE_WATCH": "ウォッチを有効にする",
|
||||
"SETS_ON_WATCH": "ウォッチにセットを表示",
|
||||
"SHOW_WELCOME_SCREEN": "ウェルカムスクリーンを表示",
|
||||
"SKYBOX": "スカイマップ",
|
||||
"SKYMAP_ALREADY_DOWNLOADED": "このスカイマップはすでにダウンロード済みです。操作を選択してください。",
|
||||
"SPACE_DRAG": "プレイスペース",
|
||||
@@ -247,7 +247,23 @@
|
||||
"STATISTICS": "統計",
|
||||
"ROTATIONS": "HMD回転数",
|
||||
"SESSION_TIME": "セッション時間",
|
||||
"LENS_SEPARATION_MM": "レンズ間隔 (mm)"
|
||||
"LENS_SEPARATION_MM": "レンズ間隔 (mm)",
|
||||
"MIDDLE_GRABS_SINGLE": "中クリック修飾キーで個別に掴む",
|
||||
"MIDDLE_GRABS_SINGLE_HELP": "中クリック修飾キー(紫色のレーザー)で掴むと、\nセット全体ではなく個別のオーバーレイを掴むことができる",
|
||||
"DIAGNOSTICS_REASON": {
|
||||
"PACTL": "スピーカー・マイクの制御およびオーディオカードの切り替えに必要",
|
||||
"CAGE": "一部のX11アプリで互換性問題が発生する場合に推奨(互換モード)",
|
||||
"XWAYLAND_SATELLITE": "X11 (Xwayland) アプリケーションをネイティブモードで動作させたい場合に推奨",
|
||||
"XDG_OPEN": "URLやSteamゲームの起動に必要"
|
||||
},
|
||||
"DIAGNOSTICS": {
|
||||
"CPU_ACCESSIBLE_VRAM": "CPUアクセス可能VRAM: {NUM} MiB",
|
||||
"TITLE": "診断",
|
||||
"ENABLE_REBAR": "VRパフォーマンスを最適化するため、BIOS設定でresizable BARを有効にして下さい"
|
||||
},
|
||||
"TUTORIAL": "チュートリアル",
|
||||
"RESTART_WAYVR": "WayVRを再起動",
|
||||
"CLOSE_WAYVR": "WayVRを閉じる"
|
||||
},
|
||||
"APPLICATION_LAUNCHER": "アプリランチャー",
|
||||
"APPLICATION_STARTED": "アプリが起動しました",
|
||||
@@ -264,7 +280,8 @@
|
||||
"SETTINGS": "オーディオ設定",
|
||||
"SPEAKERS": "スピーカー",
|
||||
"SPEAKERS_SET_SUCCESSFULLY": "スピーカーを設定完了",
|
||||
"VOLUME": "音量"
|
||||
"VOLUME": "音量",
|
||||
"SWITCH_TO_THIS_DEVICE": "このデバイスに切り替え"
|
||||
},
|
||||
"CLOSE_WINDOW": "ウィンドウを閉じる",
|
||||
"CREATION_DATE": "作成日",
|
||||
@@ -349,5 +366,15 @@
|
||||
"ADD_TO_FAVOURITES": "固定済みアプリに追加",
|
||||
"ADD_TO_FAVOURITES_HINT": "プリセットを保存し、アプリドロワーに固定する",
|
||||
"REMOVE_FROM_FAVOURITES": "固定済みアプリから外す",
|
||||
"EMPTY": "なし"
|
||||
"EMPTY": "なし",
|
||||
"INSTALLED": "インストール済み",
|
||||
"NOT_INSTALLED": "未インストール",
|
||||
"POWER_OPTIONS": {
|
||||
"TITLE": "電源オプション",
|
||||
"SHUTDOWN": "シャットダウン",
|
||||
"SUSPEND": "サスペンド",
|
||||
"SHUTDOWN_ARE_YOU_SURE": "コンピューターをシャットダウンしてもよろしいですか?"
|
||||
},
|
||||
"YES": "はい",
|
||||
"NO": "いいえ"
|
||||
}
|
||||
@@ -97,7 +97,10 @@
|
||||
"RIGHT": "Prawy",
|
||||
"COMPONENT": "Typ aktywacji",
|
||||
"SUBPATH": "Aktywujący sterownik",
|
||||
"THRESHOLD": "Aktywuj powyżej górnej wartości\nDeaktywuj poniżej dolnej wartości"
|
||||
"THRESHOLD": "Aktywuj powyżej górnej wartości\nDeaktywuj poniżej dolnej wartości",
|
||||
"CHORD": "Akord",
|
||||
"CHORD_ADD_INPUT": "Dodaj wejście do akordu",
|
||||
"CHORD_INPUT_HAND": "Ręka sterująca"
|
||||
},
|
||||
"BROWSE_ONLINE_CATALOG": "Przeglądaj katalog online...",
|
||||
"BROWSE_SKYMAPS": "Przeglądaj skymapy",
|
||||
@@ -194,8 +197,6 @@
|
||||
"SAVE_PLAYSPACE_CENTER_HELP": "Użyj aktualnej przestrzeni gry dla przypisania resetowania przestrzeni.",
|
||||
"RESET_PLAYSPACE_CENTER": "Zresetuj centrum przestrzeni gry",
|
||||
"RESET_PLAYSPACE_CENTER_HELP": "Resetuj aktualny środek przestrzeni gry.",
|
||||
"RESTART_SOFTWARE": "Uruchom ponownie program",
|
||||
"RESTART_SOFTWARE_HELP": "Zastosuj ustawienia wymagające restartu",
|
||||
"ROUND_MULTIPLIER": "Zaokrąglenie krawędzi UI",
|
||||
"SCREEN_RENDER_DOWN": "Renderuj ekran w niższej rozdzielczości",
|
||||
"SCREEN_RENDER_DOWN_HELP": "Pomaga zredukować aliasing na ekranach o wysokiej rozdzielczości",
|
||||
@@ -205,7 +206,6 @@
|
||||
"SNAP_ANGLE_DEG_HELP": "Chwycone okna przyciągają się do tego kąta na osi pochylenia i obrotu.",
|
||||
"ENABLE_WATCH": "Włącz zegarek",
|
||||
"SETS_ON_WATCH": "Zestawy na zegarku",
|
||||
"SHOW_WELCOME_SCREEN": "Pokaż ekran powitalny",
|
||||
"SKYBOX": "Skymap",
|
||||
"SKYMAP_ALREADY_DOWNLOADED": "Ta skymapa jest już pobrana. Wybierz żądaną akcję.",
|
||||
"SPACE_DRAG": "Przestrzeń gry",
|
||||
@@ -247,7 +247,23 @@
|
||||
"STATISTICS": "Statystyki",
|
||||
"ROTATIONS": "Obroty HMD",
|
||||
"SESSION_TIME": "Czas sesji",
|
||||
"LENS_SEPARATION_MM": "Rozstaw soczewek (mm)"
|
||||
"LENS_SEPARATION_MM": "Rozstaw soczewek (mm)",
|
||||
"MIDDLE_GRABS_SINGLE": "Modyfikator środkowego przycisku chwyta pojedyncze obiekty",
|
||||
"MIDDLE_GRABS_SINGLE_HELP": "Chwytanie z modyfikatorem środkowego przycisku myszy (fioletowy laser)\npochwyci pojedyncze nakładki, a nie cały zestaw",
|
||||
"DIAGNOSTICS_REASON": {
|
||||
"PACTL": "Wymagane do sterowania głośnikami/mikrofonem i przełączania kart dźwiękowych",
|
||||
"CAGE": "Zalecane w przypadku niekompatybilności z niektórymi aplikacjami X11 (tryb kompatybilności)",
|
||||
"XWAYLAND_SATELLITE": "Zalecane, jeśli chcesz uruchamiać aplikacje X11 (Xwayland) w trybie natywnym",
|
||||
"XDG_OPEN": "Wymagane do otwierania linków URL lub gier Steam"
|
||||
},
|
||||
"DIAGNOSTICS": {
|
||||
"CPU_ACCESSIBLE_VRAM": "VRAM dostępny dla CPU: {NUM} MiB",
|
||||
"TITLE": "Diagnostyka",
|
||||
"ENABLE_REBAR": "Włącz resizable BAR w ustawieniach BIOS, aby uzyskać optymalną wydajność VR"
|
||||
},
|
||||
"TUTORIAL": "Samouczek",
|
||||
"RESTART_WAYVR": "Zrestartuj WayVR",
|
||||
"CLOSE_WAYVR": "Zamknij WayVR"
|
||||
},
|
||||
"APPLICATION_LAUNCHER": "Launcher aplikacji",
|
||||
"APPLICATION_STARTED": "Uruchomiono aplikację",
|
||||
@@ -264,7 +280,8 @@
|
||||
"SETTINGS": "Ustawienia dźwięku",
|
||||
"SPEAKERS": "Głośniki",
|
||||
"SPEAKERS_SET_SUCCESSFULLY": "Głośniki ustawione pomyślnie",
|
||||
"VOLUME": "Głośność"
|
||||
"VOLUME": "Głośność",
|
||||
"SWITCH_TO_THIS_DEVICE": "Przełącz na to urządzenie"
|
||||
},
|
||||
"CLOSE_WINDOW": "Zamknij okno",
|
||||
"CREATION_DATE": "Data utworzenia",
|
||||
@@ -349,5 +366,15 @@
|
||||
"ADD_TO_FAVOURITES": "Przypnij aplikację",
|
||||
"ADD_TO_FAVOURITES_HINT": "Zapisz preset i przypnij go do szuflady aplikacji",
|
||||
"REMOVE_FROM_FAVOURITES": "Usuń z przypiętych aplikacji",
|
||||
"EMPTY": "Puste"
|
||||
"EMPTY": "Puste",
|
||||
"INSTALLED": "Zainstalowano",
|
||||
"NOT_INSTALLED": "Nie zainstalowano",
|
||||
"POWER_OPTIONS": {
|
||||
"TITLE": "Opcje zasilania",
|
||||
"SHUTDOWN": "Wyłącz",
|
||||
"SUSPEND": "Uśpij",
|
||||
"SHUTDOWN_ARE_YOU_SURE": "Czy na pewno chcesz wyłączyć komputer?"
|
||||
},
|
||||
"YES": "Tak",
|
||||
"NO": "Nie"
|
||||
}
|
||||
@@ -97,7 +97,10 @@
|
||||
"RIGHT": "右侧",
|
||||
"COMPONENT": "触发类型",
|
||||
"SUBPATH": "激活控件",
|
||||
"THRESHOLD": "高于上限值时激活\n低于下限值时取消激活"
|
||||
"THRESHOLD": "高于上限值时激活\n低于下限值时取消激活",
|
||||
"CHORD": "组合键",
|
||||
"CHORD_ADD_INPUT": "添加组合键输入",
|
||||
"CHORD_INPUT_HAND": "输入手"
|
||||
},
|
||||
"BROWSE_ONLINE_CATALOG": "浏览在线目录...",
|
||||
"BROWSE_SKYMAPS": "浏览天空盒",
|
||||
@@ -194,8 +197,6 @@
|
||||
"SAVE_PLAYSPACE_CENTER_HELP": "将当前的活动空间设置为重置活动空间的绑定点。",
|
||||
"RESET_PLAYSPACE_CENTER": "重置活动空间中心",
|
||||
"RESET_PLAYSPACE_CENTER_HELP": "重置之前设置的活动空间中心。",
|
||||
"RESTART_SOFTWARE": "重启软件",
|
||||
"RESTART_SOFTWARE_HELP": "应用需要重启才能生效的设置",
|
||||
"ROUND_MULTIPLIER": "UI 边缘圆角",
|
||||
"SCREEN_RENDER_DOWN": "低分辨率渲染屏幕",
|
||||
"SCREEN_RENDER_DOWN_HELP": "有助于减轻高分辨率屏幕上的锯齿现象",
|
||||
@@ -205,7 +206,6 @@
|
||||
"SNAP_ANGLE_DEG_HELP": "抓取窗口在俯仰轴和翻滚轴上按此角度对齐。",
|
||||
"ENABLE_WATCH": "启用手表",
|
||||
"SETS_ON_WATCH": "手表显示套装",
|
||||
"SHOW_WELCOME_SCREEN": "显示欢迎界面",
|
||||
"SKYBOX": "天空图",
|
||||
"SKYMAP_ALREADY_DOWNLOADED": "此天空盒已下载。请选择操作。",
|
||||
"SPACE_DRAG": "活动空间",
|
||||
@@ -247,7 +247,23 @@
|
||||
"STATISTICS": "统计数据",
|
||||
"ROTATIONS": "HMD 旋转次数",
|
||||
"SESSION_TIME": "会话时间",
|
||||
"LENS_SEPARATION_MM": "瞳距 (mm)"
|
||||
"LENS_SEPARATION_MM": "瞳距 (mm)",
|
||||
"MIDDLE_GRABS_SINGLE": "中键点击修饰键抓取单个对象",
|
||||
"MIDDLE_GRABS_SINGLE_HELP": "使用中键点击修饰键(紫色激光)抓取时\n将抓取单个叠加层,而非整个集合",
|
||||
"DIAGNOSTICS_REASON": {
|
||||
"PACTL": "用于扬声器/麦克风控制及声卡切换",
|
||||
"CAGE": "若部分 X11 应用程序不兼容,建议开启(兼容模式)",
|
||||
"XWAYLAND_SATELLITE": "如果你想以原生模式运行 X11 (Xwayland) 应用程序,建议开启此项",
|
||||
"XDG_OPEN": "打开 URL 或 Steam 游戏所需"
|
||||
},
|
||||
"DIAGNOSTICS": {
|
||||
"CPU_ACCESSIBLE_VRAM": "CPU 可访问 VRAM: {NUM} MiB",
|
||||
"TITLE": "诊断",
|
||||
"ENABLE_REBAR": "请在 BIOS 设置中启用 Resizable BAR 以获得最佳 VR 性能"
|
||||
},
|
||||
"TUTORIAL": "教程",
|
||||
"RESTART_WAYVR": "重启 WayVR",
|
||||
"CLOSE_WAYVR": "关闭 WayVR"
|
||||
},
|
||||
"APPLICATION_LAUNCHER": "应用启动器",
|
||||
"APPLICATION_STARTED": "应用程序已启动",
|
||||
@@ -264,7 +280,8 @@
|
||||
"SETTINGS": "音频设置",
|
||||
"SPEAKERS": "扬声器",
|
||||
"SPEAKERS_SET_SUCCESSFULLY": "扬声器设置成功",
|
||||
"VOLUME": "音量"
|
||||
"VOLUME": "音量",
|
||||
"SWITCH_TO_THIS_DEVICE": "切换至此设备"
|
||||
},
|
||||
"CLOSE_WINDOW": "关闭窗口",
|
||||
"CREATION_DATE": "创建日期",
|
||||
@@ -349,5 +366,15 @@
|
||||
"ADD_TO_FAVOURITES": "添加到固定应用",
|
||||
"ADD_TO_FAVOURITES_HINT": "保存预设并将其固定到应用抽屉",
|
||||
"REMOVE_FROM_FAVOURITES": "从固定应用中移除",
|
||||
"EMPTY": "暂无"
|
||||
"EMPTY": "暂无",
|
||||
"INSTALLED": "已安装",
|
||||
"NOT_INSTALLED": "未安装",
|
||||
"POWER_OPTIONS": {
|
||||
"TITLE": "电源选项",
|
||||
"SHUTDOWN": "关机",
|
||||
"SUSPEND": "暂停",
|
||||
"SHUTDOWN_ARE_YOU_SURE": "确定要关机吗?"
|
||||
},
|
||||
"YES": "确定",
|
||||
"NO": "否"
|
||||
}
|
||||
+114
-11
@@ -1,11 +1,11 @@
|
||||
use std::{path::PathBuf, rc::Rc};
|
||||
use std::{path::PathBuf, process::Command, rc::Rc};
|
||||
|
||||
use chrono::Timelike;
|
||||
use glam::Vec2;
|
||||
use strum::EnumCount;
|
||||
use wgui::{
|
||||
assets::{AssetPathRef, AssetProvider},
|
||||
components::button::ComponentButton,
|
||||
components::{ComponentTrait, button::ComponentButton},
|
||||
event::{CallbackDataCommon, StyleSetRequest},
|
||||
font_config::WguiFontConfig,
|
||||
globals::WguiGlobals,
|
||||
@@ -17,7 +17,10 @@ use wgui::{
|
||||
task::Tasks,
|
||||
theme::WguiTheme,
|
||||
widget::{label::WidgetLabel, rectangle::WidgetRectangle, sprite::WidgetSprite},
|
||||
windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
|
||||
windowing::{
|
||||
context_menu::{self, ContextMenu},
|
||||
window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
|
||||
},
|
||||
};
|
||||
use wlx_common::{
|
||||
async_executor::AsyncExecutor,
|
||||
@@ -35,10 +38,11 @@ use crate::{
|
||||
settings::TabSettings, welcome::TabWelcome,
|
||||
},
|
||||
util::{
|
||||
popup_manager::{MountPopupOnceParams, PopupManager, PopupManagerParams},
|
||||
is_exec_installed,
|
||||
popup_manager::{MountPopupOnceParams, PopupHolder, PopupManager, PopupManagerParams},
|
||||
toast_manager::ToastManager,
|
||||
},
|
||||
views,
|
||||
views::{self, ViewUpdateParams},
|
||||
};
|
||||
|
||||
pub struct FrontendWidgets {
|
||||
@@ -74,9 +78,11 @@ pub struct Frontend<T> {
|
||||
toast_manager: ToastManager,
|
||||
timestep: Timestep,
|
||||
sounds_to_play: Vec<SoundType>,
|
||||
context_menu_power: ContextMenu,
|
||||
|
||||
window_audio_settings: WguiWindow,
|
||||
view_audio_settings: Option<views::audio_settings::View>,
|
||||
popup_dialog_box: PopupHolder<views::dialog_box::View>,
|
||||
}
|
||||
|
||||
pub struct FrontendUpdateParams<'a, T> {
|
||||
@@ -116,11 +122,13 @@ pub enum FrontendTask {
|
||||
MountPopupOnce(MountPopupOnceParams),
|
||||
RefreshPopupManager,
|
||||
ShowAudioSettings,
|
||||
ShowPowerSettings(WidgetID /* source widget */),
|
||||
UpdateAudioSettingsView,
|
||||
RecenterPlayspace,
|
||||
PushToast(Translation),
|
||||
PlaySound(SoundType),
|
||||
OpenURL(Rc<str>),
|
||||
RunCommand(String /* program */, Vec<String> /* args */),
|
||||
HideDashboard,
|
||||
MarkTutorialGraduated,
|
||||
}
|
||||
@@ -202,9 +210,11 @@ impl<T: 'static> Frontend<T> {
|
||||
popup_manager,
|
||||
toast_manager,
|
||||
window_audio_settings: WguiWindow::default(),
|
||||
context_menu_power: Default::default(),
|
||||
view_audio_settings: None,
|
||||
executor: params.executor,
|
||||
sounds_to_play: Vec::new(),
|
||||
popup_dialog_box: Default::default(),
|
||||
};
|
||||
|
||||
// init some things first
|
||||
@@ -220,7 +230,7 @@ impl<T: 'static> Frontend<T> {
|
||||
self.sounds_to_play.push(sound_type);
|
||||
}
|
||||
|
||||
fn play_sound(&mut self, audio_system: &mut audio::AudioSystem, sound_type: SoundType) -> anyhow::Result<()> {
|
||||
fn play_sound(&mut self, audio: &mut Box<dyn audio::AudioProvider>, sound_type: SoundType) -> anyhow::Result<()> {
|
||||
let mut assets = self.globals.assets_builtin();
|
||||
|
||||
let path = match sound_type {
|
||||
@@ -236,7 +246,7 @@ impl<T: 'static> Frontend<T> {
|
||||
};
|
||||
|
||||
let sample = audio::AudioSample::from_mp3(&sound_bytes)?;
|
||||
audio_system.play_sample(&sample);
|
||||
audio.play_raw_sample(&sample);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -264,24 +274,88 @@ impl<T: 'static> Frontend<T> {
|
||||
pub fn process_update(
|
||||
&mut self,
|
||||
res: FrontendUpdateResult,
|
||||
audio_system: &mut audio::AudioSystem,
|
||||
audio_sample_player: &mut audio::SamplePlayer,
|
||||
audio: &mut Box<dyn audio::AudioProvider>,
|
||||
) -> anyhow::Result<()> {
|
||||
for sound_type in res.sounds_to_play {
|
||||
self.play_sound(audio_system, sound_type)?;
|
||||
self.play_sound(audio, sound_type)?;
|
||||
}
|
||||
|
||||
audio_sample_player.play_wgui_samples(audio_system, res.layout_result.sounds_to_play);
|
||||
audio.play_wgui_samples(res.layout_result.sounds_to_play);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_poweroff_confirm(&mut self) {
|
||||
let tasks = self.tasks.clone();
|
||||
views::dialog_box::mount_popup(
|
||||
self.popup_dialog_box.clone(),
|
||||
self.tasks.clone(),
|
||||
views::dialog_box::Params {
|
||||
globals: self.globals.clone(),
|
||||
message: Translation::from_translation_key("POWER_OPTIONS.SHUTDOWN_ARE_YOU_SURE"),
|
||||
entries: views::dialog_box::get_entries_yes_no(),
|
||||
on_action_click: Box::new(move |action| {
|
||||
if action == "yes" {
|
||||
tasks.push(FrontendTask::RunCommand(
|
||||
String::from("loginctl"),
|
||||
vec![String::from("poweroff")],
|
||||
));
|
||||
}
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn process_power_action(&mut self, action: &str, data: &mut T) {
|
||||
if !is_exec_installed("loginctl") {
|
||||
// TODO: add support for other init systems?
|
||||
self.tasks.push(FrontendTask::PushToast(Translation::from_raw_text(
|
||||
"loginctl is not installed",
|
||||
)));
|
||||
return;
|
||||
}
|
||||
|
||||
match action {
|
||||
"restart" => {
|
||||
self.interface.restart(data);
|
||||
}
|
||||
"close" => {
|
||||
self.interface.close(data);
|
||||
}
|
||||
"suspend" => {
|
||||
self.tasks.push(FrontendTask::RunCommand(
|
||||
String::from("loginctl"),
|
||||
vec![String::from("suspend")],
|
||||
));
|
||||
}
|
||||
"shutdown" => {
|
||||
self.action_poweroff_confirm();
|
||||
}
|
||||
_ => {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tick(&mut self, params: FrontendUpdateParams<T>) -> anyhow::Result<FrontendUpdateResult> {
|
||||
// fixme: timer events instead of this thing
|
||||
if self.ticks.is_multiple_of(1000) {
|
||||
self.update_time(params.data)?;
|
||||
}
|
||||
|
||||
// TODO: refactor: simplify update function (no configs or T are required in most cases)
|
||||
let mut config_change_kind = None;
|
||||
self.popup_dialog_box.update(&mut ViewUpdateParams {
|
||||
layout: &mut self.layout,
|
||||
executor: &self.executor,
|
||||
general_config: self.interface.general_config(params.data),
|
||||
config_change_kind: &mut config_change_kind,
|
||||
})?;
|
||||
|
||||
if let context_menu::TickResult::Action(action) = self.context_menu_power.tick(&mut self.layout, &mut self.state)? {
|
||||
self.process_power_action(&action, params.data);
|
||||
}
|
||||
|
||||
{
|
||||
// always 30 times per second
|
||||
while self.timestep.on_tick() {
|
||||
@@ -372,12 +446,14 @@ impl<T: 'static> Frontend<T> {
|
||||
FrontendTask::MountPopupOnce(popup_params) => self.mount_popup_once(popup_params, params.data)?,
|
||||
FrontendTask::RefreshPopupManager => self.refresh_popup_manager()?,
|
||||
FrontendTask::ShowAudioSettings => self.action_show_audio_settings()?,
|
||||
FrontendTask::ShowPowerSettings(pos) => self.action_show_power_settings(pos)?,
|
||||
FrontendTask::UpdateAudioSettingsView => self.action_update_audio_settings()?,
|
||||
FrontendTask::RecenterPlayspace => self.action_recenter_playspace(params.data)?,
|
||||
FrontendTask::PushToast(content) => self.toast_manager.push(content),
|
||||
FrontendTask::PlaySound(sound_type) => self.queue_play_sound(sound_type),
|
||||
FrontendTask::HideDashboard => self.action_hide_dashboard(params.data),
|
||||
FrontendTask::OpenURL(url) => self.action_open_url(url),
|
||||
FrontendTask::RunCommand(program, args) => self.action_run_command(program, args),
|
||||
FrontendTask::MarkTutorialGraduated => self.action_tutorial_graduated(params.data),
|
||||
};
|
||||
Ok(())
|
||||
@@ -504,6 +580,11 @@ impl<T: 'static> Frontend<T> {
|
||||
FrontendTask::ShowAudioSettings,
|
||||
);
|
||||
|
||||
let btn_power = self.state.fetch_component_as::<ComponentButton>("btn_power")?;
|
||||
self
|
||||
.tasks
|
||||
.handle_button(&btn_power, FrontendTask::ShowPowerSettings(btn_power.base().get_id()));
|
||||
|
||||
// "Recenter playspace" bottom bar button
|
||||
self.tasks.handle_button(
|
||||
&self.state.fetch_component_as::<ComponentButton>("btn_recenter")?,
|
||||
@@ -513,6 +594,18 @@ impl<T: 'static> Frontend<T> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_show_power_settings(&mut self, source_id: WidgetID) -> anyhow::Result<()> {
|
||||
self.context_menu_power.open(context_menu::OpenParams {
|
||||
on_custom_attribs: None,
|
||||
position: source_id.into(),
|
||||
blueprint: context_menu::Blueprint::Template {
|
||||
template_name: "context_menu_power_options".into(),
|
||||
template_params: Default::default(),
|
||||
},
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_show_audio_settings(&mut self) -> anyhow::Result<()> {
|
||||
self.window_audio_settings.open(&mut WguiWindowParams {
|
||||
position: Vec2::new(64.0, 64.0),
|
||||
@@ -577,4 +670,14 @@ impl<T: 'static> Frontend<T> {
|
||||
url
|
||||
))));
|
||||
}
|
||||
|
||||
fn action_run_command(&mut self, command: String, args: Vec<String>) {
|
||||
if let Err(e) = Command::new(command).args(args).spawn() {
|
||||
self
|
||||
.tasks
|
||||
.push(FrontendTask::PushToast(Translation::from_raw_text_string(format!(
|
||||
"Failed to launch command: {e}"
|
||||
))))
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{marker::PhantomData, rc::Rc};
|
||||
|
||||
use wgui::{
|
||||
assets::{AssetPathRef},
|
||||
assets::AssetPathRef,
|
||||
components::button::ComponentButton,
|
||||
globals::WguiGlobals,
|
||||
layout::{Layout, WidgetID},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{marker::PhantomData, rc::Rc};
|
||||
|
||||
use wgui::{
|
||||
animation::{Animation, AnimationEasing},
|
||||
animation::{Animation, AnimationDuration, AnimationEasing},
|
||||
assets::AssetPathRef,
|
||||
color::{WguiColor, WguiColorName, WguiNamedColor},
|
||||
components::{ComponentTrait, button::ComponentButton},
|
||||
@@ -61,7 +61,7 @@ fn get_supporter_anim(
|
||||
Animation::new_ex(
|
||||
btn.base().get_id(),
|
||||
1234,
|
||||
480,
|
||||
AnimationDuration::SecondsFixed(8.0),
|
||||
AnimationEasing::Linear,
|
||||
Box::new(move |common, data| {
|
||||
let opacity_in = f32::clamp(data.pos * FADE_SPEED, 0.0, 1.0);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{collections::HashMap, marker::PhantomData, rc::Rc};
|
||||
|
||||
use wgui::{
|
||||
assets::{AssetPathRef},
|
||||
assets::AssetPathRef,
|
||||
components::{
|
||||
bar_graph::{ComponentBarGraph, ValueCell},
|
||||
button::ComponentButton,
|
||||
|
||||
@@ -49,6 +49,49 @@ pub fn options_stat_row(mp: &mut MacroParams, parent: WidgetID, translation: &st
|
||||
mp.parser_state.get_widget_id(&id)
|
||||
}
|
||||
|
||||
pub fn options_diagnostics_row(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
title: &str,
|
||||
tooltip_str: &str,
|
||||
checked: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut params = TemplateParams::new();
|
||||
params.insert("title", title);
|
||||
params.insert("tooltip_str", tooltip_str);
|
||||
params.insert("color", if checked { "success" } else { "danger" });
|
||||
|
||||
params.insert(
|
||||
"sprite",
|
||||
if checked {
|
||||
"@/dashboard/check.svg"
|
||||
} else {
|
||||
"@/dashboard/cross.svg"
|
||||
},
|
||||
);
|
||||
|
||||
mp.parser_state
|
||||
.instantiate_template(mp.doc_params, "DiagnosticsRow", mp.layout, parent, params)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn options_diagnostics_drm_card(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
name: &str,
|
||||
info: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut params = TemplateParams::new();
|
||||
params.insert("name", name);
|
||||
params.insert("info", info);
|
||||
|
||||
mp.parser_state
|
||||
.instantiate_template(mp.doc_params, "DiagnosticsDrmCard", mp.layout, parent, params)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn options_checkbox(mp: &mut MacroParams, parent: WidgetID, setting: SettingType) -> anyhow::Result<()> {
|
||||
let id = mp.idx.to_string();
|
||||
mp.idx += 1;
|
||||
@@ -290,7 +333,7 @@ where
|
||||
let tasks = mp.tasks.clone();
|
||||
move |_common, e: ButtonClickEvent| {
|
||||
tasks.push(Task::OpenContextMenu(
|
||||
e.mouse_pos_absolute.unwrap_or_default(),
|
||||
e.mouse_pos_absolute.into(),
|
||||
EnumType::VARIANTS
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
@@ -317,12 +360,12 @@ where
|
||||
let text = &title.text;
|
||||
let translated = if title.translated { "1" } else { "0" };
|
||||
|
||||
Some(context_menu::Cell {
|
||||
Some(context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(format!("{setting_str};{id};{value};{text};{translated}").into()),
|
||||
title,
|
||||
tooltip,
|
||||
attribs: vec![],
|
||||
})
|
||||
..Default::default()
|
||||
}))
|
||||
})
|
||||
.collect(),
|
||||
));
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use glam::Vec2;
|
||||
use std::{marker::PhantomData, rc::Rc, str::FromStr};
|
||||
use strum::{AsRefStr, EnumProperty, EnumString};
|
||||
use wgui::{
|
||||
assets::{AssetPathRef},
|
||||
assets::AssetPathRef,
|
||||
color::WguiColorName,
|
||||
components::tabs::ComponentTabs,
|
||||
event::StyleSetRequest,
|
||||
@@ -35,6 +34,7 @@ use crate::{
|
||||
mod macros;
|
||||
mod tab_autostart_apps;
|
||||
mod tab_controls;
|
||||
mod tab_diagnostics;
|
||||
mod tab_features;
|
||||
mod tab_look_and_feel;
|
||||
mod tab_misc;
|
||||
@@ -53,6 +53,7 @@ pub(crate) enum TabNameEnum {
|
||||
Skybox,
|
||||
SpaceDrag,
|
||||
Statistics,
|
||||
Diagnostics,
|
||||
Troubleshooting,
|
||||
}
|
||||
|
||||
@@ -67,6 +68,7 @@ impl TabNameEnum {
|
||||
"skybox" => Some(TabNameEnum::Skybox),
|
||||
"space_drag" => Some(TabNameEnum::SpaceDrag),
|
||||
"statistics" => Some(TabNameEnum::Statistics),
|
||||
"diagnostics" => Some(TabNameEnum::Diagnostics),
|
||||
"troubleshooting" => Some(TabNameEnum::Troubleshooting),
|
||||
_ => None,
|
||||
}
|
||||
@@ -78,11 +80,10 @@ pub(crate) enum Task {
|
||||
ClearPipewireTokens,
|
||||
ClearSavedState,
|
||||
DeleteAllConfigs,
|
||||
OpenContextMenu(Vec2, Vec<context_menu::Cell>),
|
||||
OpenContextMenu(context_menu::Position, Vec<context_menu::Cell>),
|
||||
RemoveAutostartApp(Rc<str>),
|
||||
ResetPlayspace,
|
||||
ResetPlayspaceCenter,
|
||||
RestartSoftware,
|
||||
SavePlayspaceCenter,
|
||||
SetTab(TabNameEnum),
|
||||
SettingUpdated(SettingType),
|
||||
@@ -228,10 +229,6 @@ impl<T> Tab<T> for TabSettings<T> {
|
||||
frontend.interface.playspace_task(data, DashPlayspaceTask::Reset)?;
|
||||
return Ok(());
|
||||
}
|
||||
Task::RestartSoftware => {
|
||||
frontend.interface.restart(data);
|
||||
return Ok(());
|
||||
}
|
||||
Task::OpenContextMenu(position, cells) => {
|
||||
self.context_menu.open(context_menu::OpenParams {
|
||||
on_custom_attribs: None,
|
||||
@@ -347,6 +344,7 @@ pub(crate) enum SettingType {
|
||||
NotificationsEnabled,
|
||||
NotificationsSoundEnabled,
|
||||
OpaqueBackground,
|
||||
MiddleGrabsSingle,
|
||||
MouseAcceleration,
|
||||
PointerLerpFactor,
|
||||
MouseSpeed,
|
||||
@@ -403,6 +401,7 @@ impl SettingType {
|
||||
Self::NotificationsEnabled => &mut config.notifications_enabled,
|
||||
Self::NotificationsSoundEnabled => &mut config.notifications_sound_enabled,
|
||||
Self::OpaqueBackground => &mut config.opaque_background,
|
||||
Self::MiddleGrabsSingle => &mut config.middle_grabs_single,
|
||||
Self::MouseAcceleration => &mut config.wvr_mouse_acceleration,
|
||||
Self::ScreenRenderDown => &mut config.screen_render_down,
|
||||
Self::SetsOnWatch => &mut config.sets_on_watch,
|
||||
@@ -553,6 +552,7 @@ impl SettingType {
|
||||
Self::NotificationsEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_ENABLED"),
|
||||
Self::NotificationsSoundEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_SOUND_ENABLED"),
|
||||
Self::OpaqueBackground => Ok("APP_SETTINGS.OPAQUE_BACKGROUND"),
|
||||
Self::MiddleGrabsSingle => Ok("APP_SETTINGS.MIDDLE_GRABS_SINGLE"),
|
||||
Self::MouseSpeed => Ok("APP_SETTINGS.POINTER_SPEED"),
|
||||
Self::MouseAcceleration => Ok("APP_SETTINGS.POINTER_ACCELERATION"),
|
||||
Self::PointerLerpFactor => Ok("APP_SETTINGS.POINTER_LERP_FACTOR"),
|
||||
@@ -602,6 +602,7 @@ impl SettingType {
|
||||
Self::InputEmulationMethod => Some("APP_SETTINGS.INPUT_EMULATION_METHOD_HELP"),
|
||||
Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"),
|
||||
Self::LeftHandedMouse => Some("APP_SETTINGS.LEFT_HANDED_MOUSE_HELP"),
|
||||
Self::MiddleGrabsSingle => Some("APP_SETTINGS.MIDDLE_GRABS_SINGLE_HELP"),
|
||||
Self::MouseAcceleration => Some("APP_SETTINGS.POINTER_ACCELERATION_HELP"),
|
||||
Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"),
|
||||
Self::SnapAngleDeg => Some("APP_SETTINGS.SNAP_ANGLE_DEG_HELP"),
|
||||
@@ -749,6 +750,9 @@ impl<T> TabSettings<T> {
|
||||
TabNameEnum::Statistics => {
|
||||
self.current_tab = Some(Box::new(tab_statistics::State::mount(settings_mount_params)?));
|
||||
}
|
||||
TabNameEnum::Diagnostics => {
|
||||
self.current_tab = Some(Box::new(tab_diagnostics::State::mount(settings_mount_params)?));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -100,6 +100,7 @@ impl State {
|
||||
options_dropdown::<wlx_common::config::HandsfreeAltTab>(par.mp, c, &SettingType::HandsfreeAltTab)?;
|
||||
options_checkbox(par.mp, c, SettingType::FocusFollowsMouseMode)?;
|
||||
options_checkbox(par.mp, c, SettingType::LeftHandedMouse)?;
|
||||
options_checkbox(par.mp, c, SettingType::MiddleGrabsSingle)?;
|
||||
options_checkbox(par.mp, c, SettingType::AllowSliding)?;
|
||||
options_checkbox(par.mp, c, SettingType::InvertScrollDirectionX)?;
|
||||
options_checkbox(par.mp, c, SettingType::InvertScrollDirectionY)?;
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
use wgui::layout::WidgetID;
|
||||
|
||||
use crate::{
|
||||
tab::settings::{
|
||||
SettingsMountParams, SettingsTab,
|
||||
macros::{MacroParams, options_category, options_diagnostics_drm_card, options_diagnostics_row},
|
||||
},
|
||||
util::is_exec_installed,
|
||||
};
|
||||
|
||||
pub struct State {}
|
||||
|
||||
impl SettingsTab for State {}
|
||||
|
||||
#[derive(Default)]
|
||||
struct LspciInfo {
|
||||
// slot: String,
|
||||
// class: String,
|
||||
vendor: String,
|
||||
device: String,
|
||||
// s_vendor: String,
|
||||
}
|
||||
|
||||
fn lspci_info(vendor: &str /* hex */, device: &str /* hex */) -> Option<LspciInfo> {
|
||||
// example output for "lspci -vmmd 1002:747e":
|
||||
//
|
||||
// Slot: 0b:00.0
|
||||
// Class: VGA compatible controller
|
||||
// Vendor: Advanced Micro Devices, Inc. [AMD/ATI]
|
||||
// Device: Navi 32 [Radeon RX 7700 XT / 7800 XT]
|
||||
// SVendor: Sapphire Technology Limited
|
||||
// SDevice: Device 475a
|
||||
// Rev: c8
|
||||
// ProgIf: 00
|
||||
|
||||
let mut cmd = std::process::Command::new("lspci");
|
||||
cmd.arg("-vmmd");
|
||||
cmd.arg(format!("{}:{}", vendor, device));
|
||||
|
||||
let res = cmd.output().ok()?;
|
||||
|
||||
if !res.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut info = LspciInfo::default();
|
||||
|
||||
let stdout = str::from_utf8(&res.stdout).ok()?;
|
||||
for line in stdout.lines() {
|
||||
let Some((key, value)) = line.split_once(":") else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if key == "Device" {
|
||||
info.device = String::from(value.trim());
|
||||
}
|
||||
}
|
||||
|
||||
Some(info)
|
||||
}
|
||||
|
||||
fn mount_installed_path(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
exec: &str,
|
||||
reason_translation: Option<&str>,
|
||||
) -> anyhow::Result<()> {
|
||||
let title = if is_exec_installed(exec) {
|
||||
"INSTALLED"
|
||||
} else {
|
||||
"NOT_INSTALLED"
|
||||
};
|
||||
|
||||
let title_translated = mp.layout.state.globals.i18n().translate(title);
|
||||
let tooltip = if let Some(reason) = reason_translation {
|
||||
format!(
|
||||
"{}\n{}",
|
||||
title_translated,
|
||||
mp.layout.state.globals.i18n().translate(reason)
|
||||
)
|
||||
} else {
|
||||
title_translated.to_string()
|
||||
};
|
||||
options_diagnostics_row(mp, parent, exec, &tooltip, is_exec_installed(exec))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_sysfs_param(device_path: &str, param: &str) -> anyhow::Result<String> {
|
||||
let string = std::fs::read_to_string(format!("{}/{}", device_path, param))?;
|
||||
Ok(string.trim().into())
|
||||
}
|
||||
|
||||
fn read_sysfs_param_int(device_path: &str, param: &str) -> anyhow::Result<u64> {
|
||||
Ok(read_sysfs_param(device_path, param)?.parse::<u64>()?)
|
||||
}
|
||||
|
||||
fn mount_drm_card(mp: &mut MacroParams, parent: WidgetID, device_path: &str) -> anyhow::Result<()> {
|
||||
let vendor = read_sysfs_param(device_path, "vendor")?;
|
||||
let device = read_sysfs_param(device_path, "device")?;
|
||||
let vram_total = read_sysfs_param_int(device_path, "mem_info_vram_total").ok();
|
||||
let vis_vram_total = read_sysfs_param_int(device_path, "mem_info_vis_vram_total").ok();
|
||||
|
||||
let name = match lspci_info(&vendor, &device) {
|
||||
Some(info) => {
|
||||
format!("{} {}", info.vendor, info.device)
|
||||
}
|
||||
None => format!("{}:{}", vendor, device),
|
||||
};
|
||||
|
||||
let info = if let Some(vram_total) = vram_total
|
||||
&& let Some(vis_vram_total) = vis_vram_total
|
||||
{
|
||||
let mut s = mp.layout.state.globals.i18n().translate_and_replace(
|
||||
"APP_SETTINGS.DIAGNOSTICS.CPU_ACCESSIBLE_VRAM",
|
||||
(
|
||||
"{NUM}",
|
||||
&format!("{}/{}", vis_vram_total / 1024 / 1024, vram_total / 1024 / 1024),
|
||||
),
|
||||
);
|
||||
|
||||
if vis_vram_total != vram_total {
|
||||
s += " [ALERT!]\nEnable Resizable BAR in the BIOS settings for optimal VR performance";
|
||||
}
|
||||
|
||||
s
|
||||
} else {
|
||||
String::from("No info")
|
||||
};
|
||||
|
||||
options_diagnostics_drm_card(mp, parent, &name, &info)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_drm_cards(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
|
||||
for file in std::fs::read_dir("/sys/class/drm/")?.flatten() {
|
||||
let file_name = file.file_name();
|
||||
let Some(file_name) = file_name.to_str() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !file_name.starts_with("renderD") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = mount_drm_card(mp, parent, &format!("/sys/class/drm/{}/device", file_name)) {
|
||||
log::error!("failed to mount drm card: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn mount(par: SettingsMountParams) -> anyhow::Result<Self> {
|
||||
let c = options_category(
|
||||
par.mp,
|
||||
par.id_parent,
|
||||
"APP_SETTINGS.DIAGNOSTICS.TITLE",
|
||||
"@/dashboard/diagnostics.svg",
|
||||
)?;
|
||||
|
||||
mount_installed_path(par.mp, c, "pactl", Some("APP_SETTINGS.DIAGNOSTICS_REASON.PACTL"))?;
|
||||
mount_installed_path(par.mp, c, "cage", Some("APP_SETTINGS.DIAGNOSTICS_REASON.CAGE"))?;
|
||||
mount_installed_path(
|
||||
par.mp,
|
||||
c,
|
||||
"xwayland-satellite",
|
||||
Some("APP_SETTINGS.DIAGNOSTICS_REASON.XWAYLAND_SATELLITE"),
|
||||
)?;
|
||||
mount_installed_path(par.mp, c, "xdg-open", Some("APP_SETTINGS.DIAGNOSTICS_REASON.XDG_OPEN"))?;
|
||||
mount_installed_path(par.mp, c, "steam", None)?;
|
||||
|
||||
mount_drm_cards(par.mp, c)?;
|
||||
|
||||
Ok(State {})
|
||||
}
|
||||
}
|
||||
@@ -328,32 +328,33 @@ fn whisper_models_dropdown(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Re
|
||||
let mut cells = WHISPER_MODELS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, item)| context_menu::Cell {
|
||||
action_name: Some(format!(";{id};{}", item.file_name).into()),
|
||||
title: Translation::from_raw_text_string(format!(
|
||||
"{}{}",
|
||||
item.display_name,
|
||||
if downloaded_models.contains(&idx) { " ✅" } else { "" }
|
||||
)),
|
||||
tooltip: None,
|
||||
attribs: vec![],
|
||||
.map(|(idx, item)| {
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(format!(";{id};{}", item.file_name).into()),
|
||||
title: Translation::from_raw_text(item.display_name),
|
||||
tooltip: None,
|
||||
attribs: vec![],
|
||||
sprite_src_builtin: if downloaded_models.contains(&idx) {
|
||||
Some("@/dashboard/check.svg".into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cells.insert(
|
||||
0,
|
||||
context_menu::Cell {
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(format!(";{id};").into()),
|
||||
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
|
||||
tooltip: None,
|
||||
attribs: vec![],
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
|
||||
parent_tasks.push(ParentTask::OpenContextMenu(
|
||||
e.mouse_pos_absolute.unwrap_or_default(),
|
||||
cells,
|
||||
));
|
||||
parent_tasks.push(ParentTask::OpenContextMenu(e.mouse_pos_absolute.into(), cells));
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -36,13 +36,6 @@ impl State {
|
||||
"@/dashboard/circle.svg",
|
||||
Task::DeleteAllConfigs,
|
||||
)?;
|
||||
options_danger_button(
|
||||
par.mp,
|
||||
c,
|
||||
"APP_SETTINGS.RESTART_SOFTWARE",
|
||||
"@/dashboard/refresh.svg",
|
||||
Task::RestartSoftware,
|
||||
)?;
|
||||
Ok(State {})
|
||||
}
|
||||
}
|
||||
@@ -7,3 +7,16 @@ pub mod steam_utils;
|
||||
pub mod toast_manager;
|
||||
pub mod wgui_simple;
|
||||
pub mod whisper;
|
||||
|
||||
// works good enough
|
||||
pub fn is_exec_installed(exec: &str) -> bool {
|
||||
let mut cmd = std::process::Command::new("sh");
|
||||
cmd.arg("-c");
|
||||
cmd.arg(format!("command -v {}", exec));
|
||||
|
||||
let Ok(res) = cmd.output() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
res.status.success()
|
||||
}
|
||||
@@ -21,6 +21,9 @@ pub struct Volume {
|
||||
|
||||
#[serde(rename = "front-right")]
|
||||
pub front_right: Option<VolumeChannel>,
|
||||
|
||||
#[serde(rename = "mono")]
|
||||
pub mono: Option<VolumeChannel>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
@@ -207,7 +210,7 @@ pub fn list_sources() -> anyhow::Result<Vec<Source>> {
|
||||
// exclude all monitor sources
|
||||
sources.retain(|source| match &source.properties.device_class {
|
||||
Some(c) => c != "monitor",
|
||||
None => false,
|
||||
None => true, /* unknown, pass it */
|
||||
});
|
||||
|
||||
Ok(sources)
|
||||
@@ -242,6 +245,8 @@ pub fn get_source_volume(source: &Source) -> anyhow::Result<f32> {
|
||||
front_left
|
||||
} else if let Some(aux0) = &source.volume.aux0 {
|
||||
aux0
|
||||
} else if let Some(mono) = &source.volume.mono {
|
||||
mono
|
||||
} else {
|
||||
return Ok(0.0); // fail silently
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ use std::{
|
||||
rc::{Rc, Weak},
|
||||
};
|
||||
|
||||
use glam::Vec2;
|
||||
use wgui::{
|
||||
animation::{Animation, AnimationDuration, AnimationEasing},
|
||||
assets::AssetPathRef,
|
||||
components::button::ComponentButton,
|
||||
event::{EventAlterables, StyleSetRequest},
|
||||
@@ -12,7 +14,7 @@ use wgui::{
|
||||
layout::{Layout, LayoutTask, LayoutTasks, WidgetID},
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||
taffy::Rect,
|
||||
widget::label::WidgetLabel,
|
||||
widget::{WidgetObj, label::WidgetLabel},
|
||||
};
|
||||
use wlx_common::config::GeneralConfig;
|
||||
|
||||
@@ -251,6 +253,13 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
struct MountPopupPrepareResult {
|
||||
popup_handle: PopupHandle,
|
||||
id_content: WidgetID,
|
||||
id_top_bar_contents: WidgetID,
|
||||
id_popup_title: WidgetID,
|
||||
}
|
||||
|
||||
impl PopupManager {
|
||||
pub fn new(params: PopupManagerParams) -> Self {
|
||||
Self {
|
||||
@@ -273,7 +282,7 @@ impl PopupManager {
|
||||
frontend_tasks: &FrontendTasks,
|
||||
popup_title: &Translation,
|
||||
popup_padding: PopupPadding,
|
||||
) -> anyhow::Result<(PopupHandle, WidgetID /* content widget ID */)> {
|
||||
) -> anyhow::Result<MountPopupPrepareResult> {
|
||||
let doc_params = &ParseDocumentParams {
|
||||
globals: globals.clone(),
|
||||
path: AssetPathRef::BuiltIn("gui/view/popup_window.xml"),
|
||||
@@ -283,6 +292,7 @@ impl PopupManager {
|
||||
|
||||
let id_root = state.get_widget_id("root")?;
|
||||
let id_content = state.get_widget_id("content")?;
|
||||
let id_top_bar_contents = state.get_widget_id("top_bar_contents")?;
|
||||
|
||||
let padding = match popup_padding {
|
||||
PopupPadding::Normal => 16.0_f32,
|
||||
@@ -294,10 +304,11 @@ impl PopupManager {
|
||||
StyleSetRequest::Padding(Rect::length(padding)),
|
||||
));
|
||||
|
||||
{
|
||||
let id_popup_title = {
|
||||
let mut label_title = state.fetch_widget_as::<WidgetLabel>(&layout.state, "popup_title")?;
|
||||
label_title.set_text_simple(&mut globals.get(), popup_title.clone());
|
||||
}
|
||||
label_title.get_id()
|
||||
};
|
||||
|
||||
let but_back = state.fetch_component_as::<ComponentButton>("but_back")?;
|
||||
|
||||
@@ -338,7 +349,13 @@ impl PopupManager {
|
||||
});
|
||||
|
||||
frontend_tasks.push(FrontendTask::RefreshPopupManager);
|
||||
Ok((popup_handle, id_content))
|
||||
|
||||
Ok(MountPopupPrepareResult {
|
||||
popup_handle,
|
||||
id_content,
|
||||
id_top_bar_contents,
|
||||
id_popup_title,
|
||||
})
|
||||
}
|
||||
|
||||
/// Mount a new popup on top of the existing popup stack.
|
||||
@@ -356,18 +373,43 @@ impl PopupManager {
|
||||
anyhow::bail!("mount_popup_once called more than once");
|
||||
};
|
||||
|
||||
let (popup_handle, id_content) =
|
||||
self.mount_popup_prepare(globals, layout, frontend_tasks, ¶ms.title, params.extra.padding)?;
|
||||
let res = self.mount_popup_prepare(globals, layout, frontend_tasks, ¶ms.title, params.extra.padding)?;
|
||||
|
||||
// text fade-in
|
||||
Animation::effect_label_fade_in(
|
||||
res.id_popup_title,
|
||||
AnimationDuration::Seconds(0.25),
|
||||
AnimationEasing::OutQuad,
|
||||
)
|
||||
.submit_l(layout);
|
||||
|
||||
// top bar slide
|
||||
Animation::effect_slide(
|
||||
res.id_top_bar_contents,
|
||||
AnimationDuration::Seconds(0.5),
|
||||
AnimationEasing::OutQuint,
|
||||
Vec2::new(10.0, 0.0),
|
||||
)
|
||||
.submit_l(layout);
|
||||
|
||||
// content slide
|
||||
Animation::effect_slide(
|
||||
res.id_content,
|
||||
AnimationDuration::Seconds(0.5),
|
||||
AnimationEasing::OutQuint,
|
||||
Vec2::new(5.0, 0.0),
|
||||
)
|
||||
.submit_l(layout);
|
||||
|
||||
// mount user-set popup content
|
||||
let closed_callback = on_content_func(PopupContentFuncData {
|
||||
layout,
|
||||
handle: popup_handle.clone(),
|
||||
id_content,
|
||||
handle: res.popup_handle.clone(),
|
||||
id_content: res.id_content,
|
||||
config,
|
||||
})?;
|
||||
|
||||
popup_handle.state.borrow_mut().closed_callback = Some(closed_callback);
|
||||
res.popup_handle.state.borrow_mut().closed_callback = Some(closed_callback);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use glam::{Mat4, Vec3};
|
||||
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
|
||||
use wgui::{
|
||||
animation::{Animation, AnimationEasing},
|
||||
animation::{Animation, AnimationDuration, AnimationEasing},
|
||||
color::{WguiColor, WguiColorName},
|
||||
components::tooltip::{TOOLTIP_BORDER_COLOR, TOOLTIP_COLOR},
|
||||
i18n::Translation,
|
||||
@@ -45,7 +45,9 @@ impl Drop for MountedToast {
|
||||
}
|
||||
}
|
||||
|
||||
const TOAST_DURATION_TICKS: u32 = 150;
|
||||
fn def_toast_duration() -> AnimationDuration {
|
||||
AnimationDuration::Seconds(2.5)
|
||||
}
|
||||
|
||||
impl ToastManager {
|
||||
pub fn new() -> Self {
|
||||
@@ -116,9 +118,9 @@ impl ToastManager {
|
||||
let (label, _) = layout.add_child(rect.id, label, taffy::Style { ..Default::default() })?;
|
||||
|
||||
// show-up animation
|
||||
layout.animations.add(Animation::new(
|
||||
Animation::new(
|
||||
rect.id,
|
||||
(TOAST_DURATION_TICKS as f32 * layout.state.theme.animation_mult) as u32,
|
||||
def_toast_duration(),
|
||||
AnimationEasing::Linear,
|
||||
Box::new(move |common, data| {
|
||||
let pos_showup = AnimationEasing::OutQuint.interpolate((data.pos * 4.0).min(1.0));
|
||||
@@ -143,7 +145,8 @@ impl ToastManager {
|
||||
);
|
||||
common.alterables.mark_redraw();
|
||||
}),
|
||||
));
|
||||
)
|
||||
.submit_l(layout);
|
||||
|
||||
state.toast = Some(MountedToast {
|
||||
id_root: root.id,
|
||||
@@ -165,7 +168,7 @@ impl ToastManager {
|
||||
|
||||
if state.timeout == 0 {
|
||||
state.toast = None;
|
||||
state.timeout = TOAST_DURATION_TICKS;
|
||||
state.timeout = def_toast_duration().to_ticks(layout.state.ticks_per_second, layout.state.theme.animation_mult);
|
||||
// mount next
|
||||
if let Some(content) = state.queue.pop_front() {
|
||||
self.mount_toast(layout, &mut state, content)?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use glam::{Mat4, Vec2};
|
||||
use wgui::{
|
||||
animation::{Animation, AnimationEasing},
|
||||
animation::{Animation, AnimationDuration, AnimationEasing},
|
||||
assets::AssetPathRef,
|
||||
color::WguiColorName,
|
||||
components::{self, button::ButtonClickCallback},
|
||||
@@ -145,9 +145,9 @@ pub fn create_loading(par: CreateLoadingParams) -> anyhow::Result<WidgetID> {
|
||||
let id_root = data.get_widget_id("root")?;
|
||||
let id_sprite_loading = data.get_widget_id("sprite_loading")?;
|
||||
|
||||
par.layout.animations.add(Animation::new(
|
||||
Animation::new(
|
||||
id_sprite_loading,
|
||||
60 * 30, /* spin it for 30 seconds at most */
|
||||
AnimationDuration::SecondsFixed(120.0), /* spin it for 120 seconds at most */
|
||||
AnimationEasing::Linear,
|
||||
Box::new(move |common, data| {
|
||||
// spin it
|
||||
@@ -161,7 +161,8 @@ pub fn create_loading(par: CreateLoadingParams) -> anyhow::Result<WidgetID> {
|
||||
}
|
||||
common.alterables.mark_redraw();
|
||||
}),
|
||||
));
|
||||
)
|
||||
.submit_l(par.layout);
|
||||
|
||||
Ok(id_root)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use wgui::{
|
||||
assets::AssetPathRef,
|
||||
assets::{AssetPathRef, AssetPathSource},
|
||||
color::{WguiColor, WguiColorName},
|
||||
components::{
|
||||
self,
|
||||
@@ -12,9 +12,9 @@ use wgui::{
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID},
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState, TemplateParams},
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState, TemplateParams, strip_path_as},
|
||||
task::Tasks,
|
||||
widget::ConstructEssentials,
|
||||
widget::{ConstructEssentials, rectangle::WidgetRectangle},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -100,7 +100,7 @@ struct ProfileDisplayName {
|
||||
struct SelectorCell {
|
||||
key: String,
|
||||
display_text: String,
|
||||
icon_path: &'static str,
|
||||
icon_path_builtin: &'static str,
|
||||
}
|
||||
|
||||
struct MultiSelectorParams<'a> {
|
||||
@@ -124,7 +124,7 @@ fn mount_multi_selector(params: MultiSelectorParams) -> anyhow::Result<()> {
|
||||
params.ess,
|
||||
components::button::Params {
|
||||
text: Some(Translation::from_raw_text(&cell.display_text)),
|
||||
sprite_src: Some(AssetPathRef::BuiltIn(cell.icon_path)),
|
||||
sprite_src: Some(strip_path_as(AssetPathSource::BuiltIn, cell.icon_path_builtin).unwrap()),
|
||||
color,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -402,7 +402,7 @@ struct MountDeviceSliderParams<'a> {
|
||||
checked: bool,
|
||||
muted: bool,
|
||||
disp: Option<ProfileDisplayName>,
|
||||
alt_desc: String,
|
||||
device_desc: String,
|
||||
}
|
||||
|
||||
fn push_popup_speakers_set_successfully(globals: &WguiGlobals, frontend_tasks: &FrontendTasks, name: &str) {
|
||||
@@ -775,13 +775,13 @@ impl View {
|
||||
par.insert("device_name", &disp.name);
|
||||
par.insert("device_icon", disp.icon_path);
|
||||
} else {
|
||||
let icon_path = if params.alt_desc.contains("WiVRn") {
|
||||
let icon_path = if params.device_desc.contains("WiVRn") {
|
||||
"@/dashboard/wivrn_head_symbolic.svg"
|
||||
} else {
|
||||
"@/dashboard/binary.svg"
|
||||
};
|
||||
|
||||
par.insert("device_name", ¶ms.alt_desc);
|
||||
par.insert("device_name", ¶ms.device_desc);
|
||||
par.insert("device_icon", icon_path);
|
||||
}
|
||||
|
||||
@@ -802,6 +802,13 @@ impl View {
|
||||
par,
|
||||
)?;
|
||||
|
||||
if params.checked {
|
||||
let rect = data.fetch_widget(¶ms.layout.state, "rect")?;
|
||||
let mut rect = rect.widget.cast::<WidgetRectangle>()?;
|
||||
rect.set_color(&mut params.layout.common(), WguiColorName::BackgroundVariant.into());
|
||||
rect.set_border_color(&mut params.layout.common(), WguiColorName::Primary.into());
|
||||
}
|
||||
|
||||
let mut common = params.layout.common();
|
||||
let checkbox = data.fetch_component_as::<ComponentCheckbox>("checkbox")?;
|
||||
let btn_mute = data.fetch_component_as::<ComponentButton>("btn_mute")?;
|
||||
@@ -858,7 +865,7 @@ impl View {
|
||||
false
|
||||
};
|
||||
|
||||
let alt_desc = sink.description.clone();
|
||||
let device_desc = sink.description.clone();
|
||||
let muted = sink.mute;
|
||||
|
||||
let control = Rc::new(ControlSink::new(self.tasks.clone(), self.on_update.clone(), sink));
|
||||
@@ -870,7 +877,7 @@ impl View {
|
||||
self.mount_device_slider(MountDeviceSliderParams {
|
||||
checked,
|
||||
disp,
|
||||
alt_desc,
|
||||
device_desc,
|
||||
layout,
|
||||
control,
|
||||
muted,
|
||||
@@ -898,7 +905,7 @@ impl View {
|
||||
false
|
||||
};
|
||||
|
||||
let alt_desc = source.description.clone();
|
||||
let device_desc = source.description.clone();
|
||||
let muted = source.mute;
|
||||
|
||||
let control = Rc::new(ControlSource::new(self.tasks.clone(), self.on_update.clone(), source));
|
||||
@@ -910,7 +917,7 @@ impl View {
|
||||
self.mount_device_slider(MountDeviceSliderParams {
|
||||
checked,
|
||||
disp,
|
||||
alt_desc,
|
||||
device_desc,
|
||||
layout,
|
||||
control,
|
||||
muted,
|
||||
@@ -968,7 +975,7 @@ impl View {
|
||||
cells.push(SelectorCell {
|
||||
key: profile_name.clone(),
|
||||
display_text: disp_name.name,
|
||||
icon_path: disp_name.icon_path,
|
||||
icon_path_builtin: disp_name.icon_path,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ use wgui::{
|
||||
};
|
||||
use wlx_common::{
|
||||
config_io,
|
||||
openxr_actions::{OneOrMany, OpenXrInputAction, OpenXrInputProfile, load_xr_input_profiles},
|
||||
openxr_actions::{OneOrMany, OpenXrInputAction, OpenXrInputChordMember, OpenXrInputProfile, load_xr_input_profiles},
|
||||
openxr_bindings_schema::{
|
||||
DEFAULT_BUTTON_THRESHOLDS, XrControllerProfile, XrInputComponent, XrInputSide, XrInputSubpathKind,
|
||||
},
|
||||
@@ -39,8 +39,11 @@ use crate::{
|
||||
enum Task {
|
||||
Save,
|
||||
Cancel,
|
||||
OpenContextMenu(glam::Vec2, Vec<context_menu::Cell>),
|
||||
OpenContextMenu(context_menu::Position, Vec<context_menu::Cell>),
|
||||
UpdateThreshold(Rc<str>, XrInputSide, usize, f32),
|
||||
AddChordMember(Rc<str>, XrInputSide),
|
||||
RemoveChordMember(Rc<str>, XrInputSide, usize),
|
||||
UpdateChordThreshold(Rc<str>, XrInputSide, usize, usize, f32),
|
||||
}
|
||||
|
||||
pub struct Params<'a> {
|
||||
@@ -100,53 +103,113 @@ impl ViewTrait for View {
|
||||
};
|
||||
|
||||
threshold[i] = val;
|
||||
log::warn!("UpdateThreshold {action_name} {side:?} {threshold:?}");
|
||||
}
|
||||
Task::AddChordMember(action_name, output_side) => {
|
||||
if let Some(member) = default_chord_member(self.controller_profile, output_side) {
|
||||
let cur_profile = &mut self.profiles[self.cur_profile_idx];
|
||||
let action_mut = get_action_mut(cur_profile, &action_name);
|
||||
action_chord_mut(action_mut, output_side)
|
||||
.get_or_insert_default()
|
||||
.push(member);
|
||||
self.refresh(par.layout)?;
|
||||
}
|
||||
}
|
||||
Task::RemoveChordMember(action_name, output_side, member_idx) => {
|
||||
let cur_profile = &mut self.profiles[self.cur_profile_idx];
|
||||
let action_mut = get_action_mut(cur_profile, &action_name);
|
||||
let chord = action_chord_mut(action_mut, output_side);
|
||||
let chord_empty = if let Some(members) = chord.as_mut()
|
||||
&& member_idx < members.len()
|
||||
{
|
||||
members.remove(member_idx);
|
||||
members.is_empty()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if chord_empty {
|
||||
*chord = None;
|
||||
}
|
||||
self.refresh(par.layout)?;
|
||||
}
|
||||
Task::UpdateChordThreshold(action_name, output_side, member_idx, i, val) => {
|
||||
let cur_profile = &mut self.profiles[self.cur_profile_idx];
|
||||
let action_mut = get_action_mut(cur_profile, &action_name);
|
||||
if let Some(member) = action_chord_mut(action_mut, output_side)
|
||||
.as_mut()
|
||||
.and_then(|members| members.get_mut(member_idx))
|
||||
{
|
||||
let mut threshold = member.threshold().unwrap_or(DEFAULT_BUTTON_THRESHOLDS);
|
||||
threshold[i] = val;
|
||||
set_chord_member_threshold(member, threshold);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown handling
|
||||
if let TickResult::Action(name) = self.context_menu.tick(par.layout, &mut self.parser_state)?
|
||||
&& let (Some(action), Some(action_name), Some(side), Some(value)) = {
|
||||
let mut s = name.splitn(4, ';');
|
||||
(s.next(), s.next(), s.next(), s.next())
|
||||
} {
|
||||
if let TickResult::Action(name) = self.context_menu.tick(par.layout, &mut self.parser_state)? {
|
||||
let parts = name.split(';').collect::<Vec<_>>();
|
||||
let cur_profile = &mut self.profiles[self.cur_profile_idx];
|
||||
let action_mut = get_action_mut(cur_profile, action_name);
|
||||
let side_mut = if side == "right" {
|
||||
&mut action_mut.right
|
||||
} else {
|
||||
&mut action_mut.left
|
||||
};
|
||||
|
||||
match action {
|
||||
"clear" => {
|
||||
*side_mut = None;
|
||||
}
|
||||
"subpath" => {
|
||||
apply_subpath(side_mut, side, value, self.controller_profile);
|
||||
}
|
||||
"comp" => {
|
||||
apply_comp(side_mut, side, value);
|
||||
}
|
||||
"click" => match value {
|
||||
"triple" => {
|
||||
action_mut.triple_click = Some(true);
|
||||
action_mut.double_click = None;
|
||||
match parts.as_slice() {
|
||||
[action, action_name, side, value] => {
|
||||
let action_mut = get_action_mut(cur_profile, action_name);
|
||||
|
||||
match *action {
|
||||
"clear" => {
|
||||
*action_binding_mut(action_mut, side) = None;
|
||||
}
|
||||
"subpath" => {
|
||||
apply_subpath(
|
||||
action_binding_mut(action_mut, side),
|
||||
side,
|
||||
value,
|
||||
self.controller_profile,
|
||||
);
|
||||
}
|
||||
"comp" => {
|
||||
apply_comp(action_binding_mut(action_mut, side), side, value);
|
||||
}
|
||||
"click" => apply_click_count(action_mut, value),
|
||||
_ => log::warn!("Unknown action {action}"),
|
||||
}
|
||||
"double" => {
|
||||
action_mut.triple_click = None;
|
||||
action_mut.double_click = Some(true);
|
||||
self.refresh(par.layout)?;
|
||||
}
|
||||
[kind, action_name, output_side, member_idx, value] => {
|
||||
let (Ok(output_side), Ok(member_idx)) = (XrInputSide::try_from(*output_side), member_idx.parse::<usize>())
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let action_mut = get_action_mut(cur_profile, action_name);
|
||||
let Some(member) = action_chord_mut(action_mut, output_side)
|
||||
.as_mut()
|
||||
.and_then(|members| members.get_mut(member_idx))
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
match *kind {
|
||||
"chord_hand" => {
|
||||
if let Ok(side) = XrInputSide::try_from(*value) {
|
||||
apply_chord_hand(member, side, self.controller_profile);
|
||||
}
|
||||
}
|
||||
"chord_subpath" => {
|
||||
if let Ok(subpath) = XrInputSubpathKind::try_from(*value) {
|
||||
apply_chord_subpath(member, subpath, self.controller_profile);
|
||||
}
|
||||
}
|
||||
"chord_comp" => {
|
||||
if let Ok(component) = XrInputComponent::try_from(*value) {
|
||||
apply_chord_component(member, component);
|
||||
}
|
||||
}
|
||||
_ => log::warn!("Unknown chord action {kind}"),
|
||||
}
|
||||
_ => {
|
||||
action_mut.triple_click = None;
|
||||
action_mut.double_click = None;
|
||||
}
|
||||
},
|
||||
_ => log::warn!("Unknown action {action}"),
|
||||
self.refresh(par.layout)?;
|
||||
}
|
||||
_ => log::warn!("Malformed bindings context action: {name}"),
|
||||
}
|
||||
|
||||
self.refresh(par.layout)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -413,6 +476,172 @@ fn input_controls_for_action(
|
||||
},
|
||||
)?;
|
||||
|
||||
if &*action != "scroll" {
|
||||
if is_side_agnostic_action(action.as_ref()) {
|
||||
chord_controls_for_output(
|
||||
mp,
|
||||
parent,
|
||||
&action,
|
||||
XrInputSide::Left,
|
||||
current.left_chord.as_deref(),
|
||||
click_type,
|
||||
profile,
|
||||
false,
|
||||
)?;
|
||||
} else {
|
||||
for output_side in [XrInputSide::Left, XrInputSide::Right] {
|
||||
if profile.find_userpath(output_side).is_none() {
|
||||
continue;
|
||||
}
|
||||
let chord = if matches!(output_side, XrInputSide::Right) {
|
||||
current.right_chord.as_deref()
|
||||
} else {
|
||||
current.left_chord.as_deref()
|
||||
};
|
||||
chord_controls_for_output(mp, parent, &action, output_side, chord, click_type, profile, true)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_side_agnostic_action(action: &str) -> bool {
|
||||
matches!(action, "show_hide" | "toggle_dashboard")
|
||||
}
|
||||
|
||||
fn chord_controls_for_output(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
action: &Rc<str>,
|
||||
output_side: XrInputSide,
|
||||
members: Option<&[OpenXrInputChordMember]>,
|
||||
click_type: ClickType,
|
||||
profile: &XrControllerProfile,
|
||||
show_output_side: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let header = horiz_cell(mp.layout, parent)?;
|
||||
|
||||
if show_output_side {
|
||||
wgui_simple::create_icon(
|
||||
mp.layout,
|
||||
header,
|
||||
Vec2::new(24.0, 24.0),
|
||||
AssetPathRef::BuiltIn(&format!("dashboard/hand_{}.svg", output_side.as_ref())),
|
||||
)?;
|
||||
wgui_simple::create_label(mp.layout, header, side_translation(output_side))?;
|
||||
}
|
||||
|
||||
wgui_simple::create_label(
|
||||
mp.layout,
|
||||
header,
|
||||
Translation::from_translation_key("APP_SETTINGS.BINDINGS.CHORD"),
|
||||
)?;
|
||||
clicks_dropdown(mp, header, action.clone(), click_type)?;
|
||||
|
||||
let tasks = mp.tasks.clone();
|
||||
let action_for_add = action.clone();
|
||||
wgui_simple::create_button(wgui_simple::CreateButtonParams {
|
||||
id_parent: header,
|
||||
layout: mp.layout,
|
||||
content: Translation::from_translation_key("APP_SETTINGS.BINDINGS.CHORD_ADD_INPUT"),
|
||||
icon_builtin: AssetPathRef::BuiltIn("dashboard/add.svg"),
|
||||
on_click: Rc::new(move |_common, _event| {
|
||||
tasks.push(Task::AddChordMember(action_for_add.clone(), output_side));
|
||||
Ok(())
|
||||
}),
|
||||
})?;
|
||||
|
||||
for (member_idx, member) in members.unwrap_or_default().iter().enumerate() {
|
||||
chord_member_controls(mp, parent, action, output_side, member_idx, member, profile)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn chord_member_controls(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
action: &Rc<str>,
|
||||
output_side: XrInputSide,
|
||||
member_idx: usize,
|
||||
member: &OpenXrInputChordMember,
|
||||
profile: &XrControllerProfile,
|
||||
) -> anyhow::Result<()> {
|
||||
let row = horiz_cell(mp.layout, parent)?;
|
||||
let parsed = ParsedOpenXrInputPath::try_from(member.path())
|
||||
.log_warn(member.path())
|
||||
.ok();
|
||||
let physical_side = parsed.as_ref().map(|x| x.side).unwrap_or(output_side);
|
||||
|
||||
chord_hand_dropdown(mp, row, action.clone(), output_side, member_idx, profile, physical_side)?;
|
||||
|
||||
let available_subpaths: Rc<[XrInputSubpathKind]> = profile
|
||||
.find_userpath(physical_side)
|
||||
.map(|user_path| {
|
||||
user_path
|
||||
.paths
|
||||
.iter()
|
||||
.filter(|x| !x.kind.get_bool("Hidden").unwrap_or_default())
|
||||
.map(|x| x.kind)
|
||||
.collect::<Vec<_>>()
|
||||
.into()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
chord_subpath_dropdown(
|
||||
mp,
|
||||
row,
|
||||
action.clone(),
|
||||
output_side,
|
||||
member_idx,
|
||||
available_subpaths,
|
||||
parsed.as_ref().map(|x| x.subpath),
|
||||
)?;
|
||||
|
||||
let available_components: Rc<[XrInputComponent]> = parsed
|
||||
.as_ref()
|
||||
.and_then(|parsed| {
|
||||
profile
|
||||
.find_userpath(parsed.side)
|
||||
.and_then(|user_path| user_path.find_subpath(parsed.subpath))
|
||||
})
|
||||
.map(|subpath| subpath.components)
|
||||
.unwrap_or_default()
|
||||
.into();
|
||||
|
||||
chord_component_dropdown(
|
||||
mp,
|
||||
row,
|
||||
action.clone(),
|
||||
output_side,
|
||||
member_idx,
|
||||
available_components,
|
||||
parsed.as_ref().map(|x| x.component),
|
||||
)?;
|
||||
|
||||
let tasks = mp.tasks.clone();
|
||||
let action_for_remove = action.clone();
|
||||
wgui_simple::create_button(wgui_simple::CreateButtonParams {
|
||||
id_parent: row,
|
||||
layout: mp.layout,
|
||||
content: Translation::from_raw_text(""),
|
||||
icon_builtin: AssetPathRef::BuiltIn("dashboard/trash.svg"),
|
||||
on_click: Rc::new(move |_common, _event| {
|
||||
tasks.push(Task::RemoveChordMember(
|
||||
action_for_remove.clone(),
|
||||
output_side,
|
||||
member_idx,
|
||||
));
|
||||
Ok(())
|
||||
}),
|
||||
})?;
|
||||
|
||||
// put threshold slider on its own row
|
||||
if parsed.as_ref().is_some_and(|x| x.component.is_analog()) {
|
||||
chord_threshold_slider(mp, parent, action.clone(), output_side, member_idx, member.threshold())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -501,7 +730,7 @@ fn subpath_dropdown(
|
||||
mp.layout,
|
||||
parent,
|
||||
Vec2::new(32.0, 32.0),
|
||||
AssetPathRef::BuiltIn(&format!("@/dashboard/hand_{}.svg", side.as_ref())),
|
||||
AssetPathRef::BuiltIn(&format!("dashboard/hand_{}.svg", side.as_ref())),
|
||||
)?;
|
||||
|
||||
let current_text = current
|
||||
@@ -552,6 +781,181 @@ fn clicks_dropdown(mp: &mut MacroParams, parent: WidgetID, action: Rc<str>, curr
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn side_translation(side: XrInputSide) -> Translation {
|
||||
Translation::from_translation_key(match side {
|
||||
XrInputSide::Left => "APP_SETTINGS.BINDINGS.LEFT",
|
||||
XrInputSide::Right => "APP_SETTINGS.BINDINGS.RIGHT",
|
||||
})
|
||||
}
|
||||
|
||||
fn chord_hand_dropdown(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
action: Rc<str>,
|
||||
output_side: XrInputSide,
|
||||
member_idx: usize,
|
||||
profile: &XrControllerProfile,
|
||||
current: XrInputSide,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut params = TemplateParams::new();
|
||||
params.insert("tooltip", "APP_SETTINGS.BINDINGS.CHORD_INPUT_HAND");
|
||||
params.insert("min_width", "100");
|
||||
|
||||
let cells = [XrInputSide::Left, XrInputSide::Right]
|
||||
.into_iter()
|
||||
.filter(|side| profile.find_userpath(*side).is_some())
|
||||
.map(|side| {
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(
|
||||
format!(
|
||||
"chord_hand;{};{};{};{}",
|
||||
action,
|
||||
output_side.as_ref(),
|
||||
member_idx,
|
||||
side.as_ref()
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
title: side_translation(side),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
create_dropdown_with_cells(mp, parent, params, side_translation(current), cells)
|
||||
}
|
||||
|
||||
fn chord_subpath_dropdown(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
action: Rc<str>,
|
||||
output_side: XrInputSide,
|
||||
member_idx: usize,
|
||||
available: Rc<[XrInputSubpathKind]>,
|
||||
current: Option<XrInputSubpathKind>,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut params = TemplateParams::new();
|
||||
params.insert("tooltip", "APP_SETTINGS.BINDINGS.SUBPATH");
|
||||
params.insert("min_width", "100");
|
||||
|
||||
let current_text = current
|
||||
.map(|subpath| subpath.translation())
|
||||
.unwrap_or_else(|| Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"));
|
||||
let cells = available
|
||||
.iter()
|
||||
.map(|subpath| {
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(
|
||||
format!(
|
||||
"chord_subpath;{};{};{};{}",
|
||||
action,
|
||||
output_side.as_ref(),
|
||||
member_idx,
|
||||
subpath.as_ref()
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
title: subpath.translation(),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
create_dropdown_with_cells(mp, parent, params, current_text, cells)
|
||||
}
|
||||
|
||||
fn chord_component_dropdown(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
action: Rc<str>,
|
||||
output_side: XrInputSide,
|
||||
member_idx: usize,
|
||||
available: Rc<[XrInputComponent]>,
|
||||
current: Option<XrInputComponent>,
|
||||
) -> anyhow::Result<bool> {
|
||||
if available.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut params = TemplateParams::new();
|
||||
params.insert("text", "・");
|
||||
params.insert("tooltip", "APP_SETTINGS.BINDINGS.COMPONENT");
|
||||
params.insert("min_width", "100");
|
||||
|
||||
let current_text = current
|
||||
.map(|component| component.translation())
|
||||
.unwrap_or_else(|| Translation::from_raw_text_rc(Default::default()));
|
||||
let cells = available
|
||||
.iter()
|
||||
.map(|component| {
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(
|
||||
format!(
|
||||
"chord_comp;{};{};{};{}",
|
||||
action,
|
||||
output_side.as_ref(),
|
||||
member_idx,
|
||||
component.as_ref()
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
title: component.translation(),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
create_dropdown_with_cells(mp, parent, params, current_text, cells)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn chord_threshold_slider(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
action: Rc<str>,
|
||||
output_side: XrInputSide,
|
||||
member_idx: usize,
|
||||
current: Option<[f32; 2]>,
|
||||
) -> anyhow::Result<()> {
|
||||
let id = mp.idx.to_string();
|
||||
mp.idx += 1;
|
||||
|
||||
let current = current.unwrap_or(DEFAULT_BUTTON_THRESHOLDS);
|
||||
|
||||
let mut params = TemplateParams::new();
|
||||
params.insert("id", &id);
|
||||
params.insert("tooltip", "APP_SETTINGS.BINDINGS.THRESHOLD");
|
||||
params.insert_str("value", format!("{:.2}", current[0]));
|
||||
params.insert_str("value2", format!("{:.2}", current[1]));
|
||||
params.insert("min", "0.0");
|
||||
params.insert("max", "1.0");
|
||||
params.insert("step", "0.1");
|
||||
|
||||
mp.parser_state
|
||||
.instantiate_template(mp.doc_params, "ThresholdSlider", mp.layout, parent, params)?;
|
||||
|
||||
let slider = mp.parser_state.fetch_component_as::<ComponentSlider>(&id)?;
|
||||
slider.on_value_changed(Box::new({
|
||||
let tasks = mp.tasks.clone();
|
||||
move |_common, e| {
|
||||
let threshold_idx = if matches!(e.index, wgui::components::slider::ValueIndex::Primary) {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
};
|
||||
tasks.push(Task::UpdateChordThreshold(
|
||||
action.clone(),
|
||||
output_side,
|
||||
member_idx,
|
||||
threshold_idx,
|
||||
e.value,
|
||||
));
|
||||
}
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn threshold_slider(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
@@ -594,11 +998,43 @@ fn threshold_slider(
|
||||
fn create_dropdown<B: 'static + BindingsDropdown>(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
mut params: TemplateParams,
|
||||
params: TemplateParams,
|
||||
action: Rc<str>,
|
||||
side: XrInputSide,
|
||||
current_text: Translation,
|
||||
available: Rc<[B]>,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut cells = available
|
||||
.iter()
|
||||
.map(|item| {
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(item.action_str(&action, side)),
|
||||
title: item.translation(),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(action_str) = B::clear_str(&action, side) {
|
||||
cells.insert(
|
||||
0,
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(action_str),
|
||||
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
create_dropdown_with_cells(mp, parent, params, current_text, cells)
|
||||
}
|
||||
|
||||
fn create_dropdown_with_cells(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
mut params: TemplateParams,
|
||||
current_text: Translation,
|
||||
cells: Vec<context_menu::Cell>,
|
||||
) -> anyhow::Result<()> {
|
||||
let id = mp.idx.to_string();
|
||||
mp.idx += 1;
|
||||
@@ -617,35 +1053,9 @@ fn create_dropdown<B: 'static + BindingsDropdown>(
|
||||
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
|
||||
btn.on_click(Rc::new({
|
||||
let tasks = mp.tasks.clone();
|
||||
let available = available.clone();
|
||||
let cells = cells.clone();
|
||||
move |_common, e: ButtonClickEvent| {
|
||||
let mut cells = available
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let title = item.translation();
|
||||
|
||||
context_menu::Cell {
|
||||
action_name: Some(item.action_str(&action, side)),
|
||||
title,
|
||||
tooltip: None,
|
||||
attribs: vec![],
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(action_str) = B::clear_str(&action, side) {
|
||||
cells.insert(
|
||||
0,
|
||||
context_menu::Cell {
|
||||
action_name: Some(action_str),
|
||||
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
|
||||
tooltip: None,
|
||||
attribs: vec![],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
tasks.push(Task::OpenContextMenu(e.mouse_pos_absolute.unwrap_or_default(), cells));
|
||||
tasks.push(Task::OpenContextMenu(e.mouse_pos_absolute.into(), cells.clone()));
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
@@ -710,3 +1120,165 @@ fn apply_comp(side_mut: &mut Option<OneOrMany<String>>, side: &str, comp: &str)
|
||||
|
||||
*side_mut = Some(OneOrMany::One(format!("/user/hand/{side}/input/{subpath}/{comp}")));
|
||||
}
|
||||
|
||||
fn action_chord_mut(
|
||||
action: &mut OpenXrInputAction,
|
||||
output_side: XrInputSide,
|
||||
) -> &mut Option<Vec<OpenXrInputChordMember>> {
|
||||
match output_side {
|
||||
XrInputSide::Left => &mut action.left_chord,
|
||||
XrInputSide::Right => &mut action.right_chord,
|
||||
}
|
||||
}
|
||||
|
||||
fn action_binding_mut<'a>(action: &'a mut OpenXrInputAction, side: &str) -> &'a mut Option<OneOrMany<String>> {
|
||||
if side == "right" {
|
||||
&mut action.right
|
||||
} else {
|
||||
&mut action.left
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_click_count(action: &mut OpenXrInputAction, value: &str) {
|
||||
match value {
|
||||
"triple" => {
|
||||
action.triple_click = Some(true);
|
||||
action.double_click = None;
|
||||
}
|
||||
"double" => {
|
||||
action.triple_click = None;
|
||||
action.double_click = Some(true);
|
||||
}
|
||||
_ => {
|
||||
action.triple_click = None;
|
||||
action.double_click = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_chord_member(profile: &XrControllerProfile, preferred_side: XrInputSide) -> Option<OpenXrInputChordMember> {
|
||||
let other_side = match preferred_side {
|
||||
XrInputSide::Left => XrInputSide::Right,
|
||||
XrInputSide::Right => XrInputSide::Left,
|
||||
};
|
||||
|
||||
for side in [preferred_side, other_side] {
|
||||
let Some(user_path) = profile.find_userpath(side) else {
|
||||
continue;
|
||||
};
|
||||
let Some(subpath) = user_path
|
||||
.paths
|
||||
.iter()
|
||||
.find(|subpath| !subpath.kind.get_bool("Hidden").unwrap_or_default() && !subpath.components.is_empty())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let component = *subpath.components.first()?;
|
||||
return Some(OpenXrInputChordMember::Path(format!(
|
||||
"/user/hand/{}/input/{}/{}",
|
||||
side.as_ref(),
|
||||
subpath.kind.as_ref(),
|
||||
component.as_ref()
|
||||
)));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn set_chord_member_path(member: &mut OpenXrInputChordMember, path: String, component: XrInputComponent) {
|
||||
let threshold = if component.is_analog() {
|
||||
member.threshold()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
*member = if threshold.is_some() {
|
||||
OpenXrInputChordMember::Detailed { path, threshold }
|
||||
} else {
|
||||
OpenXrInputChordMember::Path(path)
|
||||
};
|
||||
}
|
||||
|
||||
fn set_chord_member_threshold(member: &mut OpenXrInputChordMember, threshold: [f32; 2]) {
|
||||
*member = OpenXrInputChordMember::Detailed {
|
||||
path: member.path().to_owned(),
|
||||
threshold: Some(threshold),
|
||||
};
|
||||
}
|
||||
|
||||
fn apply_chord_hand(member: &mut OpenXrInputChordMember, side: XrInputSide, profile: &XrControllerProfile) {
|
||||
let parsed = ParsedOpenXrInputPath::try_from(member.path()).ok();
|
||||
let Some(user_path) = profile.find_userpath(side) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let subpath = parsed
|
||||
.and_then(|parsed| user_path.find_subpath(parsed.subpath))
|
||||
.filter(|subpath| !subpath.kind.get_bool("Hidden").unwrap_or_default() && !subpath.components.is_empty())
|
||||
.or_else(|| {
|
||||
user_path
|
||||
.paths
|
||||
.iter()
|
||||
.find(|subpath| !subpath.kind.get_bool("Hidden").unwrap_or_default() && !subpath.components.is_empty())
|
||||
});
|
||||
let Some(subpath) = subpath else {
|
||||
return;
|
||||
};
|
||||
|
||||
let component = parsed
|
||||
.map(|parsed| parsed.component)
|
||||
.filter(|component| subpath.components.contains(component))
|
||||
.unwrap_or(subpath.components[0]);
|
||||
let path = format!(
|
||||
"/user/hand/{}/input/{}/{}",
|
||||
side.as_ref(),
|
||||
subpath.kind.as_ref(),
|
||||
component.as_ref()
|
||||
);
|
||||
set_chord_member_path(member, path, component);
|
||||
}
|
||||
|
||||
fn apply_chord_subpath(
|
||||
member: &mut OpenXrInputChordMember,
|
||||
subpath: XrInputSubpathKind,
|
||||
profile: &XrControllerProfile,
|
||||
) {
|
||||
let Ok(parsed) = ParsedOpenXrInputPath::try_from(member.path()) else {
|
||||
return;
|
||||
};
|
||||
let Some(subpath_obj) = profile
|
||||
.find_userpath(parsed.side)
|
||||
.and_then(|user_path| user_path.find_subpath(subpath))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(first_component) = subpath_obj.components.first().copied() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let component = if subpath_obj.components.contains(&parsed.component) {
|
||||
parsed.component
|
||||
} else {
|
||||
first_component
|
||||
};
|
||||
let path = format!(
|
||||
"/user/hand/{}/input/{}/{}",
|
||||
parsed.side.as_ref(),
|
||||
subpath.as_ref(),
|
||||
component.as_ref()
|
||||
);
|
||||
set_chord_member_path(member, path, component);
|
||||
}
|
||||
|
||||
fn apply_chord_component(member: &mut OpenXrInputChordMember, component: XrInputComponent) {
|
||||
let Ok(parsed) = ParsedOpenXrInputPath::try_from(member.path()) else {
|
||||
return;
|
||||
};
|
||||
let path = format!(
|
||||
"/user/hand/{}/input/{}/{}",
|
||||
parsed.side.as_ref(),
|
||||
parsed.subpath.as_ref(),
|
||||
component.as_ref()
|
||||
);
|
||||
set_chord_member_path(member, path, component);
|
||||
}
|
||||
@@ -32,6 +32,21 @@ enum Task {
|
||||
ActionClicked(&'static str),
|
||||
}
|
||||
|
||||
pub fn get_entries_yes_no() -> Vec<ButtonEntry> {
|
||||
vec![
|
||||
ButtonEntry {
|
||||
content: Translation::from_translation_key("YES"),
|
||||
icon: "@/dashboard/check.svg",
|
||||
action: "yes",
|
||||
},
|
||||
ButtonEntry {
|
||||
content: Translation::from_translation_key("NO"),
|
||||
icon: "@/dashboard/cancel.svg",
|
||||
action: "no",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
tasks: Tasks<Task>,
|
||||
|
||||
@@ -52,6 +67,13 @@ fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> {
|
||||
|
||||
impl ViewTrait for View {
|
||||
fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
self.tick();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub fn tick(&mut self) {
|
||||
for task in self.tasks.drain() {
|
||||
match task {
|
||||
Task::ActionClicked(action) => {
|
||||
@@ -65,11 +87,8 @@ impl ViewTrait for View {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub fn new(
|
||||
layout: &mut Layout,
|
||||
id_parent: WidgetID,
|
||||
|
||||
@@ -17,7 +17,5 @@ glam.workspace = true
|
||||
log.workspace = true
|
||||
rust-embed.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
vulkano.workspace = true
|
||||
vulkano-shaders.workspace = true
|
||||
winit.workspace = true
|
||||
ureq = "3.3.0"
|
||||
+45
-144
@@ -1,25 +1,14 @@
|
||||
use glam::{Vec2, vec2};
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
use std::rc::Rc;
|
||||
use testbed::{Testbed, testbed_any::TestbedAny};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::filter::LevelFilter;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use vulkan::init_window;
|
||||
use vulkano::{
|
||||
Validated, VulkanError,
|
||||
command_buffer::CommandBufferUsage,
|
||||
format::Format,
|
||||
image::{ImageUsage, view::ImageView},
|
||||
swapchain::{
|
||||
ColorSpace, CompositeAlpha, PresentMode, Surface, SurfaceInfo, Swapchain, SwapchainCreateInfo,
|
||||
SwapchainPresentInfo, acquire_next_image,
|
||||
},
|
||||
sync::GpuFuture,
|
||||
};
|
||||
use wgui::{
|
||||
event::{MouseButtonEvent, MouseButtonIndex, MouseMotionEvent, MouseWheelEvent},
|
||||
gfx::{WGfx, cmd::WGfxClearMode},
|
||||
gfx::{CommandBufferUsage, PresentStatus, SwapchainError, WGfxClearMode},
|
||||
renderer_vk::{self},
|
||||
};
|
||||
use winit::{
|
||||
@@ -59,9 +48,9 @@ fn init_logging() {
|
||||
.init();
|
||||
}
|
||||
|
||||
fn load_testbed(audio_sample_player: &mut audio::SamplePlayer) -> anyhow::Result<Box<dyn Testbed>> {
|
||||
fn load_testbed(audio: &mut Box<dyn audio::AudioProvider>) -> anyhow::Result<Box<dyn Testbed>> {
|
||||
let mut assets = Box::new(assets::Asset {});
|
||||
audio_sample_player.register_wgui_samples(assets.as_mut())?;
|
||||
audio.register_wgui_samples(assets.as_mut())?;
|
||||
|
||||
let name = std::env::var("TESTBED").unwrap_or_default();
|
||||
Ok(match name.as_str() {
|
||||
@@ -74,41 +63,19 @@ fn load_testbed(audio_sample_player: &mut audio::SamplePlayer) -> anyhow::Result
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
init_logging();
|
||||
|
||||
let (gfx, event_loop, window, surface, color_space) =
|
||||
let (gfx, event_loop, window) =
|
||||
init_window("[-/=]: gui scale, F10: debug draw, F11: print tree")?;
|
||||
let inner_size = window.inner_size();
|
||||
let mut swapchain_size = [inner_size.width, inner_size.height];
|
||||
|
||||
let mut swapchain_create_info = swapchain_create_info(
|
||||
&gfx,
|
||||
gfx.surface_format,
|
||||
color_space,
|
||||
surface.clone(),
|
||||
swapchain_size,
|
||||
);
|
||||
|
||||
let (mut swapchain, mut images) = {
|
||||
let (swapchain, images) = Swapchain::new(
|
||||
gfx.device.clone(),
|
||||
surface.clone(),
|
||||
swapchain_create_info.clone(),
|
||||
)?;
|
||||
|
||||
let image_views = images
|
||||
.into_iter()
|
||||
.map(|image| ImageView::new_default(image).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(swapchain, image_views)
|
||||
};
|
||||
let mut swapchain = gfx.create_swapchain(swapchain_size)?;
|
||||
swapchain_size = swapchain.extent();
|
||||
|
||||
let mut recreate = false;
|
||||
|
||||
let mut scale = window.scale_factor() as f32;
|
||||
|
||||
let mut audio_system = audio::AudioSystem::new();
|
||||
let mut audio_sample_player = audio::SamplePlayer::new();
|
||||
let mut testbed = load_testbed(&mut audio_sample_player)?;
|
||||
let mut audio: Box<dyn audio::AudioProvider> = Box::new(audio::RealAudioProvider::new());
|
||||
let mut testbed = load_testbed(&mut audio)?;
|
||||
|
||||
let mut mouse = Vec2::ZERO;
|
||||
|
||||
@@ -281,20 +248,21 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
} => {
|
||||
if recreate {
|
||||
let inner_size = window.inner_size();
|
||||
swapchain_size = [inner_size.width, inner_size.height];
|
||||
if inner_size.width == 0 || inner_size.height == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
swapchain_create_info.image_extent = swapchain_size;
|
||||
|
||||
(swapchain, images) = {
|
||||
let (swapchain, images) = swapchain.recreate(swapchain_create_info.clone()).unwrap();
|
||||
|
||||
let image_views = images
|
||||
.into_iter()
|
||||
.map(|image| ImageView::new_default(image).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(swapchain, image_views)
|
||||
};
|
||||
let requested_size = [inner_size.width, inner_size.height];
|
||||
match swapchain.recreate(requested_size) {
|
||||
Ok(new_swapchain) => {
|
||||
swapchain = new_swapchain;
|
||||
swapchain_size = swapchain.extent();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("failed to recreate swapchain: {e:#}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
render_context
|
||||
.update_viewport(&mut shared_context, swapchain_size, scale)
|
||||
@@ -314,8 +282,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
width: (swapchain_size[0] as f32 / scale) as _,
|
||||
height: (swapchain_size[1] as f32 / scale) as _,
|
||||
timestep_alpha: timestep.alpha,
|
||||
audio_system: &mut audio_system,
|
||||
audio_sample_player: &mut audio_sample_player,
|
||||
audio: &mut audio,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -332,20 +299,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
profiler.start();
|
||||
|
||||
{
|
||||
let (image_index, _, acquire_future) =
|
||||
match acquire_next_image(swapchain.clone(), None).map_err(Validated::unwrap) {
|
||||
Ok(r) => r,
|
||||
Err(VulkanError::OutOfDate) => {
|
||||
recreate = true;
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("failed to acquire next image: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let frame = match swapchain.acquire() {
|
||||
Ok(frame) => frame,
|
||||
Err(SwapchainError::OutOfDate) => {
|
||||
recreate = true;
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("failed to acquire next image: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let tgt = images[image_index as usize].clone();
|
||||
let tgt = frame.image_view().clone();
|
||||
|
||||
let mut cmd_buf = gfx
|
||||
.create_gfx_command_buffer(CommandBufferUsage::OneTimeSubmit)
|
||||
@@ -388,17 +354,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
let cmd_buf = cmd_buf.build().unwrap();
|
||||
|
||||
acquire_future
|
||||
.then_execute(gfx.queue_gfx.clone(), cmd_buf)
|
||||
.unwrap()
|
||||
.then_swapchain_present(
|
||||
gfx.queue_gfx.clone(),
|
||||
SwapchainPresentInfo::swapchain_image_index(swapchain.clone(), image_index),
|
||||
)
|
||||
.then_signal_fence_and_flush()
|
||||
.unwrap()
|
||||
.wait(None)
|
||||
.unwrap();
|
||||
match frame.present(&cmd_buf) {
|
||||
Ok(PresentStatus::Optimal) => {}
|
||||
Ok(PresentStatus::Suboptimal) | Err(SwapchainError::OutOfDate) => {
|
||||
recreate = true;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("failed to present swapchain image: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
profiler.end();
|
||||
@@ -414,66 +378,3 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn swapchain_create_info(
|
||||
graphics: &WGfx,
|
||||
format: Format,
|
||||
color_space: ColorSpace,
|
||||
surface: Arc<Surface>,
|
||||
extent: [u32; 2],
|
||||
) -> SwapchainCreateInfo {
|
||||
let surface_capabilities = graphics
|
||||
.device
|
||||
.physical_device()
|
||||
.surface_capabilities(&surface, SurfaceInfo::default())
|
||||
.unwrap(); // want panic
|
||||
|
||||
let mut composite_alpha = None;
|
||||
for c in [
|
||||
CompositeAlpha::PreMultiplied,
|
||||
CompositeAlpha::PostMultiplied,
|
||||
CompositeAlpha::Opaque,
|
||||
] {
|
||||
if surface_capabilities
|
||||
.supported_composite_alpha
|
||||
.contains_enum(c)
|
||||
{
|
||||
composite_alpha = Some(c);
|
||||
break;
|
||||
}
|
||||
log::warn!(
|
||||
"GPU driver doesn't support {c:?} compositeAlpha! Desktop window will be blended using a fallback method and may look different than in VR."
|
||||
);
|
||||
}
|
||||
let composite_alpha = composite_alpha
|
||||
.expect("GPU driver issue: VkSurfaceCapabilitiesKHR has empty supportedCompositeAlpha.");
|
||||
|
||||
let present_modes = graphics
|
||||
.device
|
||||
.physical_device()
|
||||
.surface_present_modes(&surface, SurfaceInfo::default())
|
||||
.expect("Could not get GPU present modes for VKSurface.");
|
||||
|
||||
let mut present_mode = None;
|
||||
for pm in [PresentMode::Mailbox, PresentMode::Fifo] {
|
||||
if present_modes.contains(&pm) {
|
||||
present_mode = Some(pm);
|
||||
break;
|
||||
}
|
||||
log::warn!(
|
||||
"GPU driver doesn't support {pm:?} presentMode! Desktop window may have a higher latency and/or glitch during grab/resize."
|
||||
);
|
||||
}
|
||||
let present_mode = present_mode.expect("GPU driver issue: VkPresentModeKHR FIFO is not supported even though it's required by Vulkan spec.");
|
||||
|
||||
SwapchainCreateInfo {
|
||||
min_image_count: surface_capabilities.min_image_count.max(2),
|
||||
present_mode,
|
||||
image_format: format,
|
||||
image_extent: extent,
|
||||
image_usage: ImageUsage::COLOR_ATTACHMENT,
|
||||
image_color_space: color_space,
|
||||
composite_alpha,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,12 @@ pub struct TestbedUpdateParams<'a> {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
pub timestep_alpha: f32,
|
||||
pub audio_system: &'a mut audio::AudioSystem,
|
||||
pub audio_sample_player: &'a mut audio::SamplePlayer,
|
||||
pub audio: &'a mut Box<dyn audio::AudioProvider>,
|
||||
}
|
||||
|
||||
impl<'a> TestbedUpdateParams<'a> {
|
||||
pub fn process_layout_result(&mut self, res: LayoutUpdateResult) {
|
||||
self
|
||||
.audio_sample_player
|
||||
.play_wgui_samples(self.audio_system, res.sounds_to_play);
|
||||
self.audio.play_wgui_samples(res.sounds_to_play);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,9 +41,7 @@ impl Testbed for TestbedDashboard {
|
||||
height: params.height,
|
||||
timestep_alpha: params.timestep_alpha,
|
||||
})?;
|
||||
self
|
||||
.frontend
|
||||
.process_update(res, params.audio_system, params.audio_sample_player)?;
|
||||
self.frontend.process_update(res, params.audio)?;
|
||||
|
||||
while self.executor.try_tick() {}
|
||||
Ok(())
|
||||
|
||||
@@ -33,7 +33,7 @@ use wlx_common::locale::WayVRLangProvider;
|
||||
#[derive(Clone)]
|
||||
pub enum TestbedTask {
|
||||
ShowPopup,
|
||||
ShowContextMenu(Vec2),
|
||||
ShowContextMenu(context_menu::Position),
|
||||
}
|
||||
|
||||
struct Data {
|
||||
@@ -196,7 +196,7 @@ impl TestbedGeneric {
|
||||
button_context_menu.on_click({
|
||||
let tasks = testbed.tasks.clone();
|
||||
Rc::new(move |_common, m| {
|
||||
tasks.push(TestbedTask::ShowContextMenu(m.boundary.bottom_left()));
|
||||
tasks.push(TestbedTask::ShowContextMenu(m.boundary.into()));
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
@@ -212,7 +212,9 @@ impl TestbedGeneric {
|
||||
) -> anyhow::Result<()> {
|
||||
match task {
|
||||
TestbedTask::ShowPopup => self.show_popup(params, data)?,
|
||||
TestbedTask::ShowContextMenu(position) => self.show_context_menu(params, data, *position),
|
||||
TestbedTask::ShowContextMenu(position) => {
|
||||
self.show_context_menu(params, data, position.clone())
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -251,7 +253,7 @@ impl TestbedGeneric {
|
||||
&mut self,
|
||||
_params: &mut TestbedUpdateParams,
|
||||
data: &mut Data,
|
||||
position: Vec2,
|
||||
position: context_menu::Position,
|
||||
) {
|
||||
data.context_menu.open(context_menu::OpenParams {
|
||||
on_custom_attribs: Some(Rc::new(move |custom_attribs| {
|
||||
|
||||
+13
-197
@@ -1,46 +1,11 @@
|
||||
use anyhow::Context;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use vulkano::{
|
||||
device::{
|
||||
Device, DeviceCreateInfo, DeviceExtensions, DeviceFeatures, Queue, QueueCreateInfo, QueueFlags,
|
||||
physical::{PhysicalDevice, PhysicalDeviceType},
|
||||
},
|
||||
instance::{Instance, InstanceCreateInfo},
|
||||
swapchain::ColorSpace,
|
||||
};
|
||||
use wgui::gfx::WGfx;
|
||||
use std::sync::Arc;
|
||||
|
||||
static VULKAN_LIBRARY: OnceLock<Arc<vulkano::VulkanLibrary>> = OnceLock::new();
|
||||
fn get_vulkan_library() -> &'static Arc<vulkano::VulkanLibrary> {
|
||||
VULKAN_LIBRARY.get_or_init(|| vulkano::VulkanLibrary::new().unwrap()) // want panic
|
||||
}
|
||||
use wgui::gfx::{WGfx, WGfxCreateInfo};
|
||||
use winit::{event_loop::EventLoop, window::Window};
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn init_window(
|
||||
title: &str,
|
||||
) -> anyhow::Result<(
|
||||
Arc<WGfx>,
|
||||
winit::event_loop::EventLoop<()>,
|
||||
Arc<winit::window::Window>,
|
||||
Arc<vulkano::swapchain::Surface>,
|
||||
ColorSpace,
|
||||
)> {
|
||||
use vulkano::{instance::InstanceCreateFlags, swapchain::Surface};
|
||||
use winit::{event_loop::EventLoop, window::Window};
|
||||
|
||||
pub fn init_window(title: &str) -> anyhow::Result<(Arc<WGfx>, EventLoop<()>, Arc<Window>)> {
|
||||
let event_loop = EventLoop::new().unwrap(); // want panic
|
||||
let mut vk_instance_extensions = Surface::required_extensions(&event_loop).unwrap();
|
||||
vk_instance_extensions.khr_get_physical_device_properties2 = true;
|
||||
log::debug!("Instance exts for runtime: {:?}", vk_instance_extensions);
|
||||
|
||||
let instance = Instance::new(
|
||||
get_vulkan_library().clone(),
|
||||
InstanceCreateInfo {
|
||||
flags: InstanceCreateFlags::ENUMERATE_PORTABILITY,
|
||||
enabled_extensions: vk_instance_extensions,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
#[allow(deprecated)]
|
||||
let window = Arc::new(
|
||||
@@ -52,169 +17,20 @@ pub fn init_window(
|
||||
)
|
||||
.unwrap(), // want panic
|
||||
);
|
||||
let surface = Surface::from_window(instance.clone(), window.clone())?;
|
||||
|
||||
let mut device_extensions = DeviceExtensions::empty();
|
||||
device_extensions.khr_swapchain = true;
|
||||
|
||||
log::debug!("Device exts for app: {:?}", device_extensions);
|
||||
|
||||
let (physical_device, mut my_extensions, queue_families) = instance
|
||||
.enumerate_physical_devices()?
|
||||
.filter_map(|p| {
|
||||
if p.supported_extensions().contains(&device_extensions) {
|
||||
Some((p, device_extensions))
|
||||
} else {
|
||||
log::debug!(
|
||||
"Not using {} because it does not implement the following device extensions:",
|
||||
p.properties().device_name,
|
||||
);
|
||||
for (ext, missing) in p.supported_extensions().difference(&device_extensions) {
|
||||
if missing {
|
||||
log::debug!(" {ext}");
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter_map(|(p, my_extensions)| {
|
||||
try_all_queue_families(p.as_ref()).map(|families| (p, my_extensions, families))
|
||||
})
|
||||
.min_by_key(|(p, _, _)| prio_from_device_type(p))
|
||||
.expect("no suitable physical device found");
|
||||
|
||||
log::info!(
|
||||
"Using vkPhysicalDevice: {}",
|
||||
physical_device.properties().device_name,
|
||||
);
|
||||
|
||||
if physical_device.supported_extensions().img_filter_cubic {
|
||||
my_extensions.img_filter_cubic = true;
|
||||
log::info!("img_filter_cubic!");
|
||||
}
|
||||
|
||||
let (format, color_space) = physical_device
|
||||
.surface_formats(&surface, vulkano::swapchain::SurfaceInfo::default())?
|
||||
.into_iter()
|
||||
.find(|(format, _)| !format.components().iter().any(|b| *b > 8))
|
||||
.context("Could not read or no suitable surface formats for PhysicalDevice")?;
|
||||
|
||||
let (device, queues) = Device::new(
|
||||
physical_device,
|
||||
DeviceCreateInfo {
|
||||
enabled_extensions: my_extensions,
|
||||
enabled_features: DeviceFeatures {
|
||||
dynamic_rendering: true,
|
||||
descriptor_binding_sampled_image_update_after_bind: true,
|
||||
..DeviceFeatures::empty()
|
||||
},
|
||||
queue_create_infos: queue_families
|
||||
.iter()
|
||||
.map(|fam| QueueCreateInfo {
|
||||
queue_family_index: fam.queue_family_index,
|
||||
queues: fam.priorities.clone(),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
let gfx = WGfx::new_for_window(
|
||||
window.as_ref(),
|
||||
&WGfxCreateInfo {
|
||||
application_name: "uidev".to_owned(),
|
||||
engine_name: "wgui".to_owned(),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
let (queue_gfx, queue_xfer, _) = unwrap_queues(queues.collect());
|
||||
|
||||
let me = WGfx::new_from_raw(instance, device, queue_gfx, queue_xfer, format);
|
||||
Ok((me, event_loop, window, surface, color_space))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct QueueFamilyLayout {
|
||||
queue_family_index: u32,
|
||||
priorities: Vec<f32>,
|
||||
}
|
||||
|
||||
fn prio_from_device_type(physical_device: &PhysicalDevice) -> u32 {
|
||||
match physical_device.properties().device_type {
|
||||
PhysicalDeviceType::DiscreteGpu => 0,
|
||||
PhysicalDeviceType::IntegratedGpu => 1,
|
||||
PhysicalDeviceType::VirtualGpu => 2,
|
||||
PhysicalDeviceType::Cpu => 3,
|
||||
_ => 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_queues(queues: Vec<Arc<Queue>>) -> (Arc<Queue>, Arc<Queue>, Option<Arc<Queue>>) {
|
||||
match queues[..] {
|
||||
[ref g, ref t, ref c] => (g.clone(), t.clone(), Some(c.clone())),
|
||||
[ref gt, ref c] => (gt.clone(), gt.clone(), Some(c.clone())),
|
||||
[ref gt] => (gt.clone(), gt.clone(), None),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_all_queue_families(physical_device: &PhysicalDevice) -> Option<Vec<QueueFamilyLayout>> {
|
||||
queue_families_priorities(
|
||||
physical_device,
|
||||
vec![
|
||||
// main-thread graphics + uploads
|
||||
QueueFlags::GRAPHICS | QueueFlags::TRANSFER,
|
||||
// capture-thread uploads
|
||||
QueueFlags::TRANSFER,
|
||||
],
|
||||
)
|
||||
.or_else(|| {
|
||||
queue_families_priorities(
|
||||
physical_device,
|
||||
vec![
|
||||
// main thread graphics
|
||||
QueueFlags::GRAPHICS,
|
||||
// main thread uploads
|
||||
QueueFlags::TRANSFER,
|
||||
// capture thread uploads
|
||||
QueueFlags::TRANSFER,
|
||||
],
|
||||
)
|
||||
})
|
||||
.or_else(|| {
|
||||
queue_families_priorities(
|
||||
physical_device,
|
||||
// main thread-only. software capture not supported.
|
||||
vec![QueueFlags::GRAPHICS | QueueFlags::TRANSFER],
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn queue_families_priorities(
|
||||
physical_device: &PhysicalDevice,
|
||||
mut requested_queues: Vec<QueueFlags>,
|
||||
) -> Option<Vec<QueueFamilyLayout>> {
|
||||
let mut result = Vec::with_capacity(3);
|
||||
|
||||
for (idx, props) in physical_device.queue_family_properties().iter().enumerate() {
|
||||
let mut remaining = props.queue_count;
|
||||
let mut want = 0usize;
|
||||
|
||||
requested_queues.retain(|requested| {
|
||||
if props.queue_flags.intersects(*requested) && remaining > 0 {
|
||||
remaining -= 1;
|
||||
want += 1;
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if want > 0 {
|
||||
result.push(QueueFamilyLayout {
|
||||
queue_family_index: idx as u32,
|
||||
priorities: std::iter::repeat_n(1.0, want).collect(),
|
||||
});
|
||||
}
|
||||
log::info!("Using vkPhysicalDevice: {}", gfx.device_info().name);
|
||||
if gfx.capabilities().filter_cubic {
|
||||
log::info!("cubic filtering available");
|
||||
}
|
||||
|
||||
if requested_queues.is_empty() {
|
||||
log::debug!("Selected GPU queue families: {result:?}");
|
||||
Some(result)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Ok((gfx, event_loop, window))
|
||||
}
|
||||
+65
-5
@@ -321,21 +321,34 @@ impl WayVRClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fn_wvr_window_list(
|
||||
pub async fn fn_wlx_overlay_list(
|
||||
client: WayVRClientMutex,
|
||||
serial: Serial,
|
||||
) -> anyhow::Result<Option<Vec<packet_server::WvrWindow>>> {
|
||||
params: packet_client::WlxOverlayListParams,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
Ok(
|
||||
send_and_wait!(
|
||||
client,
|
||||
serial,
|
||||
&PacketClient::WvrWindowList(serial),
|
||||
WvrWindowListResponse
|
||||
&PacketClient::WlxOverlayList(serial, params),
|
||||
WlxOverlayListResponse
|
||||
)
|
||||
.map(|res| res.list),
|
||||
.list,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn fn_wlx_overlay_set_visible(
|
||||
client: WayVRClientMutex,
|
||||
overlay: String,
|
||||
visible: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
send_only!(
|
||||
client,
|
||||
&PacketClient::WlxOverlaySetVisible(overlay, visible)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fn_wvr_window_set_visible(
|
||||
client: WayVRClientMutex,
|
||||
handle: packet_server::WvrWindowHandle,
|
||||
@@ -449,6 +462,53 @@ impl WayVRClient {
|
||||
send_only!(client, &PacketClient::WlxModifyPanel(params));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fn_wlx_window_state_get(
|
||||
client: WayVRClientMutex,
|
||||
serial: Serial,
|
||||
params: packet_client::WlxWindowStateGetParams,
|
||||
) -> anyhow::Result<Result<packet_client::WlxWindowStateValue, String>> {
|
||||
Ok(send_and_wait!(
|
||||
client,
|
||||
serial,
|
||||
&PacketClient::WlxWindowStateGet(serial, params),
|
||||
WlxWindowStateGetResponse
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn fn_wlx_window_state_set(
|
||||
client: WayVRClientMutex,
|
||||
params: packet_client::WlxWindowStateSetParams,
|
||||
) -> anyhow::Result<()> {
|
||||
send_only!(client, &PacketClient::WlxWindowStateSet(params));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fn_wlx_window_attrib_get(
|
||||
client: WayVRClientMutex,
|
||||
serial: Serial,
|
||||
params: packet_client::WlxWindowAttribGetParams,
|
||||
) -> anyhow::Result<Result<packet_client::WlxWindowAttribValue, String>> {
|
||||
Ok(send_and_wait!(
|
||||
client,
|
||||
serial,
|
||||
&PacketClient::WlxWindowAttribGet(serial, params),
|
||||
WlxWindowAttribGetResponse
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn fn_wlx_window_attrib_set(
|
||||
client: WayVRClientMutex,
|
||||
serial: Serial,
|
||||
params: packet_client::WlxWindowAttribSetParams,
|
||||
) -> anyhow::Result<Result<(), String>> {
|
||||
Ok(send_and_wait!(
|
||||
client,
|
||||
serial,
|
||||
&PacketClient::WlxWindowAttribSet(serial, params),
|
||||
WlxWindowAttribSetResponse
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WayVRClient {
|
||||
|
||||
@@ -22,7 +22,7 @@ impl SerialGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 4;
|
||||
pub const PROTOCOL_VERSION: u32 = 5;
|
||||
pub const CONNECTION_MAGIC: &str = "wayvr_ipc";
|
||||
|
||||
pub fn data_encode<T>(data: &T) -> Vec<u8>
|
||||
|
||||
@@ -81,10 +81,130 @@ pub struct WlxModifyPanelParams {
|
||||
pub command: WlxModifyPanelCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum WlxHand {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
// see wlx_common::windowing::Positioning
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub enum WlxPositioning {
|
||||
Floating,
|
||||
Anchored,
|
||||
Static,
|
||||
FollowHead {
|
||||
#[serde(default)]
|
||||
lerp: f32,
|
||||
},
|
||||
FollowHand {
|
||||
hand: WlxHand,
|
||||
#[serde(default)]
|
||||
lerp: f32,
|
||||
},
|
||||
}
|
||||
|
||||
// see wlx_common::windowing::OverlayWindowState
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum WlxWindowStateField {
|
||||
Alpha,
|
||||
Grabbable,
|
||||
Interactable,
|
||||
Positioning,
|
||||
Curvature,
|
||||
Additive,
|
||||
BlockInput,
|
||||
AlignToHmd,
|
||||
/// see wlx_common::windowing::OverlayWindowConfig::global
|
||||
Global,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum WlxWindowStateValue {
|
||||
Bool(bool),
|
||||
Float(f32),
|
||||
Positioning(WlxPositioning),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WlxWindowStateGetParams {
|
||||
pub overlay: String,
|
||||
pub field: WlxWindowStateField,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WlxWindowStateSetParams {
|
||||
pub overlay: String,
|
||||
pub field: WlxWindowStateField,
|
||||
pub value: WlxWindowStateValue,
|
||||
}
|
||||
|
||||
// see wlx_common::overlays::BackendAttrib
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum WlxWindowAttrib {
|
||||
Stereo,
|
||||
StereoFullFrame,
|
||||
StereoAdjustMouse,
|
||||
MouseTransform,
|
||||
WindowSize,
|
||||
}
|
||||
|
||||
// see wlx_common::overlays::StereoMode
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum WlxStereoMode {
|
||||
None,
|
||||
LeftRight,
|
||||
RightLeft,
|
||||
TopBottom,
|
||||
BottomTop,
|
||||
}
|
||||
|
||||
// see wlx_common::overlays::MouseTransform
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum WlxMouseTransform {
|
||||
Default,
|
||||
Normal,
|
||||
Rotated90,
|
||||
Rotated180,
|
||||
Rotated270,
|
||||
Flipped,
|
||||
Flipped90,
|
||||
Flipped180,
|
||||
Flipped270,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum WlxWindowAttribValue {
|
||||
Stereo(WlxStereoMode),
|
||||
StereoFullFrame(bool),
|
||||
StereoAdjustMouse(bool),
|
||||
MouseTransform(WlxMouseTransform),
|
||||
WindowSize([u32; 2]),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WlxWindowAttribGetParams {
|
||||
pub overlay: String,
|
||||
pub attrib: WlxWindowAttrib,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WlxWindowAttribSetParams {
|
||||
pub overlay: String,
|
||||
pub attrib: WlxWindowAttrib,
|
||||
pub value: WlxWindowAttribValue,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct WlxOverlayListParams {
|
||||
pub visible: bool,
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum PacketClient {
|
||||
Handshake(Handshake),
|
||||
WvrWindowList(Serial),
|
||||
WlxOverlayList(Serial, WlxOverlayListParams),
|
||||
WvrWindowSetVisible(packet_server::WvrWindowHandle, bool),
|
||||
WvrProcessGet(Serial, packet_server::WvrProcessHandle),
|
||||
WvrProcessLaunch(Serial, WvrProcessLaunchParams),
|
||||
@@ -97,4 +217,9 @@ pub enum PacketClient {
|
||||
WlxShowHide,
|
||||
WlxSwitchSet(Option<usize>),
|
||||
WlxHandsfree(HandsfreeParams),
|
||||
WlxWindowStateGet(Serial, WlxWindowStateGetParams),
|
||||
WlxWindowStateSet(WlxWindowStateSetParams),
|
||||
WlxWindowAttribGet(Serial, WlxWindowAttribGetParams),
|
||||
WlxWindowAttribSet(Serial, WlxWindowAttribSetParams),
|
||||
WlxOverlaySetVisible(String, bool),
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::ipc::Serial;
|
||||
use super::{ipc::Serial, packet_client};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerInfo {}
|
||||
@@ -39,8 +39,8 @@ pub struct WvrWindow {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WvrWindowList {
|
||||
pub list: Vec<WvrWindow>,
|
||||
pub struct WlxOverlayList {
|
||||
pub list: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -98,10 +98,13 @@ pub enum PacketServer {
|
||||
Disconnect(Disconnect),
|
||||
HandshakeSuccess(HandshakeSuccess),
|
||||
WlxInputStateResponse(Serial, WlxInputState),
|
||||
WvrWindowListResponse(Serial, Option<WvrWindowList>),
|
||||
WlxOverlayListResponse(Serial, WlxOverlayList),
|
||||
WvrProcessGetResponse(Serial, Option<WvrProcess>),
|
||||
WvrProcessLaunchResponse(Serial, Result<WvrProcessHandle, String>),
|
||||
WvrProcessListResponse(Serial, WvrProcessList),
|
||||
WlxWindowStateGetResponse(Serial, Result<packet_client::WlxWindowStateValue, String>),
|
||||
WlxWindowAttribGetResponse(Serial, Result<packet_client::WlxWindowAttribValue, String>),
|
||||
WlxWindowAttribSetResponse(Serial, Result<(), String>),
|
||||
WvrStateChanged(WvrStateChanged),
|
||||
}
|
||||
|
||||
@@ -111,10 +114,13 @@ impl PacketServer {
|
||||
PacketServer::Disconnect(_) => None,
|
||||
PacketServer::HandshakeSuccess(_) => None,
|
||||
PacketServer::WlxInputStateResponse(serial, _) => Some(serial),
|
||||
PacketServer::WvrWindowListResponse(serial, _) => Some(serial),
|
||||
PacketServer::WlxOverlayListResponse(serial, _) => Some(serial),
|
||||
PacketServer::WvrProcessGetResponse(serial, _) => Some(serial),
|
||||
PacketServer::WvrProcessLaunchResponse(serial, _) => Some(serial),
|
||||
PacketServer::WvrProcessListResponse(serial, _) => Some(serial),
|
||||
PacketServer::WlxWindowStateGetResponse(serial, _) => Some(serial),
|
||||
PacketServer::WlxWindowAttribGetResponse(serial, _) => Some(serial),
|
||||
PacketServer::WlxWindowAttribSetResponse(serial, _) => Some(serial),
|
||||
PacketServer::WvrStateChanged(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
+2
-4
@@ -53,15 +53,12 @@ smol = { workspace = true }
|
||||
strum.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
uuid = { workspace = true }
|
||||
vulkano.workspace = true
|
||||
vulkano-shaders.workspace = true
|
||||
wayland-client = { workspace = true }
|
||||
wayland-protocols = { workspace = true }
|
||||
wayland-protocols-misc = { workspace = true }
|
||||
winit = { workspace = true, optional = true }
|
||||
xdg.workspace = true
|
||||
|
||||
ash = "^0.38.0" # must match vulkano
|
||||
bytes = { workspace = true }
|
||||
chrono-tz = "0.10.4"
|
||||
config = "0.15.25"
|
||||
@@ -86,7 +83,7 @@ mint = "0.5.9"
|
||||
openxr = { version = "0.21.1", features = ["linked", "mint"], optional = true }
|
||||
ovr_overlay = {
|
||||
git = "https://github.com/galister/ovr_overlay_oyasumi",
|
||||
rev = "e477bd2a9e04293ea68c1e7529ef2cb131f32acc",
|
||||
rev = "3b7d24cee24aa2b1ad7dffc5d5c8568508049bdd",
|
||||
features = [
|
||||
"ovr_input",
|
||||
"ovr_system",
|
||||
@@ -130,6 +127,7 @@ evdev = "0.13.2"
|
||||
|
||||
[build-dependencies]
|
||||
regex.workspace = true
|
||||
shaderc = "0.8"
|
||||
|
||||
[features]
|
||||
default = ["openvr", "openxr", "osc", "wayland", "whisper", "x11"]
|
||||
|
||||
+38
-2
@@ -1,7 +1,21 @@
|
||||
use regex::Regex;
|
||||
use std::process::Command;
|
||||
use shaderc::{Compiler, EnvVersion, ShaderKind, TargetEnv};
|
||||
use std::{path::PathBuf, process::Command};
|
||||
|
||||
const SHADERS: &[(&str, ShaderKind)] = &[
|
||||
("quad.vert", ShaderKind::Vertex),
|
||||
("color.frag", ShaderKind::Fragment),
|
||||
("grid.frag", ShaderKind::Fragment),
|
||||
("screen.frag", ShaderKind::Fragment),
|
||||
("simple.frag", ShaderKind::Fragment),
|
||||
("wayland.frag", ShaderKind::Fragment),
|
||||
("srgb.frag", ShaderKind::Fragment),
|
||||
("sky.frag", ShaderKind::Fragment),
|
||||
];
|
||||
|
||||
fn main() {
|
||||
compile_shaders();
|
||||
|
||||
let mut wlx_build = get_version().unwrap_or(format!("{}-unknown", env!("CARGO_PKG_VERSION")));
|
||||
|
||||
match std::env::var("GITHUB_JOB").as_deref() {
|
||||
@@ -13,7 +27,29 @@ fn main() {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
println!("cargo:rustc-env=WLX_BUILD={}", wlx_build);
|
||||
println!("cargo:rustc-env=WLX_BUILD={wlx_build}");
|
||||
}
|
||||
|
||||
fn compile_shaders() {
|
||||
let shader_dir = PathBuf::from("src/shaders");
|
||||
let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo"));
|
||||
let compiler = Compiler::new().expect("shaderc compiler is available");
|
||||
|
||||
let mut options = shaderc::CompileOptions::new().expect("shaderc options are available");
|
||||
options.set_target_env(TargetEnv::Vulkan, EnvVersion::Vulkan1_3 as u32);
|
||||
|
||||
for (name, kind) in SHADERS {
|
||||
let path = shader_dir.join(name);
|
||||
println!("cargo:rerun-if-changed={}", path.display());
|
||||
|
||||
let source = std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));
|
||||
let artifact = compiler
|
||||
.compile_into_spirv(&source, *kind, name, "main", Some(&options))
|
||||
.unwrap_or_else(|e| panic!("failed to compile {}: {e}", path.display()));
|
||||
std::fs::write(out_dir.join(format!("{name}.spv")), artifact.as_binary_u8())
|
||||
.unwrap_or_else(|e| panic!("failed to write compiled shader {name}: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
fn get_version() -> Result<String, Box<dyn std::error::Error>> {
|
||||
|
||||
@@ -5984,45 +5984,6 @@
|
||||
"dest": "cargo/vendor/vk-parse-0.15.0",
|
||||
"dest-filename": ".cargo-checksum.json"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"archive-type": "tar-gzip",
|
||||
"url": "https://static.crates.io/crates/vulkano/vulkano-0.35.1.crate",
|
||||
"sha256": "08840c2b51759a6f88f26f5ea378bc8b5c199a5b4760ddda292304be087249c4",
|
||||
"dest": "cargo/vendor/vulkano-0.35.1"
|
||||
},
|
||||
{
|
||||
"type": "inline",
|
||||
"contents": "{\"package\": \"08840c2b51759a6f88f26f5ea378bc8b5c199a5b4760ddda292304be087249c4\", \"files\": {}}",
|
||||
"dest": "cargo/vendor/vulkano-0.35.1",
|
||||
"dest-filename": ".cargo-checksum.json"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"archive-type": "tar-gzip",
|
||||
"url": "https://static.crates.io/crates/vulkano-macros/vulkano-macros-0.35.0.crate",
|
||||
"sha256": "1dc929c42c9336fd082079ac3ea30126e4a0dfe36fd2e2b3581303f7d140d20f",
|
||||
"dest": "cargo/vendor/vulkano-macros-0.35.0"
|
||||
},
|
||||
{
|
||||
"type": "inline",
|
||||
"contents": "{\"package\": \"1dc929c42c9336fd082079ac3ea30126e4a0dfe36fd2e2b3581303f7d140d20f\", \"files\": {}}",
|
||||
"dest": "cargo/vendor/vulkano-macros-0.35.0",
|
||||
"dest-filename": ".cargo-checksum.json"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"archive-type": "tar-gzip",
|
||||
"url": "https://static.crates.io/crates/vulkano-shaders/vulkano-shaders-0.35.0.crate",
|
||||
"sha256": "3bf501461be7cef2893c0e62c50945add9763cc482051d29053f6157089d5ea9",
|
||||
"dest": "cargo/vendor/vulkano-shaders-0.35.0"
|
||||
},
|
||||
{
|
||||
"type": "inline",
|
||||
"contents": "{\"package\": \"3bf501461be7cef2893c0e62c50945add9763cc482051d29053f6157089d5ea9\", \"files\": {}}",
|
||||
"dest": "cargo/vendor/vulkano-shaders-0.35.0",
|
||||
"dest-filename": ".cargo-checksum.json"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"archive-type": "tar-gzip",
|
||||
|
||||
@@ -3,6 +3,5 @@ use wgui::layout::LayoutUpdateResult;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub fn process_layout_result(app: &mut AppState, res: LayoutUpdateResult) {
|
||||
app.audio_sample_player
|
||||
.play_wgui_samples(&mut app.audio_system, res.sounds_to_play);
|
||||
app.audio.play_wgui_samples(res.sounds_to_play);
|
||||
}
|
||||
@@ -180,10 +180,10 @@
|
||||
<blueprint name="menu_handsfree">
|
||||
<context_menu >
|
||||
<cell translation="BAR.HANDSFREE.NONE" _press="::HandsfreeMode None" _press2="::ContextMenuClose" />
|
||||
<cell translation="BAR.HANDSFREE.HMD" _press="::HandsfreeMode Hmd" _press2="::ContextMenuClose" />
|
||||
<cell translation="BAR.HANDSFREE.HMD" _press="::HandsfreeMode Hmd" _press2="::ContextMenuClose" skip="${openvr}" />
|
||||
<cell translation="BAR.HANDSFREE.HMD_ONLY" _press="::HandsfreeMode HmdOnly" _press2="::ContextMenuClose" />
|
||||
<cell translation="BAR.HANDSFREE.EYE_TRACKING" _press="::HandsfreeMode EyeTracking" _press2="::ContextMenuClose" skip="${openvr}" />
|
||||
<cell translation="BAR.HANDSFREE.EYE_ONLY" _press="::HandsfreeMode EyeTrackingOnly" _press2="::ContextMenuClose" skip="${openvr}" />
|
||||
<cell translation="BAR.HANDSFREE.EYE_ONLY" _press="::HandsfreeMode EyeTrackingOnly" _press2="::ContextMenuClose" />
|
||||
</context_menu>
|
||||
</blueprint>
|
||||
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
<layout version="2">
|
||||
<elements>
|
||||
<div interactable="0">
|
||||
<!-- Provides a larger hitbox for eye gaze -->
|
||||
<div padding="60">
|
||||
<rectangle
|
||||
max_width="500"
|
||||
padding="16"
|
||||
flex_direction="column"
|
||||
gap="8"
|
||||
color="background" border_color="outline" border="2" round="8">
|
||||
color="background" border_color="outline" border="2" round="8"
|
||||
id="rect">
|
||||
|
||||
<!-- Toast title -->
|
||||
<div flex_direction="row" align_items="center" gap="8">
|
||||
<sprite src="@/icons/bell.svg" color="primary" min_width="32" min_height="32" max_width="32" max_height="32" flex_grow="1" />
|
||||
<label wrap="1" id="toast_title" color="primary" weight="bold" size="25" padding_left="16" padding_right="16" />
|
||||
<div flex_direction="row" align_items="center" gap="8" id="div_title">
|
||||
<sprite id="sprite_bell" src="@/icons/bell.svg" color="primary" min_width="32" min_height="32" max_width="32" max_height="32" flex_grow="1" />
|
||||
<label wrap="1" id="label_title" color="primary" weight="bold" size="25" padding_left="16" padding_right="16" />
|
||||
</div>
|
||||
|
||||
<!-- A simple separator -->
|
||||
<rectangle width="100%" height="2" color="outline" />
|
||||
<rectangle id="rect_separator" width="100%" height="2" color="outline" />
|
||||
|
||||
<div>
|
||||
<!-- Toast message -->
|
||||
<label id="toast_body" color="on_background" wrap="1" size="20" padding_left="16" padding_right="16" />
|
||||
<label id="label_body" color="on_background" wrap="1" size="20" padding_left="16" padding_right="16" />
|
||||
</div>
|
||||
</rectangle>
|
||||
</div>
|
||||
|
||||
@@ -108,7 +108,8 @@
|
||||
"TOGGLE_CAPTURE": "Kontrolle an Desktop zurückgeben",
|
||||
"FOCUS_MODE": "Fokus-Modus (Super halten)",
|
||||
"CLOSE": "Fokussierte App schließen",
|
||||
"MOUSE": "Echte Mausklicks & Scrollen wirken ebenfalls als VR-Pointer"
|
||||
"MOUSE": "Echte Mausklicks & Scrollen wirken ebenfalls als VR-Pointer",
|
||||
"KEYBOARD": "Tastatur umschalten"
|
||||
},
|
||||
"GRAB": {
|
||||
"ADJUST_DISTANCE": "Entfernung anpassen",
|
||||
@@ -176,5 +177,7 @@
|
||||
"SELECT_SCREEN_CAST_FOR": "Screen-Cast für Display {DISPLAY} auswählen",
|
||||
"CANNOT_CAPTURE_KBD_MOUSE": "Tastatur/Maus-Erfassung konnte nicht initialisiert werden!\nPrüfe, ob dein Benutzer in der Gruppe 'input' ist.",
|
||||
"WE_ARE_GRABBING": "WayVR steuert jetzt deine Tastatur und Maus!\nDrücke Super+Z, um die Steuerung freizugeben."
|
||||
}
|
||||
},
|
||||
"STEAMVR_DASH_TOAST_TITLE": "SteamVR Dashboard geöffnet",
|
||||
"STEAMVR_DASH_TOAST_TEXT": "Schließe das SteamVR-Dashboard, um WayVR zu nutzen."
|
||||
}
|
||||
@@ -88,6 +88,7 @@ ALTTAB.GRABALL,Move entire set,Key binding action to grab and move the entire se
|
||||
ALTTAB.GRAB,Move individual overlay,Key binding action to grab and move a single overlay. “Overlay” was previously translated as “{$DEFINITIONS.OVERLAY}”
|
||||
ALTTAB.DASH,Toggle dashboard,Key binding action that toggles the dashboard. “Dashboard” was previously translated as “${DEFINITIONS.DASHBOARD}”
|
||||
ALTTAB.EDIT,Toggle edit mode,Key binding action that toggles edit mode. “Edit mode” was previously translated as “${DEFINITIONS.EDIT_MODE}”
|
||||
ALTTAB.KEYBOARD,Toggle keyboard,Key binding action that toggles the keyboard. “Keyboard” was previously translated as “${DEFINITIONS.KEYBOARD}”
|
||||
ALTTAB.SET,Switch set,Key binding action to switch between sets. “Set” was previously translated as “${DEFINITIONS.SET}”
|
||||
ALTTAB.RECENTER,Show/hide/recenter,"Key binding action to show, hide, or recenter the current set. “Set” was previously translated as “${DEFINITIONS.SET}”"
|
||||
ALTTAB.GENERAL_BINDS,General bindings,Title for key bindings that don’t need any specific criteria
|
||||
@@ -113,6 +114,8 @@ SCREENCAST.FAIL,Could not initialize screen cast source.,Error message on a wind
|
||||
TOAST.TITLE_SCREENCAST_FAIL,Could not start screen cast source!,Error message on a window that normally displays screen casts.
|
||||
TOAST.CANNOT_ADD_SET,Cannot add set!,Error message telling the user the set could not be added. “Set” was previously translated as “${DEFINITIONS.SET}”
|
||||
TOAST.CANNOT_REMOVE_SET,Cannot remove set!,Error message telling the user the set could not be removed. “Set” was previously translated as “${DEFINITIONS.SET}”
|
||||
STEAMVR_DASH_TOAST_TITLE,SteamVR Dashboard Open,Title of toast shown when SteamVR dashboard is visible
|
||||
STEAMVR_DASH_TOAST_TEXT,Close SteamVR dashboard to start using WayVR.,Body of toast shown when SteamVR dashboard is visible
|
||||
TOAST.DEFAULT_TITLE,Notification,Default title of notifications
|
||||
TOAST.EMPTY_SET,Empty set!,Title of a pop-up when the user switches to an empty set. “Set” was previously translated as “${DEFINITIONS.SET}”
|
||||
TOAST.ERROR,Error,Default title for error pop-ups
|
||||
|
||||
|
@@ -102,6 +102,7 @@
|
||||
"GRAB": "Move individual overlay",
|
||||
"DASH": "Toggle dashboard",
|
||||
"EDIT": "Toggle edit mode",
|
||||
"KEYBOARD": "Toggle keyboard",
|
||||
"SET": "Switch set",
|
||||
"RECENTER": "Show/hide/recenter",
|
||||
"GENERAL_BINDS": "General bindings",
|
||||
@@ -144,6 +145,8 @@
|
||||
"NO_SET_SELECTED": "No set is selected.",
|
||||
"ONE_CONTROLLER_ON_FLOOR": "Place one controller on the floor!"
|
||||
},
|
||||
"STEAMVR_DASH_TOAST_TITLE": "SteamVR Dashboard Open",
|
||||
"STEAMVR_DASH_TOAST_TEXT": "Close SteamVR dashboard to start using WayVR.",
|
||||
"WATCH": {
|
||||
"ADD_NEW_SET": "Add a new set",
|
||||
"CLEANUP_MIRRORS": "Remove mirrors that are\nnot currently visible",
|
||||
|
||||
@@ -108,7 +108,8 @@
|
||||
"TOGGLE_CAPTURE": "Devolver el control al escritorio",
|
||||
"FOCUS_MODE": "Modo enfoque (mantén Super)",
|
||||
"CLOSE": "Cerrar app enfocada",
|
||||
"MOUSE": "Los clics y el scroll del ratón real también actúan como el puntero de VR"
|
||||
"MOUSE": "Los clics y el scroll del ratón real también actúan como el puntero de VR",
|
||||
"KEYBOARD": "Alternar teclado"
|
||||
},
|
||||
"GRAB": {
|
||||
"ADJUST_DISTANCE": "Ajustar distancia",
|
||||
@@ -176,5 +177,7 @@
|
||||
"SELECT_SCREEN_CAST_FOR": "Seleccionar transmisión de pantalla para la pantalla {DISPLAY}",
|
||||
"CANNOT_CAPTURE_KBD_MOUSE": "¡No se pudo inicializar la captura de teclado/ratón!\nComprueba que tu usuario esté en el grupo 'input'.",
|
||||
"WE_ARE_GRABBING": "¡WayVR tiene el control de tu teclado y ratón!\nPulsa Super+Z para liberarlos."
|
||||
}
|
||||
},
|
||||
"STEAMVR_DASH_TOAST_TITLE": "Panel de SteamVR abierto",
|
||||
"STEAMVR_DASH_TOAST_TEXT": "Cierra el tablero de SteamVR para empezar a usar WayVR."
|
||||
}
|
||||
@@ -108,7 +108,8 @@
|
||||
"TOGGLE_CAPTURE": "Rendi il controllo al desktop",
|
||||
"FOCUS_MODE": "Modalità focus (tieni premuto Super)",
|
||||
"CLOSE": "Chiudi app selezionata",
|
||||
"MOUSE": "I clic e lo scorrimento del mouse reale influenzano anche il puntatore VR"
|
||||
"MOUSE": "I clic e lo scorrimento del mouse reale influenzano anche il puntatore VR",
|
||||
"KEYBOARD": "Attiva/disattiva tastiera"
|
||||
},
|
||||
"GRAB": {
|
||||
"ADJUST_DISTANCE": "Regola distanza",
|
||||
@@ -176,5 +177,7 @@
|
||||
"SELECT_SCREEN_CAST_FOR": "Seleziona la trasmissione schermo per il display {DISPLAY}",
|
||||
"CANNOT_CAPTURE_KBD_MOUSE": "Impossibile inizializzare la cattura di tastiera/mouse!\nControlla che il tuo utente sia nel gruppo 'input'.",
|
||||
"WE_ARE_GRABBING": "WayVR sta controllando tastiera e mouse!\nPremi Super+Z per rilasciare il controllo."
|
||||
}
|
||||
},
|
||||
"STEAMVR_DASH_TOAST_TITLE": "Dashboard di SteamVR aperta",
|
||||
"STEAMVR_DASH_TOAST_TEXT": "Chiudi il pannello di SteamVR per iniziare a usare WayVR."
|
||||
}
|
||||
@@ -108,7 +108,8 @@
|
||||
"TOGGLE_CAPTURE": "デスクトップに操作を戻す",
|
||||
"FOCUS_MODE": "フォーカスモード (Superキーを長押し)",
|
||||
"CLOSE": "フォーカス中のアプリを閉じる",
|
||||
"MOUSE": "実マウスのクリックとスクロールもVRポインターに反映される"
|
||||
"MOUSE": "実マウスのクリックとスクロールもVRポインターに反映される",
|
||||
"KEYBOARD": "キーボードの切り替え"
|
||||
},
|
||||
"GRAB": {
|
||||
"ADJUST_DISTANCE": "距離を調整",
|
||||
@@ -176,5 +177,7 @@
|
||||
"SELECT_SCREEN_CAST_FOR": "ディスプレイ {DISPLAY} のスクリーンキャストを選択してください",
|
||||
"CANNOT_CAPTURE_KBD_MOUSE": "キーボード/マウスのキャプチャを初期化できませんでした!\nユーザーが 'input' グループに所属しているか確認してください。",
|
||||
"WE_ARE_GRABBING": "WayVR がキーボードとマウスを制御しています。\n解除するには、Super+Zを押してください。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"STEAMVR_DASH_TOAST_TITLE": "SteamVRダッシュボード展開中",
|
||||
"STEAMVR_DASH_TOAST_TEXT": "WayVRを使用するには、SteamVRダッシュボードを閉じてください。"
|
||||
}
|
||||
@@ -108,7 +108,8 @@
|
||||
"TOGGLE_CAPTURE": "Oddaj kontrolę do pulpitu",
|
||||
"FOCUS_MODE": "Tryb skupienia (przytrzymaj Super)",
|
||||
"CLOSE": "Zamknij aktywną aplikację",
|
||||
"MOUSE": "Kliknięcia i przewijanie myszką przenoszą się na wskaźnik VR"
|
||||
"MOUSE": "Kliknięcia i przewijanie myszką przenoszą się na wskaźnik VR",
|
||||
"KEYBOARD": "Przełącz klawiaturę"
|
||||
},
|
||||
"GRAB": {
|
||||
"ADJUST_DISTANCE": "Dostosuj odległość",
|
||||
@@ -176,5 +177,7 @@
|
||||
"SELECT_SCREEN_CAST_FOR": "Wybierz przesyłanie ekranu dla wyświetlacza {DISPLAY}",
|
||||
"CANNOT_CAPTURE_KBD_MOUSE": "Nie można zainicjować przechwytywania klawiatury/myszy!\nSprawdź, czy Twój użytkownik należy do grupy 'input'.",
|
||||
"WE_ARE_GRABBING": "WayVR przejął kontrolę nad klawiaturą i myszą!\nNaciśnij Super+Z, aby zwolnić."
|
||||
}
|
||||
},
|
||||
"STEAMVR_DASH_TOAST_TITLE": "Panel SteamVR otwarty",
|
||||
"STEAMVR_DASH_TOAST_TEXT": "Zamknij pulpit SteamVR, aby zacząć korzystać z WayVR."
|
||||
}
|
||||
@@ -108,7 +108,8 @@
|
||||
"TOGGLE_CAPTURE": "将控制权还给桌面",
|
||||
"FOCUS_MODE": "焦点模式 (按住 Super 键)",
|
||||
"CLOSE": "关闭当前应用",
|
||||
"MOUSE": "实体鼠标点击与滚动同样可转换为 VR 指针"
|
||||
"MOUSE": "实体鼠标点击与滚动同样可转换为 VR 指针",
|
||||
"KEYBOARD": "切换键盘"
|
||||
},
|
||||
"GRAB": {
|
||||
"ADJUST_DISTANCE": "调整距离",
|
||||
@@ -176,5 +177,7 @@
|
||||
"SELECT_SCREEN_CAST_FOR": "为显示器 {DISPLAY} 选择屏幕投射",
|
||||
"CANNOT_CAPTURE_KBD_MOUSE": "无法初始化键盘/鼠标捕获!\n请检查您的用户是否在 'input' 组中。",
|
||||
"WE_ARE_GRABBING": "WayVR 正在接管你的键盘和鼠标!\n按 Super+Z 释放。"
|
||||
}
|
||||
},
|
||||
"STEAMVR_DASH_TOAST_TITLE": "SteamVR 控制面板已开启",
|
||||
"STEAMVR_DASH_TOAST_TEXT": "关闭 SteamVR 仪表板以开始使用 WayVR。"
|
||||
}
|
||||
+29
-105
@@ -1,9 +1,8 @@
|
||||
use std::f32::consts::PI;
|
||||
use std::process::{Child, Command};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use glam::{Affine3A, Quat, Vec2, Vec3A, Vec3Swizzles};
|
||||
use glam::{Affine3A, Quat, Vec2, Vec3A};
|
||||
|
||||
use idmap_derive::IntegerId;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
@@ -21,7 +20,7 @@ use crate::subsystem::input::InputFocus;
|
||||
use crate::windowing::backend::OverlayEventData;
|
||||
use crate::windowing::manager::OverlayWindowManager;
|
||||
use crate::windowing::window::{self, OverlayCategory, OverlayWindowData, realign, scalar_scale};
|
||||
use crate::windowing::{OverlayID, OverlaySelector};
|
||||
use crate::windowing::{OverlayID, OverlaySelector, raycast_overlay};
|
||||
|
||||
use super::task::TaskType;
|
||||
|
||||
@@ -69,6 +68,7 @@ pub enum FocusPickState {
|
||||
pub struct InputState {
|
||||
pub hmd: Affine3A,
|
||||
pub ipd: f32,
|
||||
pub eye_gaze: Option<Affine3A>,
|
||||
pub pointers: [Pointer; 2],
|
||||
pub devices: Vec<TrackedDevice>,
|
||||
pub handsfree_state: PointerState,
|
||||
@@ -84,6 +84,7 @@ impl InputState {
|
||||
Self {
|
||||
hmd: Affine3A::IDENTITY,
|
||||
ipd: 0.0,
|
||||
eye_gaze: None,
|
||||
pointers: [Pointer::new(0), Pointer::new(1)],
|
||||
devices: Vec::new(),
|
||||
processes: Vec::new(),
|
||||
@@ -348,6 +349,8 @@ pub struct Pointer {
|
||||
pub(super) interaction: InteractionState,
|
||||
pub tracked: bool,
|
||||
pub handsfree: bool,
|
||||
/// distance of the most recent ray hit on a wayland client window, used for drag n drop
|
||||
pub last_wvr_hit_distance: Option<f32>,
|
||||
}
|
||||
|
||||
impl Pointer {
|
||||
@@ -364,6 +367,7 @@ impl Pointer {
|
||||
interaction: InteractionState::default(),
|
||||
tracked: false,
|
||||
handsfree: false,
|
||||
last_wvr_hit_distance: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,7 +430,7 @@ pub struct GrabData {
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
|
||||
pub enum PointerMode {
|
||||
#[default]
|
||||
Left,
|
||||
@@ -514,6 +518,8 @@ where
|
||||
);
|
||||
}
|
||||
|
||||
crate::overlays::drag_item::update_position(overlays, app);
|
||||
|
||||
[hits[0].1, hits[1].1]
|
||||
}
|
||||
|
||||
@@ -548,10 +554,24 @@ where
|
||||
|
||||
let hovered_id = pointer.interaction.hovered_id.take();
|
||||
let (Some((mut hit, raw_hit)), haptics) = get_nearest_hit(idx, overlays, app) else {
|
||||
if let Some(wvr_server) = app.wvr_server.as_mut() {
|
||||
wvr_server.clear_dnd_focus(idx);
|
||||
}
|
||||
handle_no_hit(idx, hovered_id, overlays, app);
|
||||
return (None, pending_haptics); // no hit
|
||||
};
|
||||
|
||||
let hit_is_wayvr = overlays
|
||||
.iter()
|
||||
.find(|(id, _)| *id == hit.overlay)
|
||||
.is_some_and(|(_, overlay)| overlay.config.category == OverlayCategory::WayVR);
|
||||
|
||||
if hit_is_wayvr {
|
||||
app.input_state.pointers[idx].last_wvr_hit_distance = Some(raw_hit.dist);
|
||||
} else if let Some(wvr_server) = app.wvr_server.as_mut() {
|
||||
wvr_server.clear_dnd_focus(idx);
|
||||
}
|
||||
|
||||
// focus change
|
||||
if let Some(hovered_id) = hovered_id
|
||||
&& hovered_id != hit.overlay
|
||||
@@ -782,7 +802,7 @@ where
|
||||
let Some(overlay_state) = overlay.config.active_state.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
if !overlay_state.interactable && !edit_mode {
|
||||
if (!overlay_state.interactable || overlay_state.alpha < 0.025) && !edit_mode {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -857,7 +877,9 @@ fn start_grab(par: StartGrabParams) {
|
||||
let grab_anchor = !par.edit_mode
|
||||
&& !par.grab_float
|
||||
&& !app.anchor_grabbed
|
||||
&& matches!(state.positioning, Positioning::Anchored);
|
||||
&& matches!(state.positioning, Positioning::Anchored)
|
||||
&& !(app.session.config.middle_grabs_single
|
||||
&& pointer.interaction.mode == PointerMode::Middle);
|
||||
|
||||
let relative_grab_transform = if grab_anchor {
|
||||
app.anchor
|
||||
@@ -1099,17 +1121,7 @@ fn ray_test(
|
||||
overlay_pose: &Affine3A,
|
||||
curvature: Option<&f32>,
|
||||
) -> Option<RayHit> {
|
||||
let (dist, local_pos) = curvature.map_or_else(
|
||||
|| {
|
||||
Some(raycast_plane(
|
||||
ray_origin,
|
||||
Vec3A::NEG_Z,
|
||||
overlay_pose,
|
||||
Vec3A::NEG_Z,
|
||||
))
|
||||
},
|
||||
|curvature| raycast_cylinder(ray_origin, Vec3A::NEG_Z, overlay_pose, *curvature),
|
||||
)?;
|
||||
let (dist, local_pos) = raycast_overlay(ray_origin, overlay_pose, curvature.copied())?;
|
||||
|
||||
if dist < 0.0 {
|
||||
// hit is behind us
|
||||
@@ -1123,91 +1135,3 @@ fn ray_test(
|
||||
dist,
|
||||
})
|
||||
}
|
||||
|
||||
fn raycast_plane(
|
||||
source: &Affine3A,
|
||||
source_fwd: Vec3A,
|
||||
plane: &Affine3A,
|
||||
plane_norm: Vec3A,
|
||||
) -> (f32, Vec2) {
|
||||
let plane_normal = plane.transform_vector3a(plane_norm);
|
||||
let ray_dir = source.transform_vector3a(source_fwd);
|
||||
|
||||
let d = plane.translation.dot(-plane_normal);
|
||||
let mut dist = -(d + source.translation.dot(plane_normal)) / ray_dir.dot(plane_normal);
|
||||
|
||||
let hit_local = plane
|
||||
.inverse()
|
||||
.transform_point3a(source.translation + ray_dir * dist)
|
||||
.xy();
|
||||
|
||||
// hitting the backside of the plane, make the hit invalid
|
||||
if ray_dir.dot(plane_normal) < 0.0 && dist.is_sign_positive() {
|
||||
dist = -dist;
|
||||
}
|
||||
|
||||
(dist, hit_local)
|
||||
}
|
||||
|
||||
fn raycast_cylinder(
|
||||
source: &Affine3A,
|
||||
source_fwd: Vec3A,
|
||||
plane: &Affine3A,
|
||||
curvature: f32,
|
||||
) -> Option<(f32, Vec2)> {
|
||||
// this is solved locally; (0,0) is the center of the cylinder, and the cylinder is aligned with the Y axis
|
||||
let size = plane.x_axis.length();
|
||||
let to_local = Affine3A {
|
||||
matrix3: plane.matrix3.mul_scalar(1.0 / size),
|
||||
translation: plane.translation,
|
||||
}
|
||||
.inverse();
|
||||
|
||||
let radius = size / (2.0 * PI * curvature);
|
||||
|
||||
let ray_dir = to_local.transform_vector3a(source.transform_vector3a(source_fwd));
|
||||
let ray_origin = to_local.transform_point3a(source.translation) + Vec3A::NEG_Z * radius;
|
||||
|
||||
let v_dir = ray_dir.xz();
|
||||
let v_pos = ray_origin.xz();
|
||||
|
||||
let l_dir = v_dir.dot(v_dir);
|
||||
let l_pos = v_dir.dot(v_pos);
|
||||
let c = radius.mul_add(-radius, v_pos.dot(v_pos));
|
||||
|
||||
let d = l_pos.mul_add(l_pos, -(l_dir * c));
|
||||
if d < f32::EPSILON {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sqrt_d = d.sqrt();
|
||||
|
||||
let t1 = (-l_pos - sqrt_d) / l_dir;
|
||||
let t2 = (-l_pos + sqrt_d) / l_dir;
|
||||
|
||||
let mut t = t1.max(t2);
|
||||
|
||||
if t < f32::EPSILON {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut hit_local = ray_origin + ray_dir * t;
|
||||
if hit_local.z > 0.0 {
|
||||
// hitting the opposite half of the cylinder
|
||||
return None;
|
||||
}
|
||||
|
||||
let normal = Vec3A::new(hit_local.x, 0.0, hit_local.z).normalize();
|
||||
// If hitting from the outside, flip t
|
||||
if ray_dir.dot(normal) < 0.0 && t.is_sign_positive() {
|
||||
t = -t;
|
||||
}
|
||||
|
||||
let max_angle = 2.0 * (size / (2.0 * radius));
|
||||
let x_angle = (hit_local.x / radius).asin();
|
||||
|
||||
hit_local.x = x_angle / max_angle;
|
||||
hit_local.y /= size;
|
||||
|
||||
Some((t, hit_local.xy()))
|
||||
}
|
||||
@@ -37,4 +37,5 @@ pub enum BackendError {
|
||||
pub struct RunParams {
|
||||
pub wgui_globals: WguiGlobals,
|
||||
pub config: GeneralConfig,
|
||||
pub no_audio: bool,
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{array, fs::File, io::Write, time::Duration};
|
||||
|
||||
use anyhow::bail;
|
||||
use glam::{Affine3A, FloatExt, Quat, Vec3};
|
||||
use ovr_overlay::{
|
||||
TrackedDeviceIndex,
|
||||
input::{ActionHandle, ActionSetHandle, ActiveActionSet, InputManager, InputValueHandle},
|
||||
@@ -11,10 +12,10 @@ use ovr_overlay::{
|
||||
},
|
||||
system::SystemManager,
|
||||
};
|
||||
use wlx_common::config_io;
|
||||
use wlx_common::{config::HandsfreePointer, config_io};
|
||||
|
||||
use crate::{
|
||||
backend::input::{Haptics, TrackedDevice, TrackedDeviceRole},
|
||||
backend::input::{Haptics, Pointer, PointerState, TrackedDevice, TrackedDeviceRole},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
@@ -22,6 +23,7 @@ use super::helpers::{Affine3AConvert, OVRError};
|
||||
|
||||
const SET_DEFAULT: &str = "/actions/default";
|
||||
const INPUT_SOURCES: [&str; 2] = ["/user/hand/left", "/user/hand/right"];
|
||||
const INPUT_HEAD: &str = "/user/head";
|
||||
const PATH_POSES: [&str; 2] = [
|
||||
"/actions/default/in/LeftHand",
|
||||
"/actions/default/in/RightHand",
|
||||
@@ -32,6 +34,7 @@ const PATH_HAPTICS: [&str; 2] = [
|
||||
];
|
||||
|
||||
const PATH_ALT_CLICK: &str = "/actions/default/in/AltClick";
|
||||
const PATH_EYE_TRACKING: &str = "/actions/default/in/EyeTracking";
|
||||
const PATH_CLICK_MODIFIER_MIDDLE: &str = "/actions/default/in/ClickModifierMiddle";
|
||||
const PATH_CLICK_MODIFIER_RIGHT: &str = "/actions/default/in/ClickModifierRight";
|
||||
const PATH_CLICK: &str = "/actions/default/in/Click";
|
||||
@@ -48,7 +51,9 @@ const INPUT_ANY: InputValueHandle = InputValueHandle(ovr_overlay::sys::k_ulInval
|
||||
|
||||
pub(super) struct OpenVrInputSource {
|
||||
hands: [OpenVrHandSource; 2],
|
||||
head_hnd: InputValueHandle,
|
||||
set_hnd: ActionSetHandle,
|
||||
eye_tracking_hnd: ActionHandle,
|
||||
click_hnd: ActionHandle,
|
||||
grab_hnd: ActionHandle,
|
||||
scroll_hnd: ActionHandle,
|
||||
@@ -74,7 +79,9 @@ pub(super) struct OpenVrHandSource {
|
||||
impl OpenVrInputSource {
|
||||
pub fn new(input: &mut InputManager) -> Result<Self, OVRError> {
|
||||
let set_hnd = input.get_action_set_handle(SET_DEFAULT)?;
|
||||
let head_hnd = input.get_input_source_handle(INPUT_HEAD)?;
|
||||
|
||||
let eye_tracking_hnd = input.get_action_handle(PATH_EYE_TRACKING)?;
|
||||
let click_hnd = input.get_action_handle(PATH_CLICK)?;
|
||||
let grab_hnd = input.get_action_handle(PATH_GRAB)?;
|
||||
let scroll_hnd = input.get_action_handle(PATH_SCROLL)?;
|
||||
@@ -113,7 +120,9 @@ impl OpenVrInputSource {
|
||||
|
||||
Ok(Self {
|
||||
hands,
|
||||
head_hnd,
|
||||
set_hnd,
|
||||
eye_tracking_hnd,
|
||||
click_hnd,
|
||||
grab_hnd,
|
||||
scroll_hnd,
|
||||
@@ -182,90 +191,188 @@ impl OpenVrInputSource {
|
||||
},
|
||||
});
|
||||
|
||||
let _ = input.update_actions(&mut [aas_left, aas_right]);
|
||||
let aas_head = ActiveActionSet(ovr_overlay::sys::VRActiveActionSet_t {
|
||||
ulActionSet: self.set_hnd.0,
|
||||
ulRestrictedToDevice: self.head_hnd.0,
|
||||
ulSecondaryActionSet: 0,
|
||||
unPadding: 0,
|
||||
nPriority: 0x0,
|
||||
});
|
||||
|
||||
let _ = input.update_actions(&mut [aas_left, aas_right, aas_head]);
|
||||
|
||||
let eye_gaze = input
|
||||
.get_eye_tracking_data_relative_to_now(self.eye_tracking_hnd, universe.clone(), 0.005)
|
||||
.ok()
|
||||
.and_then(|data| eye_tracking_pose(&data.0));
|
||||
app.input_state.eye_gaze = eye_gaze;
|
||||
|
||||
let devices = system.get_device_to_absolute_tracking_pose(universe.clone(), 0.005);
|
||||
if devices[0].bPoseIsValid {
|
||||
app.input_state.hmd = devices[0].mDeviceToAbsoluteTracking.to_affine();
|
||||
let hmd = devices[0].mDeviceToAbsoluteTracking.to_affine();
|
||||
let hmd_tracked = devices[0].bPoseIsValid;
|
||||
if hmd_tracked {
|
||||
app.input_state.hmd = hmd;
|
||||
}
|
||||
|
||||
for i in 0..2 {
|
||||
let hand = &mut self.hands[i];
|
||||
let app_hand = &mut app.input_state.pointers[i];
|
||||
let picking_focus = app.input_state.picking_focus;
|
||||
let mut any_tracked = false;
|
||||
|
||||
if let Some(device) = hand.device.filter(|_| !overlay.is_dashboard_visible()) {
|
||||
app_hand.raw_pose = devices[device.0 as usize]
|
||||
.mDeviceToAbsoluteTracking
|
||||
.to_affine();
|
||||
app_hand.tracked = devices[device.0 as usize].bPoseIsValid;
|
||||
} else {
|
||||
app_hand.tracked = false;
|
||||
if picking_focus.is_none() {
|
||||
for i in 0..2 {
|
||||
let hand = &mut self.hands[i];
|
||||
let app_hand = &mut app.input_state.pointers[i];
|
||||
app_hand.handsfree = false;
|
||||
|
||||
if let Some(device) = hand.device.filter(|_| !overlay.is_dashboard_visible()) {
|
||||
app_hand.raw_pose = devices[device.0 as usize]
|
||||
.mDeviceToAbsoluteTracking
|
||||
.to_affine();
|
||||
app_hand.tracked = devices[device.0 as usize].bPoseIsValid;
|
||||
} else {
|
||||
app_hand.tracked = false;
|
||||
}
|
||||
any_tracked |= app_hand.tracked;
|
||||
|
||||
hand.has_pose = false;
|
||||
|
||||
let _ = input
|
||||
.get_pose_action_data_relative_to_now(
|
||||
hand.pose_hnd,
|
||||
universe.clone(),
|
||||
0.005,
|
||||
INPUT_ANY,
|
||||
)
|
||||
.map(|pose| {
|
||||
app_hand.pose = pose.0.pose.mDeviceToAbsoluteTracking.to_affine();
|
||||
hand.has_pose = true;
|
||||
});
|
||||
|
||||
app_hand.now.click = input
|
||||
.get_digital_action_data(self.click_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.grab = input
|
||||
.get_digital_action_data(self.grab_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.alt_click = input
|
||||
.get_digital_action_data(self.alt_click_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.show_hide = input
|
||||
.get_digital_action_data(self.show_hide_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.toggle_dashboard = input
|
||||
.get_digital_action_data(self.toggle_dashboard_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.space_drag = input
|
||||
.get_digital_action_data(self.space_drag_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.space_rotate = input
|
||||
.get_digital_action_data(self.space_rotate_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.space_reset = input
|
||||
.get_digital_action_data(self.space_reset_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.click_modifier_right = input
|
||||
.get_digital_action_data(self.click_modifier_right_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.click_modifier_middle = input
|
||||
.get_digital_action_data(self.click_modifier_middle_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.move_mouse = input
|
||||
.get_digital_action_data(self.move_mouse_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
let scroll = input
|
||||
.get_analog_action_data(self.scroll_hnd, hand.input_hnd)
|
||||
.map_or((0.0, 0.0), |x| (x.0.x, x.0.y));
|
||||
app_hand.now.scroll_x = scroll.0;
|
||||
app_hand.now.scroll_y = scroll.1;
|
||||
}
|
||||
} else {
|
||||
app.input_state.handsfree_state.scroll_x =
|
||||
app.input_state.handsfree_state.scroll_x.lerp(0.0, 0.7);
|
||||
app.input_state.handsfree_state.scroll_y =
|
||||
app.input_state.handsfree_state.scroll_y.lerp(0.0, 0.7);
|
||||
|
||||
hand.has_pose = false;
|
||||
|
||||
let _ = input
|
||||
.get_pose_action_data_relative_to_now(
|
||||
hand.pose_hnd,
|
||||
universe.clone(),
|
||||
0.005,
|
||||
INPUT_ANY,
|
||||
)
|
||||
.map(|pose| {
|
||||
app_hand.pose = pose.0.pose.mDeviceToAbsoluteTracking.to_affine();
|
||||
hand.has_pose = true;
|
||||
});
|
||||
|
||||
app_hand.now.click = input
|
||||
.get_digital_action_data(self.click_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.grab = input
|
||||
.get_digital_action_data(self.grab_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.alt_click = input
|
||||
.get_digital_action_data(self.alt_click_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.show_hide = input
|
||||
.get_digital_action_data(self.show_hide_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.toggle_dashboard = input
|
||||
.get_digital_action_data(self.toggle_dashboard_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.space_drag = input
|
||||
.get_digital_action_data(self.space_drag_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.space_rotate = input
|
||||
.get_digital_action_data(self.space_rotate_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.space_reset = input
|
||||
.get_digital_action_data(self.space_reset_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.click_modifier_right = input
|
||||
.get_digital_action_data(self.click_modifier_right_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.click_modifier_middle = input
|
||||
.get_digital_action_data(self.click_modifier_middle_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
app_hand.now.move_mouse = input
|
||||
.get_digital_action_data(self.move_mouse_hnd, hand.input_hnd)
|
||||
.is_ok_and(|x| x.0.bState);
|
||||
|
||||
let scroll = input
|
||||
.get_analog_action_data(self.scroll_hnd, hand.input_hnd)
|
||||
.map_or((0.0, 0.0), |x| (x.0.x, x.0.y));
|
||||
app_hand.now.scroll_x = scroll.0;
|
||||
app_hand.now.scroll_y = scroll.1;
|
||||
for pointer in &mut app.input_state.pointers {
|
||||
pointer.before = pointer.now;
|
||||
pointer.now = PointerState::default();
|
||||
pointer.tracked = false;
|
||||
pointer.handsfree = false;
|
||||
}
|
||||
}
|
||||
|
||||
if !picking_focus.is_none() || !any_tracked {
|
||||
let mode: HandsfreePointer = if picking_focus.is_none() {
|
||||
app.session.config.handsfree_pointer
|
||||
} else {
|
||||
app.session.config.handsfree_alt_tab.into()
|
||||
};
|
||||
let handsfree_state = app.input_state.handsfree_state;
|
||||
let delta_time = app.delta_time;
|
||||
let pointer_lerp_factor = app.session.config.pointer_lerp_factor;
|
||||
|
||||
Self::update_handsfree(
|
||||
&mut app.input_state.pointers[0],
|
||||
mode,
|
||||
hmd,
|
||||
hmd_tracked,
|
||||
eye_gaze,
|
||||
handsfree_state,
|
||||
delta_time,
|
||||
pointer_lerp_factor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_handsfree(
|
||||
pointer: &mut Pointer,
|
||||
mode: HandsfreePointer,
|
||||
hmd: Affine3A,
|
||||
hmd_tracked: bool,
|
||||
eye_gaze: Option<Affine3A>,
|
||||
handsfree_state: PointerState,
|
||||
delta_time: f32,
|
||||
pointer_lerp_factor: f32,
|
||||
) {
|
||||
let (raw_pose, tracked, lerp_scale) = match mode {
|
||||
HandsfreePointer::None => return,
|
||||
HandsfreePointer::Hmd | HandsfreePointer::HmdOnly => (hmd, hmd_tracked, 1.0),
|
||||
HandsfreePointer::EyeTracking | HandsfreePointer::EyeTrackingOnly => {
|
||||
let Some(gaze) = eye_gaze else {
|
||||
pointer.tracked = false;
|
||||
pointer.handsfree = false;
|
||||
pointer.now = handsfree_pointer_state(handsfree_state);
|
||||
return;
|
||||
};
|
||||
// Match the OpenXR backend's stronger smoothing for eye gaze.
|
||||
(gaze, true, 0.5)
|
||||
}
|
||||
};
|
||||
|
||||
let cur_quat = Quat::from_affine3(&pointer.pose);
|
||||
let cur_pos = Vec3::from(pointer.pose.translation);
|
||||
let new_quat = Quat::from_affine3(&raw_pose);
|
||||
let new_pos = Vec3::from(raw_pose.translation);
|
||||
let lerp_factor = (delta_time * 100.0 * pointer_lerp_factor * lerp_scale).clamp(0.1, 1.0);
|
||||
|
||||
pointer.raw_pose = raw_pose;
|
||||
pointer.pose = Affine3A::from_rotation_translation(
|
||||
cur_quat.lerp(new_quat, lerp_factor),
|
||||
cur_pos.lerp(new_pos, lerp_factor),
|
||||
);
|
||||
pointer.tracked = tracked;
|
||||
pointer.handsfree = tracked;
|
||||
pointer.now = handsfree_pointer_state(handsfree_state);
|
||||
}
|
||||
|
||||
pub fn update_devices(&mut self, system: &mut SystemManager, app: &mut AppState) -> bool {
|
||||
@@ -317,6 +424,49 @@ impl OpenVrInputSource {
|
||||
}
|
||||
}
|
||||
|
||||
fn handsfree_pointer_state(state: PointerState) -> PointerState {
|
||||
PointerState {
|
||||
click: state.click,
|
||||
grab: state.grab,
|
||||
grab_float: state.grab_float,
|
||||
click_modifier_right: state.click_modifier_right,
|
||||
click_modifier_middle: state.click_modifier_middle,
|
||||
scroll_x: state.scroll_x,
|
||||
scroll_y: state.scroll_y,
|
||||
..PointerState::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn eye_tracking_pose(data: &ovr_overlay::sys::VREyeTrackingData_t) -> Option<Affine3A> {
|
||||
if !(data.bActive && data.bValid && data.bTracked) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let origin = Vec3::new(
|
||||
data.vGazeOrigin.v[0],
|
||||
data.vGazeOrigin.v[1],
|
||||
data.vGazeOrigin.v[2],
|
||||
);
|
||||
let target = Vec3::new(
|
||||
data.vGazeTarget.v[0],
|
||||
data.vGazeTarget.v[1],
|
||||
data.vGazeTarget.v[2],
|
||||
);
|
||||
if !origin.is_finite() || !target.is_finite() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let direction = target - origin;
|
||||
if !direction.is_finite() || direction.length_squared() <= f32::EPSILON {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Affine3A::from_rotation_translation(
|
||||
Quat::from_rotation_arc(Vec3::NEG_Z, direction.normalize()),
|
||||
origin,
|
||||
))
|
||||
}
|
||||
|
||||
fn get_tracked_device(
|
||||
system: &mut SystemManager,
|
||||
index: TrackedDeviceIndex,
|
||||
@@ -395,6 +545,18 @@ pub fn set_action_manifest(input: &mut InputManager) -> anyhow::Result<()> {
|
||||
.write_all(include_bytes!("../../res/actions_binding_oculus.json"))?;
|
||||
}
|
||||
|
||||
let binding_path = config_io::get_config_root().join("actions_binding_frame.json");
|
||||
if !binding_path.is_file() {
|
||||
File::create(&binding_path)?
|
||||
.write_all(include_bytes!("../../res/actions_binding_frame.json"))?;
|
||||
}
|
||||
|
||||
let binding_path = config_io::get_config_root().join("actions_binding_generic_hmd.json");
|
||||
if !binding_path.is_file() {
|
||||
File::create(&binding_path)?
|
||||
.write_all(include_bytes!("../../res/actions_binding_generic_hmd.json"))?;
|
||||
}
|
||||
|
||||
if let Err(e) = input.set_action_manifest(action_path.as_path()) {
|
||||
bail!("Failed to set action manifest: {e}");
|
||||
}
|
||||
|
||||
@@ -2,29 +2,14 @@ use std::f32::consts::PI;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use ash::vk::SubmitInfo;
|
||||
use glam::{Affine3A, Quat, Vec3, Vec3A, Vec4};
|
||||
use idmap::IdMap;
|
||||
use ovr_overlay::overlay::OverlayManager;
|
||||
use ovr_overlay::sys::ETrackingUniverseOrigin;
|
||||
use vulkano::{
|
||||
VulkanObject,
|
||||
command_buffer::{
|
||||
CommandBufferBeginInfo, CommandBufferLevel, CommandBufferUsage, RecordingCommandBuffer,
|
||||
},
|
||||
format::Format,
|
||||
image::view::ImageView,
|
||||
image::{Image, ImageLayout},
|
||||
sync::{
|
||||
AccessFlags, DependencyInfo, ImageMemoryBarrier, PipelineStages,
|
||||
fence::{Fence, FenceCreateInfo},
|
||||
},
|
||||
};
|
||||
use wgui::gfx::WGfx;
|
||||
use wgui::gfx::{CommandBufferUsage, Format, ImageView, WGfx};
|
||||
use wlx_common::overlays::{BackendAttrib, BackendAttribValue};
|
||||
|
||||
use crate::backend::input::{HoverResult, PointerHit};
|
||||
use crate::graphics::ExtentExt;
|
||||
use crate::state::AppState;
|
||||
use crate::subsystem::hid::WheelDelta;
|
||||
use crate::windowing::Z_ORDER_LINES;
|
||||
@@ -53,15 +38,7 @@ impl LinePool {
|
||||
let texture = command_buffer.upload_image(2, 2, Format::R8G8B8A8_UNORM, &buf)?;
|
||||
command_buffer.build_and_execute_now()?;
|
||||
|
||||
transition_layout(
|
||||
&graphics,
|
||||
texture.clone(),
|
||||
ImageLayout::ShaderReadOnlyOptimal,
|
||||
ImageLayout::TransferSrcOptimal,
|
||||
)?
|
||||
.wait(None)?;
|
||||
|
||||
let view = ImageView::new_default(texture)?;
|
||||
let view = graphics.create_image_view(texture)?;
|
||||
|
||||
Ok(Self {
|
||||
lines: IdMap::new(),
|
||||
@@ -222,7 +199,7 @@ impl OverlayBackend for LineBackend {
|
||||
}
|
||||
fn frame_meta(&mut self) -> Option<FrameMeta> {
|
||||
Some(FrameMeta {
|
||||
extent: self.view.extent_u32arr(),
|
||||
extent: self.view.extent_2d(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -247,55 +224,3 @@ impl OverlayBackend for LineBackend {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transition_layout(
|
||||
gfx: &WGfx,
|
||||
image: Arc<Image>,
|
||||
old_layout: ImageLayout,
|
||||
new_layout: ImageLayout,
|
||||
) -> anyhow::Result<Fence> {
|
||||
let barrier = ImageMemoryBarrier {
|
||||
src_stages: PipelineStages::ALL_TRANSFER,
|
||||
src_access: AccessFlags::TRANSFER_WRITE,
|
||||
dst_stages: PipelineStages::ALL_TRANSFER,
|
||||
dst_access: AccessFlags::TRANSFER_READ,
|
||||
old_layout,
|
||||
new_layout,
|
||||
subresource_range: image.subresource_range(),
|
||||
..ImageMemoryBarrier::image(image)
|
||||
};
|
||||
|
||||
let command_buffer = unsafe {
|
||||
let mut builder = RecordingCommandBuffer::new(
|
||||
gfx.command_buffer_allocator.clone(),
|
||||
gfx.queue_gfx.queue_family_index(),
|
||||
CommandBufferLevel::Primary,
|
||||
CommandBufferBeginInfo {
|
||||
usage: CommandBufferUsage::OneTimeSubmit,
|
||||
inheritance_info: None,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
builder.pipeline_barrier(&DependencyInfo {
|
||||
image_memory_barriers: smallvec::smallvec![barrier],
|
||||
..Default::default()
|
||||
})?;
|
||||
builder.end()?
|
||||
};
|
||||
|
||||
let fence = Fence::new(gfx.device.clone(), FenceCreateInfo::default())?;
|
||||
|
||||
let fns = gfx.device.fns();
|
||||
unsafe {
|
||||
(fns.v1_0.queue_submit)(
|
||||
gfx.queue_gfx.handle(),
|
||||
1,
|
||||
[SubmitInfo::default().command_buffers(&[command_buffer.handle()])].as_ptr(),
|
||||
fence.handle(),
|
||||
)
|
||||
}
|
||||
.result()?;
|
||||
|
||||
Ok(fence)
|
||||
}
|
||||
@@ -11,7 +11,6 @@ use ovr_overlay::{
|
||||
sys::{ETrackedDeviceProperty, EVRApplicationType, EVREventType},
|
||||
};
|
||||
use smallvec::smallvec;
|
||||
use vulkano::{Handle, VulkanObject, device::physical::PhysicalDevice};
|
||||
use wgui::i18n::Translation;
|
||||
use wlx_common::{dash_interface::InterfaceFeats, overlays::ToastTopic};
|
||||
|
||||
@@ -77,16 +76,12 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
let mut chaperone_mgr = context.chaperone_setup_mngr();
|
||||
let mut compositor_mgr = context.compositor_mngr();
|
||||
|
||||
let device_extensions_fn = |device: &PhysicalDevice| {
|
||||
let names = compositor_mgr.get_vulkan_device_extensions_required(device.handle().as_raw());
|
||||
names.iter().map(std::string::String::as_str).collect()
|
||||
let device_extensions_fn = |physical_device_handle: u64| {
|
||||
compositor_mgr.get_vulkan_device_extensions_required(physical_device_handle)
|
||||
};
|
||||
|
||||
let mut compositor_mgr = context.compositor_mngr();
|
||||
let instance_extensions = {
|
||||
let names = compositor_mgr.get_vulkan_instance_extensions_required();
|
||||
names.iter().map(std::string::String::as_str).collect()
|
||||
};
|
||||
let instance_extensions = compositor_mgr.get_vulkan_instance_extensions_required();
|
||||
|
||||
let feats = InterfaceFeats::default_for_backend(XrBackend::OpenVR);
|
||||
|
||||
@@ -121,6 +116,10 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
|
||||
let mut overlays = OverlayWindowManager::<OpenVrOverlayData>::new(&mut app, args.headless)?;
|
||||
|
||||
let mut dashboard_hidden = false;
|
||||
let mut dash_toast_shown = false;
|
||||
let mut dash_toast_id = None;
|
||||
|
||||
let mut playspace = playspace::PlayspaceMover::new();
|
||||
playspace.playspace_changed(&mut compositor_mgr, &mut chaperone_mgr);
|
||||
|
||||
@@ -209,6 +208,29 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
}
|
||||
}
|
||||
|
||||
if !dashboard_hidden {
|
||||
if overlay_mgr.is_dashboard_visible() {
|
||||
if !dash_toast_shown {
|
||||
let id = Toast::new(
|
||||
ToastTopic::Priority,
|
||||
Some(Translation::from_translation_key(
|
||||
"STEAMVR_DASH_TOAST_TITLE",
|
||||
)),
|
||||
Translation::from_translation_key("STEAMVR_DASH_TOAST_TEXT"),
|
||||
)
|
||||
.with_timeout(300.0) // we'll zero this if dash gets hidden
|
||||
.submit(&mut app);
|
||||
dash_toast_id = Some(id);
|
||||
dash_toast_shown = true;
|
||||
}
|
||||
} else {
|
||||
dashboard_hidden = true;
|
||||
if let Some(id) = dash_toast_id {
|
||||
Toast::expire_now(&mut app, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if next_device_update <= Instant::now() {
|
||||
let changed = input_source.update_devices(&mut system_mgr, &mut app);
|
||||
if changed {
|
||||
@@ -280,7 +302,7 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
.enqueue(TaskType::Overlay(OverlayTask::ToggleDashboard));
|
||||
}
|
||||
|
||||
overlays.values_mut().for_each(|o| o.config.tick(&mut app));
|
||||
overlays.tick(&mut app);
|
||||
|
||||
playspace.update(&mut chaperone_mgr, &mut overlays, &mut app);
|
||||
|
||||
@@ -288,7 +310,9 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
|
||||
let haptics = interact(&mut overlays, &mut app, &mut current_lines);
|
||||
for (idx, haptics) in haptics.iter().enumerate() {
|
||||
if let Some(haptics) = haptics {
|
||||
if let Some(haptics) = haptics
|
||||
&& !app.input_state.pointers[idx].handsfree
|
||||
{
|
||||
input_source.haptics(&mut input_mgr, idx, haptics);
|
||||
}
|
||||
}
|
||||
@@ -326,6 +350,9 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
|
||||
for o in overlays.values_mut() {
|
||||
if o.data.visible {
|
||||
if !(dashboard_hidden || o.data.always_visible) {
|
||||
continue;
|
||||
}
|
||||
let ShouldRender::Should = o.should_render(&mut app)? else {
|
||||
continue;
|
||||
};
|
||||
@@ -336,16 +363,19 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
let mut rdr = RenderResources::new(app.gfx.clone(), tgt, &meta)?;
|
||||
o.render(&mut app, &mut rdr)?;
|
||||
o.data.image_dirty = true;
|
||||
futures.execute_results(rdr.end()?)?;
|
||||
futures.execute_results(rdr.end()?);
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!("Rendering overlays");
|
||||
futures.wait()?;
|
||||
|
||||
overlays
|
||||
.values_mut()
|
||||
.for_each(|o| o.after_render(universe.clone(), &mut overlay_mgr, &app.gfx));
|
||||
overlays.values_mut().for_each(|o| {
|
||||
if !(dashboard_hidden || o.data.always_visible) {
|
||||
return;
|
||||
}
|
||||
o.after_render(universe.clone(), &mut overlay_mgr, &app.gfx);
|
||||
});
|
||||
|
||||
lines.reset();
|
||||
|
||||
|
||||
@@ -7,13 +7,9 @@ use ovr_overlay::{
|
||||
pose::Matrix3x4,
|
||||
sys::{ETrackingUniverseOrigin, VRVulkanTextureData_t},
|
||||
};
|
||||
use vulkano::{
|
||||
Handle, VulkanObject,
|
||||
image::{ImageUsage, view::ImageView},
|
||||
};
|
||||
use wgui::gfx::WGfx;
|
||||
use wgui::gfx::{ImageLayout, ImageUsage, ImageView, WGfx};
|
||||
|
||||
use crate::{graphics::ExtentExt, state::AppState, windowing::window::OverlayWindowData};
|
||||
use crate::{overlays::toast::TOAST_NAME, state::AppState, windowing::window::OverlayWindowData};
|
||||
|
||||
use super::helpers::Affine3AConvert;
|
||||
|
||||
@@ -21,6 +17,7 @@ use super::helpers::Affine3AConvert;
|
||||
pub(super) struct OpenVrOverlayData {
|
||||
pub(super) handle: Option<OverlayHandle>,
|
||||
pub(super) visible: bool,
|
||||
pub(super) always_visible: bool,
|
||||
pub(super) color: Vec4,
|
||||
pub(crate) width: f32,
|
||||
pub(super) override_width: bool,
|
||||
@@ -44,6 +41,7 @@ impl OverlayWindowData<OpenVrOverlayData> {
|
||||
};
|
||||
log::debug!("{}: initialize", self.config.name);
|
||||
|
||||
self.data.always_visible = self.config.name.as_ref() == TOAST_NAME.as_ref();
|
||||
self.data.handle = Some(handle);
|
||||
self.data.color = Vec4::ONE;
|
||||
|
||||
@@ -68,7 +66,7 @@ impl OverlayWindowData<OpenVrOverlayData> {
|
||||
extent: [u32; 2],
|
||||
) -> anyhow::Result<Arc<ImageView>> {
|
||||
if let Some(image_view) = self.data.image_view.as_ref()
|
||||
&& image_view.extent_u32arr() == extent
|
||||
&& image_view.extent_2d() == extent
|
||||
{
|
||||
return Ok(image_view.clone());
|
||||
}
|
||||
@@ -83,10 +81,10 @@ impl OverlayWindowData<OpenVrOverlayData> {
|
||||
let image = app.gfx.new_image(
|
||||
extent[0],
|
||||
extent[1],
|
||||
app.gfx.surface_format,
|
||||
app.gfx.surface_format(),
|
||||
ImageUsage::TRANSFER_SRC | ImageUsage::COLOR_ATTACHMENT | ImageUsage::SAMPLED,
|
||||
)?;
|
||||
let image_view = ImageView::new_default(image)?;
|
||||
let image_view = app.gfx.create_image_view(image)?;
|
||||
self.data.image_view = Some(image_view.clone());
|
||||
Ok(image_view)
|
||||
}
|
||||
@@ -293,20 +291,26 @@ impl OverlayWindowData<OpenVrOverlayData> {
|
||||
}
|
||||
}
|
||||
|
||||
let raw_image = image.handle().as_raw();
|
||||
let format = image.format();
|
||||
if let Err(e) = graphics.transition_image_now(&image, ImageLayout::TransferSrc) {
|
||||
log::error!(
|
||||
"{}: Failed to prepare overlay image: {e:#}",
|
||||
self.config.name
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let format = image.format();
|
||||
let mut texture = VRVulkanTextureData_t {
|
||||
m_nImage: raw_image,
|
||||
m_nFormat: format as _,
|
||||
m_nImage: image.raw_handle(),
|
||||
m_nFormat: graphics.raw_format(format) as _,
|
||||
m_nWidth: dimensions[0],
|
||||
m_nHeight: dimensions[1],
|
||||
m_nSampleCount: image.samples() as u32,
|
||||
m_pDevice: graphics.device.handle().as_raw() as *mut _,
|
||||
m_pPhysicalDevice: graphics.device.physical_device().handle().as_raw() as *mut _,
|
||||
m_pInstance: graphics.instance.handle().as_raw() as *mut _,
|
||||
m_pQueue: graphics.queue_gfx.handle().as_raw() as *mut _,
|
||||
m_nQueueFamilyIndex: graphics.queue_gfx.queue_family_index(),
|
||||
m_nSampleCount: 1,
|
||||
m_pDevice: graphics.raw_device_handle() as *mut _,
|
||||
m_pPhysicalDevice: graphics.raw_physical_device_handle() as *mut _,
|
||||
m_pInstance: graphics.raw_instance_handle() as *mut _,
|
||||
m_pQueue: graphics.raw_graphics_queue_handle() as *mut _,
|
||||
m_nQueueFamilyIndex: graphics.graphics_queue_family_index(),
|
||||
};
|
||||
log::trace!(
|
||||
"{}: UploadTex {:?}, {}x{}, {:?}",
|
||||
|
||||
@@ -19,8 +19,15 @@ pub(crate) use next_chain_insert;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
|
||||
#[derive(Default)]
|
||||
pub(super) struct ExtraExts {
|
||||
pub mndx_composition_layer_alpha_blend_system_ui: bool,
|
||||
pub ext_eye_gaze_interaction: bool,
|
||||
}
|
||||
|
||||
pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId, ExtraExts), anyhow::Error> {
|
||||
let entry = xr::Entry::linked();
|
||||
let mut extra_exts = ExtraExts::default();
|
||||
|
||||
let Ok(available_extensions) = entry.enumerate_extensions() else {
|
||||
bail!("Failed to enumerate OpenXR extensions.");
|
||||
@@ -43,11 +50,6 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
|
||||
} else {
|
||||
log::warn!("Missing EXT_dpad_binding extension.");
|
||||
}
|
||||
if available_extensions.fb_composition_layer_alpha_blend {
|
||||
enabled_extensions.fb_composition_layer_alpha_blend = true;
|
||||
} else {
|
||||
log::warn!("Missing XR_FB_composition_layer_alpha_blend extension.");
|
||||
}
|
||||
|
||||
if available_extensions.ext_samsung_odyssey_controller {
|
||||
enabled_extensions.ext_samsung_odyssey_controller = true;
|
||||
@@ -66,6 +68,7 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
|
||||
}
|
||||
if available_extensions.ext_eye_gaze_interaction {
|
||||
enabled_extensions.ext_eye_gaze_interaction = true;
|
||||
extra_exts.ext_eye_gaze_interaction = true;
|
||||
} else {
|
||||
log::warn!("Missing EXT_eye_gaze_interaction extension.");
|
||||
}
|
||||
@@ -85,6 +88,14 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
|
||||
log::warn!("Missing XR_KHR_composition_layer_color_scale_bias extension.");
|
||||
}
|
||||
|
||||
let xr_extension = b"XR_MNDX_composition_layer_alpha_blend_system_ui\0".to_vec();
|
||||
if available_extensions.other.contains(&xr_extension) {
|
||||
enabled_extensions.other.push(xr_extension);
|
||||
extra_exts.mndx_composition_layer_alpha_blend_system_ui = true;
|
||||
} else {
|
||||
log::warn!("Missing XR_MNDX_composition_layer_alpha_blend_system_ui extension.");
|
||||
}
|
||||
|
||||
let xr_extension = b"XR_MNDX_system_buttons\0".to_vec();
|
||||
if available_extensions.other.contains(&xr_extension) {
|
||||
enabled_extensions.other.push(xr_extension);
|
||||
@@ -131,6 +142,22 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
|
||||
instance_props.runtime_version
|
||||
);
|
||||
|
||||
let available_extensions = available_extensions
|
||||
.names()
|
||||
.into_iter()
|
||||
.filter_map(|name| {
|
||||
Some(
|
||||
CStr::from_bytes_with_nul(name)
|
||||
.ok()?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
log::debug!("Available OpenXR extensions:\n{available_extensions}");
|
||||
|
||||
let enabled_extensions = enabled_extensions
|
||||
.names()
|
||||
.into_iter()
|
||||
@@ -145,7 +172,7 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
log::info!("OpenXR extensions:\n{enabled_extensions}");
|
||||
log::debug!("Enabled OpenXR extensions:\n{enabled_extensions}");
|
||||
|
||||
let Ok(system) = xr_instance.system(xr::FormFactor::HEAD_MOUNTED_DISPLAY) else {
|
||||
bail!("Failed to access OpenXR HMD system.");
|
||||
@@ -167,7 +194,7 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
|
||||
);
|
||||
}
|
||||
|
||||
Ok((xr_instance, system))
|
||||
Ok((xr_instance, system, extra_exts))
|
||||
}
|
||||
|
||||
pub(super) unsafe fn create_overlay_session(
|
||||
|
||||
+593
-307
File diff suppressed because it is too large.
Load diff
@@ -11,20 +11,13 @@ use std::{
|
||||
};
|
||||
|
||||
use wgui::gfx::{
|
||||
Buffer, BufferUsage, CommandBufferUsage, Scissor, Vert2Uv,
|
||||
cmd::WGfxClearMode,
|
||||
pass::WGfxPass,
|
||||
pipeline::{WGfxPipeline, WPipelineCreateInfo},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::openxr::helpers,
|
||||
graphics::{GpuFutures, Vert2Uv},
|
||||
state::AppState,
|
||||
};
|
||||
use vulkano::{
|
||||
buffer::{BufferUsage, Subbuffer},
|
||||
command_buffer::CommandBufferUsage,
|
||||
};
|
||||
use crate::{backend::openxr::helpers, graphics::GpuFutures, state::AppState};
|
||||
|
||||
use super::{
|
||||
CompositionLayer, XrState,
|
||||
@@ -55,7 +48,7 @@ impl LinePool {
|
||||
let pipeline = app.gfx.create_pipeline(
|
||||
app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic
|
||||
app.gfx_extras.shaders.get("frag_color").unwrap(), // want panic
|
||||
WPipelineCreateInfo::new(app.gfx.surface_format),
|
||||
WPipelineCreateInfo::new(app.gfx.surface_format()),
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
@@ -80,7 +73,7 @@ impl LinePool {
|
||||
0..4,
|
||||
0..1,
|
||||
vec![set0],
|
||||
&Default::default(),
|
||||
Scissor::from_viewport([1.0, 1.0], [0.0, 0.0]),
|
||||
)?;
|
||||
|
||||
let srd = create_swapchain(xr, app.gfx.clone(), [1, 1], 1, SwapchainOpts::new())?;
|
||||
@@ -182,7 +175,7 @@ impl LinePool {
|
||||
cmd_buffer.run_ref(&line.pass)?;
|
||||
cmd_buffer.end_rendering()?;
|
||||
|
||||
futures.execute(cmd_buffer.queue.clone(), cmd_buffer.build()?)?;
|
||||
futures.execute(cmd_buffer.build()?);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +218,7 @@ pub(super) struct Line {
|
||||
|
||||
struct LineContainer {
|
||||
swapchain: WlxSwapchain,
|
||||
buf_color: Subbuffer<[f32]>,
|
||||
buf_color: Arc<Buffer<f32>>,
|
||||
pass: WGfxPass<Vert2Uv>,
|
||||
maybe_line: Option<Line>,
|
||||
}
|
||||
@@ -9,7 +9,6 @@ use glam::{Affine3A, Vec3};
|
||||
use input::OpenXrInputSource;
|
||||
use openxr as xr;
|
||||
use skybox::{Skybox, create_skybox};
|
||||
use vulkano::{Handle, VulkanObject};
|
||||
use wgui::i18n::Translation;
|
||||
use wlx_common::{
|
||||
dash_interface::InterfaceFeats,
|
||||
@@ -21,7 +20,12 @@ use crate::{
|
||||
backend::{
|
||||
BackendError, RunParams,
|
||||
input::interact,
|
||||
openxr::{helpers::try_apply_chroma_key, lines::LinePool, overlay::OpenXrOverlayData},
|
||||
openxr::{
|
||||
helpers::{ExtraExts, try_apply_chroma_key},
|
||||
lines::LinePool,
|
||||
overlay::OpenXrOverlayData,
|
||||
swapchain::SwapchainRetirementQueue,
|
||||
},
|
||||
task::{OpenXrTask, OverlayTask, TaskType},
|
||||
},
|
||||
config::{save_settings, save_state},
|
||||
@@ -53,12 +57,25 @@ struct XrState {
|
||||
fps: f32,
|
||||
stage: Arc<xr::Space>,
|
||||
view: Arc<xr::Space>,
|
||||
extra_exts: ExtraExts,
|
||||
}
|
||||
|
||||
/// XR runtime using our vk device/queue can have submissions in flight after last xrEndFrame/xrReleaseSwapchainImage
|
||||
/// This guard is dropped first, draining shared device before swapchain/session teardown begins
|
||||
struct OpenXrGpuIdleGuard(Arc<wgui::gfx::WGfx>);
|
||||
|
||||
impl Drop for OpenXrGpuIdleGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = self.0.wait_idle() {
|
||||
log::error!("Failed waiting for Vulkan device idle during OpenXR shutdown: {e:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
let (xr_instance, system) = match helpers::init_xr() {
|
||||
Ok((xr_instance, system)) => (xr_instance, system),
|
||||
let (xr_instance, system, extra_exts) = match helpers::init_xr() {
|
||||
Ok((xr_instance, system, extra_exts)) => (xr_instance, system, extra_exts),
|
||||
Err(e) => {
|
||||
if !args.wait {
|
||||
log::warn!("Will not use OpenXR: {e}");
|
||||
@@ -68,10 +85,7 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
};
|
||||
|
||||
let feats = InterfaceFeats {
|
||||
passthru: xr_instance
|
||||
.exts()
|
||||
.fb_composition_layer_alpha_blend
|
||||
.is_some(),
|
||||
passthru: extra_exts.mndx_composition_layer_alpha_blend_system_ui,
|
||||
..InterfaceFeats::default_for_backend(XrBackend::OpenXR)
|
||||
};
|
||||
|
||||
@@ -83,6 +97,7 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
app.session.no_autostart = args.no_autostart;
|
||||
|
||||
let modes = xr_instance.enumerate_environment_blend_modes(system, VIEW_TYPE)?;
|
||||
log::debug!("Env blend modes: {modes:?}");
|
||||
|
||||
if args.show {
|
||||
app.session.config.tutorial_graduated = true;
|
||||
@@ -112,11 +127,11 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
&xr_instance,
|
||||
system,
|
||||
&xr::vulkan::SessionCreateInfo {
|
||||
instance: app.gfx.instance.handle().as_raw() as _,
|
||||
physical_device: app.gfx.device.physical_device().handle().as_raw() as _,
|
||||
device: app.gfx.device.handle().as_raw() as _,
|
||||
queue_family_index: app.gfx.queue_gfx.queue_family_index(),
|
||||
queue_index: 0,
|
||||
instance: app.gfx.raw_instance_handle() as _,
|
||||
physical_device: app.gfx.raw_physical_device_handle() as _,
|
||||
device: app.gfx.raw_device_handle() as _,
|
||||
queue_family_index: app.gfx.graphics_queue_family_index(),
|
||||
queue_index: app.gfx.graphics_queue_index(),
|
||||
},
|
||||
)?;
|
||||
xr::Session::from_raw(xr_instance.clone(), raw_session, Box::new(()))
|
||||
@@ -134,6 +149,7 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
fps: 30.0,
|
||||
stage: Arc::new(stage),
|
||||
view: Arc::new(view),
|
||||
extra_exts,
|
||||
};
|
||||
|
||||
let mut skybox: Option<Skybox> = None;
|
||||
@@ -158,6 +174,10 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
let mut main_session_visible = false;
|
||||
let mut environment_blend_mode = modes[0];
|
||||
let mut last_frame_time = Instant::now();
|
||||
let mut retired_swapchains = SwapchainRetirementQueue::default();
|
||||
|
||||
// declared last so rust drops this before all long-lived XR objects
|
||||
let _gpu_idle_before_xr_drop = OpenXrGpuIdleGuard(app.gfx.clone());
|
||||
|
||||
'main_loop: loop {
|
||||
let now = Instant::now();
|
||||
@@ -192,6 +212,7 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
&xr_state,
|
||||
&modes,
|
||||
&mut skybox,
|
||||
&mut retired_swapchains,
|
||||
&mut environment_blend_mode,
|
||||
main_session_visible,
|
||||
);
|
||||
@@ -219,6 +240,7 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
&xr_state,
|
||||
&modes,
|
||||
&mut skybox,
|
||||
&mut retired_swapchains,
|
||||
&mut environment_blend_mode,
|
||||
main_session_visible,
|
||||
);
|
||||
@@ -241,6 +263,9 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
continue 'main_loop;
|
||||
}
|
||||
|
||||
// poll only, never wait for GPU
|
||||
retired_swapchains.collect(&app.gfx);
|
||||
|
||||
log::trace!("xrWaitFrame");
|
||||
let xr_frame_state = frame_wait.wait()?;
|
||||
log::trace!("xrBeginFrame");
|
||||
@@ -320,7 +345,7 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
)?;
|
||||
|
||||
let ipd = helpers::ipd_from_views(&views);
|
||||
if (app.input_state.ipd - ipd).abs() > 0.05 {
|
||||
if (app.input_state.ipd - ipd).abs() > 0.5 {
|
||||
log::info!("IPD changed: {} -> {}", app.input_state.ipd, ipd);
|
||||
app.input_state.ipd = ipd;
|
||||
Toast::new(
|
||||
@@ -331,7 +356,7 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
.submit(&mut app);
|
||||
}
|
||||
|
||||
overlays.values_mut().for_each(|o| o.config.tick(&mut app));
|
||||
overlays.tick(&mut app);
|
||||
|
||||
current_lines.clear();
|
||||
|
||||
@@ -406,11 +431,17 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
log::trace!("{}: render new frame", o.config.name);
|
||||
let meta = o.config.backend.frame_meta().unwrap(); // want panic
|
||||
let stereo = !matches!(meta.stereo, StereoMode::None);
|
||||
let wsi = o.ensure_swapchain_acquire(&app, &xr_state, meta.extent, stereo)?;
|
||||
let wsi = o.ensure_swapchain_acquire(
|
||||
&app,
|
||||
&xr_state,
|
||||
&mut retired_swapchains,
|
||||
meta.extent,
|
||||
stereo,
|
||||
)?;
|
||||
let tgt = RenderTarget { views: wsi.views };
|
||||
let mut rdr = RenderResources::new(app.gfx.clone(), tgt, &meta)?;
|
||||
o.render(&mut app, &mut rdr)?;
|
||||
futures.execute_results(rdr.end()?)?;
|
||||
futures.execute_results(rdr.end()?);
|
||||
} else if o.data.swapchain.is_none() {
|
||||
log::trace!("{}: not showing due to missing swapchain", o.config.name);
|
||||
continue;
|
||||
@@ -502,6 +533,7 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
&xr_state,
|
||||
&modes,
|
||||
&mut skybox,
|
||||
&mut retired_swapchains,
|
||||
&mut environment_blend_mode,
|
||||
main_session_visible,
|
||||
);
|
||||
@@ -512,10 +544,14 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(o) = overlays.pop_dropped() {
|
||||
while let Some(mut o) = overlays.pop_dropped() {
|
||||
if let Some(swapchain) = o.data.swapchain.take() {
|
||||
retired_swapchains.retire(swapchain, &app.gfx);
|
||||
}
|
||||
delete_queue.push((o, cur_frame + 5));
|
||||
}
|
||||
|
||||
retired_swapchains.collect(&app.gfx);
|
||||
delete_queue.retain(|(_, frame)| *frame > cur_frame);
|
||||
|
||||
//FIXME: Temporary workaround for Monado bug
|
||||
@@ -559,6 +595,7 @@ fn reconfigure_environment_blend(
|
||||
xr_state: &XrState,
|
||||
modes: &[xr::EnvironmentBlendMode],
|
||||
skybox: &mut Option<Skybox>,
|
||||
retired_swapchains: &mut SwapchainRetirementQueue,
|
||||
environment_blend_mode: &mut xr::EnvironmentBlendMode,
|
||||
main_session_visible: bool,
|
||||
) {
|
||||
@@ -579,18 +616,21 @@ fn reconfigure_environment_blend(
|
||||
&& !main_session_visible;
|
||||
|
||||
if want_skybox {
|
||||
if let Some(curr_skybox) = skybox.as_ref() {
|
||||
if curr_skybox.needs_recreate(app) {
|
||||
*skybox = None;
|
||||
log::debug!("Allocating skybox.");
|
||||
*skybox = create_skybox(xr_state, app);
|
||||
if skybox
|
||||
.as_ref()
|
||||
.is_some_and(|curr_skybox| curr_skybox.needs_recreate(app))
|
||||
{
|
||||
if let Some(old_skybox) = skybox.take() {
|
||||
old_skybox.retire(retired_swapchains, app);
|
||||
}
|
||||
} else {
|
||||
log::debug!("Allocating skybox.");
|
||||
*skybox = create_skybox(xr_state, app);
|
||||
} else if skybox.is_none() {
|
||||
log::debug!("Allocating skybox.");
|
||||
*skybox = create_skybox(xr_state, app);
|
||||
}
|
||||
} else {
|
||||
} else if let Some(old_skybox) = skybox.take() {
|
||||
log::debug!("Destroying skybox.");
|
||||
*skybox = None;
|
||||
old_skybox.retire(retired_swapchains, app);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use super::{CompositionLayer, XrState, helpers, swapchain::WlxSwapchain};
|
||||
use crate::{
|
||||
backend::openxr::{
|
||||
helpers::next_chain_insert,
|
||||
swapchain::{SwapchainOpts, WlxSwapchainImage, create_swapchain},
|
||||
swapchain::{SwapchainOpts, SwapchainRetirementQueue, WlxSwapchainImage, create_swapchain},
|
||||
},
|
||||
state::AppState,
|
||||
windowing::window::{OverlayCategory, OverlayWindowData},
|
||||
@@ -29,6 +29,7 @@ impl OverlayWindowData<OpenXrOverlayData> {
|
||||
&'a mut self,
|
||||
app: &AppState,
|
||||
xr: &'a XrState,
|
||||
retired_swapchains: &mut SwapchainRetirementQueue,
|
||||
extent: [u32; 2],
|
||||
stereo: bool,
|
||||
) -> anyhow::Result<WlxSwapchainImage> {
|
||||
@@ -56,7 +57,9 @@ impl OverlayWindowData<OpenXrOverlayData> {
|
||||
SwapchainOpts::new(),
|
||||
)?;
|
||||
let tgt = swapchain.acquire_wait_image()?;
|
||||
self.data.swapchain = Some(swapchain);
|
||||
if let Some(old_swapchain) = self.data.swapchain.replace(swapchain) {
|
||||
retired_swapchains.retire(old_swapchain, &app.gfx);
|
||||
}
|
||||
Ok(tgt)
|
||||
}
|
||||
|
||||
@@ -111,11 +114,7 @@ impl OverlayWindowData<OpenXrOverlayData> {
|
||||
flags |= CompositionLayerFlags::UNPREMULTIPLIED_ALPHA;
|
||||
}
|
||||
|
||||
if xr
|
||||
.instance
|
||||
.exts()
|
||||
.fb_composition_layer_alpha_blend
|
||||
.is_some()
|
||||
if xr.extra_exts.mndx_composition_layer_alpha_blend_system_ui
|
||||
&& self.config.category == OverlayCategory::Passthru
|
||||
&& self.data.layer_alpha_blend_fb.is_none()
|
||||
&& !self.config.editing
|
||||
|
||||
@@ -2,19 +2,18 @@ use std::{f32::consts::PI, fs::File, sync::Arc};
|
||||
|
||||
use glam::{Quat, Vec3A};
|
||||
use openxr as xr;
|
||||
use vulkano::{
|
||||
command_buffer::CommandBufferUsage, image::view::ImageView,
|
||||
pipeline::graphics::color_blend::AttachmentBlend,
|
||||
use wgui::gfx::{
|
||||
BLEND_ALPHA, CommandBufferUsage, ImageView, Scissor, cmd::WGfxClearMode,
|
||||
pipeline::WPipelineCreateInfo,
|
||||
};
|
||||
use wgui::gfx::{cmd::WGfxClearMode, pipeline::WPipelineCreateInfo};
|
||||
use wlx_common::config_io;
|
||||
|
||||
use crate::{
|
||||
backend::openxr::{
|
||||
helpers::{next_chain_insert, translation_rotation_to_posef},
|
||||
swapchain::SwapchainOpts,
|
||||
swapchain::{SwapchainOpts, SwapchainRetirementQueue},
|
||||
},
|
||||
graphics::{ExtentExt, GpuFutures, dds::WlxCommandBufferDds},
|
||||
graphics::{GpuFutures, dds::WlxCommandBufferDds},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
@@ -76,7 +75,7 @@ impl Skybox {
|
||||
|
||||
let view = if let Some(image) = maybe_image {
|
||||
command_buffer.build_and_execute_now()?;
|
||||
Some(ImageView::new_default(image)?)
|
||||
Some(app.gfx.create_image_view(image)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -113,6 +112,19 @@ impl Skybox {
|
||||
*self.current_skybox != *app.session.config.skybox_texture
|
||||
}
|
||||
|
||||
pub(super) fn retire(
|
||||
mut self,
|
||||
retired_swapchains: &mut SwapchainRetirementQueue,
|
||||
app: &AppState,
|
||||
) {
|
||||
if let Some(swapchain) = self.sky.take() {
|
||||
retired_swapchains.retire(swapchain, &app.gfx);
|
||||
}
|
||||
if let Some(swapchain) = self.grid.take() {
|
||||
retired_swapchains.retire(swapchain, &app.gfx);
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_sky<'a>(
|
||||
&'a mut self,
|
||||
xr: &'a XrState,
|
||||
@@ -127,7 +139,7 @@ impl Skybox {
|
||||
let extent = self
|
||||
.view
|
||||
.as_ref()
|
||||
.map_or([4096, 4096], ExtentExt::extent_u32arr);
|
||||
.map_or([4096, 4096], |view| view.extent_2d());
|
||||
|
||||
let mut swapchain = create_swapchain(xr, app.gfx.clone(), extent, 1, opts)?;
|
||||
let tgt = swapchain
|
||||
@@ -141,11 +153,11 @@ impl Skybox {
|
||||
let pipeline = app.gfx.create_pipeline(
|
||||
app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic
|
||||
app.gfx_extras.shaders.get("frag_srgb").unwrap(), // want panic
|
||||
WPipelineCreateInfo::new(app.gfx.surface_format),
|
||||
WPipelineCreateInfo::new(app.gfx.surface_format()),
|
||||
)?;
|
||||
|
||||
let set0 = pipeline.uniform_sampler(0, view.clone(), app.gfx.texture_filter)?;
|
||||
let set1 = pipeline.uniform_buffer_upload(1, vec![1f32])?;
|
||||
let set0 = pipeline.uniform_sampler(0, view.clone(), app.gfx.texture_filter())?;
|
||||
let set1 = pipeline.uniform_buffer_upload(1, &[1f32])?;
|
||||
pipeline.create_pass(
|
||||
tgt.extent_f32(),
|
||||
[0.0, 0.0],
|
||||
@@ -153,13 +165,13 @@ impl Skybox {
|
||||
0..4,
|
||||
0..1,
|
||||
vec![set0, set1],
|
||||
&Default::default(),
|
||||
Scissor::new([0, 0], tgt.extent_2d()),
|
||||
)?
|
||||
} else {
|
||||
let pipeline = app.gfx.create_pipeline(
|
||||
app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic
|
||||
app.gfx_extras.shaders.get("frag_sky").unwrap(), // want panic
|
||||
WPipelineCreateInfo::new(app.gfx.surface_format),
|
||||
WPipelineCreateInfo::new(app.gfx.surface_format()),
|
||||
)?;
|
||||
|
||||
pipeline.create_pass(
|
||||
@@ -169,7 +181,7 @@ impl Skybox {
|
||||
0..4,
|
||||
0..1,
|
||||
vec![],
|
||||
&Default::default(),
|
||||
Scissor::new([0, 0], tgt.extent_2d()),
|
||||
)?
|
||||
};
|
||||
|
||||
@@ -180,7 +192,7 @@ impl Skybox {
|
||||
cmd_buffer.run_ref(&pass)?;
|
||||
cmd_buffer.end_rendering()?;
|
||||
|
||||
futures.execute(cmd_buffer.queue.clone(), cmd_buffer.build()?)?;
|
||||
futures.execute(cmd_buffer.build()?);
|
||||
|
||||
self.sky = Some(swapchain);
|
||||
Ok(())
|
||||
@@ -207,7 +219,7 @@ impl Skybox {
|
||||
let pipeline = app.gfx.create_pipeline(
|
||||
app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic
|
||||
app.gfx_extras.shaders.get("frag_grid").unwrap(), // want panic
|
||||
WPipelineCreateInfo::new(app.gfx.surface_format).use_blend(AttachmentBlend::alpha()),
|
||||
WPipelineCreateInfo::new(app.gfx.surface_format()).use_blend(BLEND_ALPHA),
|
||||
)?;
|
||||
|
||||
let tgt = swapchain
|
||||
@@ -223,7 +235,7 @@ impl Skybox {
|
||||
0..4,
|
||||
0..1,
|
||||
vec![],
|
||||
&Default::default(),
|
||||
Scissor::new([0, 0], tgt.extent_2d()),
|
||||
)?;
|
||||
|
||||
let mut cmd_buffer = app
|
||||
@@ -233,7 +245,7 @@ impl Skybox {
|
||||
cmd_buffer.run_ref(&pass)?;
|
||||
cmd_buffer.end_rendering()?;
|
||||
|
||||
futures.execute(cmd_buffer.queue.clone(), cmd_buffer.build()?)?;
|
||||
futures.execute(cmd_buffer.build()?);
|
||||
|
||||
self.grid = Some(swapchain);
|
||||
Ok(())
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ash::vk;
|
||||
use openxr as xr;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
use vulkano::{
|
||||
Handle,
|
||||
image::{
|
||||
ImageCreateInfo, ImageUsage,
|
||||
sys::RawImage,
|
||||
view::{ImageView, ImageViewCreateInfo},
|
||||
},
|
||||
use wgui::gfx::{
|
||||
GpuCompletionMarker, ImageCreateInfo, ImageLayout, ImageUsage, ImageView, ImageViewCreateInfo,
|
||||
WGfx,
|
||||
};
|
||||
use wgui::gfx::WGfx;
|
||||
|
||||
use super::XrState;
|
||||
|
||||
@@ -48,7 +42,7 @@ pub(super) fn create_swapchain(
|
||||
let swapchain = xr.session.create_swapchain(&xr::SwapchainCreateInfo {
|
||||
create_flags,
|
||||
usage_flags: xr::SwapchainUsageFlags::COLOR_ATTACHMENT | xr::SwapchainUsageFlags::SAMPLED,
|
||||
format: gfx.surface_format as _,
|
||||
format: gfx.raw_format(gfx.surface_format()) as _,
|
||||
sample_count: 1,
|
||||
width: extent[0],
|
||||
height: extent[1],
|
||||
@@ -61,28 +55,35 @@ pub(super) fn create_swapchain(
|
||||
.enumerate_images()?
|
||||
.into_iter()
|
||||
.map(|handle| {
|
||||
let vk_image = vk::Image::from_raw(handle);
|
||||
// thanks @yshui
|
||||
let raw_image = unsafe {
|
||||
RawImage::from_handle_borrowed(
|
||||
gfx.device.clone(),
|
||||
vk_image,
|
||||
// SAFETY: XR runtime owns the swapchain image & keeps it alive until explicitly destroyed
|
||||
let image = unsafe {
|
||||
gfx.wrap_external_image_with_layout(
|
||||
handle,
|
||||
ImageCreateInfo {
|
||||
format: gfx.surface_format as _,
|
||||
format: gfx.surface_format(),
|
||||
extent: [extent[0], extent[1], 1],
|
||||
array_layers: array_size,
|
||||
usage: ImageUsage::COLOR_ATTACHMENT,
|
||||
..Default::default()
|
||||
usage: ImageUsage::COLOR_ATTACHMENT | ImageUsage::SAMPLED,
|
||||
..ImageCreateInfo::new_2d(
|
||||
extent[0],
|
||||
extent[1],
|
||||
gfx.surface_format(),
|
||||
ImageUsage::COLOR_ATTACHMENT | ImageUsage::SAMPLED,
|
||||
)
|
||||
},
|
||||
ImageLayout::ColorAttachment,
|
||||
)?
|
||||
};
|
||||
// SAFETY: OpenXR guarantees that the image is a swapchain image, thus has memory backing it.
|
||||
let image = Arc::new(unsafe { raw_image.assume_bound() });
|
||||
let mut wsi = WlxSwapchainImage::default();
|
||||
for d in 0..array_size {
|
||||
let mut create_info = ImageViewCreateInfo::from_image(&image);
|
||||
create_info.subresource_range.array_layers = d..d + 1;
|
||||
wsi.views.push(ImageView::new(image.clone(), create_info)?);
|
||||
wsi.views.push(gfx.create_image_view_with_info(
|
||||
image.clone(),
|
||||
ImageViewCreateInfo {
|
||||
base_array_layer: d,
|
||||
array_layer_count: 1,
|
||||
..Default::default()
|
||||
},
|
||||
)?);
|
||||
}
|
||||
Ok(wsi)
|
||||
})
|
||||
@@ -91,8 +92,8 @@ pub(super) fn create_swapchain(
|
||||
Ok(WlxSwapchain {
|
||||
acquired: false,
|
||||
ever_acquired: false,
|
||||
swapchain,
|
||||
images,
|
||||
swapchain,
|
||||
extent,
|
||||
array_size,
|
||||
})
|
||||
@@ -106,10 +107,11 @@ pub(super) struct WlxSwapchainImage {
|
||||
pub(super) struct WlxSwapchain {
|
||||
acquired: bool,
|
||||
pub(super) ever_acquired: bool,
|
||||
// drop image views before destroying parent swapchain
|
||||
pub(super) images: SmallVec<[WlxSwapchainImage; 4]>,
|
||||
pub(super) swapchain: xr::Swapchain<xr::Vulkan>,
|
||||
pub(super) extent: [u32; 2],
|
||||
pub(super) array_size: u32,
|
||||
pub(super) images: SmallVec<[WlxSwapchainImage; 4]>,
|
||||
}
|
||||
|
||||
impl WlxSwapchain {
|
||||
@@ -143,3 +145,72 @@ impl WlxSwapchain {
|
||||
.image_array_index(array_index)
|
||||
}
|
||||
}
|
||||
|
||||
struct RetiredSwapchain {
|
||||
// keep alive until completion is signaled
|
||||
_swapchain: WlxSwapchain,
|
||||
completion: Option<GpuCompletionMarker>,
|
||||
}
|
||||
|
||||
/// Keeps destroyed xrswapchains alive until work submitted by runtime from has finished
|
||||
#[derive(Default)]
|
||||
pub(super) struct SwapchainRetirementQueue {
|
||||
retired: Vec<RetiredSwapchain>,
|
||||
}
|
||||
|
||||
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,
|
||||
completion: None,
|
||||
});
|
||||
|
||||
let idx = self.retired.len() - 1;
|
||||
if let Err(e) = Self::arm(&mut self.retired[idx], gfx) {
|
||||
log::warn!("Failed arming retired OpenXR swapchain: {e:#}");
|
||||
}
|
||||
}
|
||||
|
||||
fn arm(retired: &mut RetiredSwapchain, gfx: &Arc<WGfx>) -> anyhow::Result<()> {
|
||||
if retired.completion.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
retired._swapchain.ensure_image_released()?;
|
||||
retired.completion = Some(gfx.submit_graphics_completion_marker()?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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() {
|
||||
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]
|
||||
.completion
|
||||
.as_ref()
|
||||
.expect("retired swapchain completion marker missing after arm")
|
||||
.is_complete()
|
||||
{
|
||||
Ok(complete) => complete,
|
||||
Err(e) => {
|
||||
log::warn!("Failed polling retired OpenXR swapchain: {e:#}");
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if complete {
|
||||
self.retired.swap_remove(idx);
|
||||
} else {
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
backend::input,
|
||||
state::AppState,
|
||||
windowing::{
|
||||
OverlaySelector,
|
||||
OverlayID, OverlaySelector,
|
||||
window::{OverlayCategory, OverlayWindowConfig},
|
||||
},
|
||||
};
|
||||
@@ -135,6 +135,8 @@ pub enum OverlayTask {
|
||||
Spawn(OverlaySelector, SpawnPos, Box<CreateOverlayTask>),
|
||||
ModifyPanel(ModifyPanelTask),
|
||||
Drop(OverlaySelector),
|
||||
DropImmediate(OverlaySelector),
|
||||
DropExpired(OverlayID),
|
||||
}
|
||||
|
||||
pub type GlobalTask = dyn FnOnce(&mut AppState) + Send;
|
||||
|
||||
+111
-25
@@ -1,4 +1,5 @@
|
||||
use anyhow::Context;
|
||||
use smithay::backend::allocator::Buffer;
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::{BufferType, buffer_type};
|
||||
use smithay::desktop::{
|
||||
@@ -14,7 +15,9 @@ use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
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;
|
||||
use smithay::reexports::wayland_server::protocol::{wl_buffer, wl_callback, wl_output, wl_seat};
|
||||
use smithay::reexports::wayland_server::protocol::{
|
||||
wl_buffer, wl_callback, wl_data_source, wl_output, wl_seat,
|
||||
};
|
||||
use smithay::reexports::wayland_server::{self, DisplayHandle};
|
||||
use smithay::utils::{Logical, Rectangle, Serial, Size};
|
||||
use smithay::wayland::buffer::BufferHandler;
|
||||
@@ -40,7 +43,7 @@ use smithay::wayland::shell::xdg::decoration::{XdgDecorationHandler, XdgDecorati
|
||||
use smithay::wayland::shell::xdg::{
|
||||
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
|
||||
};
|
||||
use smithay::wayland::shm::{ShmHandler, ShmState, with_buffer_contents};
|
||||
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::{
|
||||
@@ -57,10 +60,10 @@ use std::sync::{Arc, Mutex};
|
||||
use wayland_server::Client;
|
||||
use wayland_server::backend::{ClientData, ClientId, DisconnectReason};
|
||||
use wayland_server::protocol::wl_surface::WlSurface;
|
||||
use wlx_capture::DrmFourcc;
|
||||
|
||||
use crate::backend::wayvr::image_importer::ImageImporter;
|
||||
use crate::backend::wayvr::{SurfaceBufWithImage, WAYVR_SCREEN_RES, time};
|
||||
use crate::graphics::ExtentExt;
|
||||
use crate::ipc::event_queue::SyncEventQueue;
|
||||
|
||||
use super::WayVRTask;
|
||||
@@ -89,8 +92,15 @@ pub struct Application {
|
||||
pub viewporter: ViewporterState,
|
||||
pub display_handle: DisplayHandle,
|
||||
pub redraw_requests: HashSet<ObjectId>,
|
||||
pub(super) no_server_decorations: HashSet<ObjectId>,
|
||||
pub pending_frame_callbacks: HashMap<ObjectId, Vec<wl_callback::WlCallback>>,
|
||||
pub cursor_image: CursorImageStatus,
|
||||
/// pointer that last drove the wvr_server mouse. used for DND
|
||||
pub vr_pointer: usize,
|
||||
/// pointer that produced the button press that correlates to DND serial
|
||||
pub pointer_button_owner: Option<usize>,
|
||||
/// pointer currently owning a DND grab
|
||||
pub dnd_pointer: Option<usize>,
|
||||
}
|
||||
|
||||
impl Application {
|
||||
@@ -172,6 +182,23 @@ impl Application {
|
||||
self.redraw_requests.remove(surface_id)
|
||||
}
|
||||
|
||||
pub fn has_server_side_decorations(&self, surface: &WlSurface) -> bool {
|
||||
!self.no_server_decorations.contains(&surface.id())
|
||||
}
|
||||
|
||||
fn set_server_side_decorations(&mut self, surface: &WlSurface, enabled: bool) {
|
||||
let id = surface.id();
|
||||
let changed = if enabled {
|
||||
self.no_server_decorations.remove(&id)
|
||||
} else {
|
||||
self.no_server_decorations.insert(id.clone())
|
||||
};
|
||||
|
||||
if changed {
|
||||
self.redraw_requests.insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_logical_size(&self) -> Size<i32, Logical> {
|
||||
self.output.current_mode().map_or_else(
|
||||
|| Size::new(WAYVR_SCREEN_RES[0], WAYVR_SCREEN_RES[1]),
|
||||
@@ -181,12 +208,7 @@ impl Application {
|
||||
|
||||
fn surface_logical_size(surface: &WlSurface) -> Option<Size<i32, Logical>> {
|
||||
smithay::wayland::compositor::with_states(surface, |states| {
|
||||
SurfaceBufWithImage::get_from_surface(states).map(|buf| {
|
||||
let extent = buf.image.extent_u32arr();
|
||||
let scale = buf.scale.max(1) as u32;
|
||||
|
||||
Size::new((extent[0] / scale) as i32, (extent[1] / scale) as i32)
|
||||
})
|
||||
SurfaceBufWithImage::get_from_surface(states).map(|buf| buf.logical_size(states))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -213,6 +235,13 @@ impl Application {
|
||||
}
|
||||
}
|
||||
|
||||
fn drm_format_has_alpha(format: DrmFourcc) -> bool {
|
||||
matches!(
|
||||
format,
|
||||
DrmFourcc::Abgr8888 | DrmFourcc::Argb8888 | DrmFourcc::Abgr2101010
|
||||
)
|
||||
}
|
||||
|
||||
impl compositor::CompositorHandler for Application {
|
||||
fn compositor_state(&mut self) -> &mut compositor::CompositorState {
|
||||
&mut self.compositor
|
||||
@@ -239,8 +268,12 @@ impl compositor::CompositorHandler for Application {
|
||||
Some(BufferType::Dma) => {
|
||||
let dmabuf = get_dmabuf(&buffer).unwrap(); // always Ok due to buffer_type
|
||||
|
||||
if let Ok(image) =
|
||||
self.image_importer.get_or_import_dmabuf(dmabuf.clone())
|
||||
if let Ok(image) = self
|
||||
.image_importer
|
||||
.get_or_import_dmabuf(dmabuf.clone())
|
||||
.inspect_err(|e| {
|
||||
log::warn!("wayland_server failed to import DMA-buf: {e:?}");
|
||||
})
|
||||
{
|
||||
let sbwi = SurfaceBufWithImage {
|
||||
image,
|
||||
@@ -249,6 +282,7 @@ impl compositor::CompositorHandler for Application {
|
||||
),
|
||||
scale: attrs.buffer_scale,
|
||||
dmabuf: true,
|
||||
has_alpha: drm_format_has_alpha(dmabuf.format().code),
|
||||
};
|
||||
|
||||
if let Some(old_buffer) =
|
||||
@@ -276,6 +310,8 @@ impl compositor::CompositorHandler for Application {
|
||||
),
|
||||
scale: attrs.buffer_scale,
|
||||
dmabuf: false,
|
||||
has_alpha: shm_format_to_fourcc(buf.format)
|
||||
.is_some_and(drm_format_has_alpha),
|
||||
};
|
||||
sbwi.apply_to_surface(states, None);
|
||||
}
|
||||
@@ -297,6 +333,7 @@ impl compositor::CompositorHandler for Application {
|
||||
),
|
||||
scale: attrs.buffer_scale,
|
||||
dmabuf: false,
|
||||
has_alpha: true,
|
||||
};
|
||||
sbwi.apply_to_surface(states, None);
|
||||
}
|
||||
@@ -318,6 +355,19 @@ impl compositor::CompositorHandler for Application {
|
||||
}
|
||||
});
|
||||
|
||||
// validate wp_viewport against currently attached backing buffer
|
||||
smithay::wayland::compositor::with_states(surface, |states| {
|
||||
if let Some(buf) = SurfaceBufWithImage::get_from_surface(states) {
|
||||
let extent = buf.image.extent_2d();
|
||||
let scale = buf.scale.max(1) as u32;
|
||||
let buffer_size = Size::new(
|
||||
(extent[0] / scale).max(1) as i32,
|
||||
(extent[1] / scale).max(1) as i32,
|
||||
);
|
||||
smithay::wayland::viewporter::ensure_viewport_valid(states, buffer_size);
|
||||
}
|
||||
});
|
||||
|
||||
self.redraw_requests.insert(surface.id());
|
||||
}
|
||||
}
|
||||
@@ -347,7 +397,25 @@ impl BufferHandler for Application {
|
||||
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
|
||||
}
|
||||
|
||||
impl ClientDndGrabHandler for Application {}
|
||||
impl ClientDndGrabHandler for Application {
|
||||
fn started(
|
||||
&mut self,
|
||||
_source: Option<wl_data_source::WlDataSource>,
|
||||
icon: Option<WlSurface>,
|
||||
_seat: Seat<Self>,
|
||||
) {
|
||||
let pointer = self.pointer_button_owner.unwrap_or(self.vr_pointer);
|
||||
self.dnd_pointer = Some(pointer);
|
||||
self.wayvr_tasks
|
||||
.send(WayVRTask::DndStarted { icon, pointer });
|
||||
}
|
||||
|
||||
fn dropped(&mut self, _target: Option<WlSurface>, _validated: bool, _seat: Seat<Self>) {
|
||||
self.dnd_pointer = None;
|
||||
self.pointer_button_owner = None;
|
||||
self.wayvr_tasks.send(WayVRTask::DndDropped);
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerDndGrabHandler for Application {
|
||||
fn send(&mut self, _mime_type: String, _fd: OwnedFd, _seat: Seat<Self>) {}
|
||||
@@ -430,6 +498,8 @@ impl XdgShellHandler for Application {
|
||||
|
||||
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
|
||||
self.output.leave(surface.wl_surface());
|
||||
self.no_server_decorations
|
||||
.remove(&surface.wl_surface().id());
|
||||
|
||||
if let Some(client) = surface.wl_surface().client() {
|
||||
self.wayvr_tasks
|
||||
@@ -610,10 +680,16 @@ impl DmabufHandler for Application {
|
||||
dmabuf: Dmabuf,
|
||||
notifier: ImportNotifier,
|
||||
) {
|
||||
if self.image_importer.get_or_import_dmabuf(dmabuf).is_ok() {
|
||||
let _ = notifier.successful::<Self>();
|
||||
} else {
|
||||
notifier.failed();
|
||||
match self.image_importer.get_or_import_dmabuf(dmabuf) {
|
||||
Ok(_) => {
|
||||
log::info!("wayland_server DMA-buf import complete; notifying client");
|
||||
let _ = notifier.successful::<Self>();
|
||||
log::info!("wayland_server DMA-buf success notification returned");
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("wayland_server rejected DMA-buf import: {e:?}");
|
||||
notifier.failed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -637,24 +713,26 @@ impl selection_ext::DataControlHandler for Application {
|
||||
}
|
||||
impl XdgDecorationHandler for Application {
|
||||
fn new_decoration(&mut self, toplevel: ToplevelSurface) {
|
||||
self.set_server_side_decorations(toplevel.wl_surface(), true);
|
||||
toplevel.with_pending_state(|state| {
|
||||
state.decoration_mode = Some(zxdg_toplevel_decoration_v1::Mode::ServerSide);
|
||||
});
|
||||
toplevel.send_configure();
|
||||
}
|
||||
|
||||
fn request_mode(
|
||||
&mut self,
|
||||
toplevel: ToplevelSurface,
|
||||
_mode: zxdg_toplevel_decoration_v1::Mode,
|
||||
) {
|
||||
fn request_mode(&mut self, toplevel: ToplevelSurface, mode: zxdg_toplevel_decoration_v1::Mode) {
|
||||
self.set_server_side_decorations(
|
||||
toplevel.wl_surface(),
|
||||
matches!(mode, zxdg_toplevel_decoration_v1::Mode::ServerSide),
|
||||
);
|
||||
toplevel.with_pending_state(|state| {
|
||||
state.decoration_mode = Some(zxdg_toplevel_decoration_v1::Mode::ServerSide);
|
||||
state.decoration_mode = Some(mode);
|
||||
});
|
||||
toplevel.send_configure();
|
||||
}
|
||||
|
||||
fn unset_mode(&mut self, toplevel: ToplevelSurface) {
|
||||
self.set_server_side_decorations(toplevel.wl_surface(), true);
|
||||
toplevel.with_pending_state(|state| {
|
||||
state.decoration_mode = Some(zxdg_toplevel_decoration_v1::Mode::ServerSide);
|
||||
});
|
||||
@@ -669,11 +747,19 @@ impl KdeDecorationHandler for Application {
|
||||
|
||||
fn request_mode(
|
||||
&mut self,
|
||||
_surface: &WlSurface,
|
||||
surface: &WlSurface,
|
||||
decoration: &org_kde_kwin_server_decoration::OrgKdeKwinServerDecoration,
|
||||
_mode: wayland_server::WEnum<org_kde_kwin_server_decoration::Mode>,
|
||||
mode: wayland_server::WEnum<org_kde_kwin_server_decoration::Mode>,
|
||||
) {
|
||||
decoration.mode(org_kde_kwin_server_decoration::Mode::Server);
|
||||
let wayland_server::WEnum::Value(mode) = mode else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.set_server_side_decorations(
|
||||
surface,
|
||||
matches!(mode, org_kde_kwin_server_decoration::Mode::Server),
|
||||
);
|
||||
decoration.mode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,7 @@ use smithay::{
|
||||
},
|
||||
};
|
||||
use std::{ops::RangeInclusive, rc::Rc, sync::Arc};
|
||||
use vulkano::image::view::ImageView;
|
||||
|
||||
use crate::graphics::ExtentExt;
|
||||
use wgui::gfx::ImageView;
|
||||
|
||||
use crate::backend::input::PointerHit;
|
||||
|
||||
@@ -36,6 +34,9 @@ pub struct RenderedSurface {
|
||||
pub image: Arc<ImageView>,
|
||||
pub pos: Vec2,
|
||||
pub size: Vec2,
|
||||
pub has_alpha: bool,
|
||||
/// normalized source rect in backing wl_buffer
|
||||
pub source_uv: [f32; 4],
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -208,15 +209,17 @@ pub fn collect_rendered_surface_tree_at(
|
||||
let pos = *parent_pos + surface_location(states);
|
||||
|
||||
if let Some(surf) = SurfaceBufWithImage::get_from_surface(states) {
|
||||
let extent = surf.image.extent_f32();
|
||||
let scale = surf.scale.max(1) as f32;
|
||||
let logical_size = surf.logical_size(states);
|
||||
let source_uv = surf.source_uv(states);
|
||||
|
||||
out.push(RenderedSurface {
|
||||
surface: surface.clone(),
|
||||
surface_id: surface.id(),
|
||||
image: surf.image,
|
||||
pos: Vec2::new(pos.x as f32, pos.y as f32),
|
||||
size: Vec2::new(extent[0] / scale, extent[1] / scale),
|
||||
size: Vec2::new(logical_size.w as f32, logical_size.h as f32),
|
||||
has_alpha: surf.has_alpha,
|
||||
source_uv,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -274,10 +277,8 @@ fn surface_accepts_input_states(
|
||||
|
||||
// fallback for normal rendered surfaces
|
||||
if let Some(surf) = SurfaceBufWithImage::get_from_surface(states) {
|
||||
let extent = surf.image.extent_f32();
|
||||
let scale = surf.scale.max(1) as f32;
|
||||
|
||||
return local.x < extent[0] / scale && local.y < extent[1] / scale;
|
||||
let size = surf.logical_size(states);
|
||||
return local.x < size.w as f32 && local.y < size.h as f32;
|
||||
}
|
||||
|
||||
false
|
||||
@@ -328,11 +329,20 @@ pub fn rendered_surfaces_dirty(old: &[RenderedSurface], new: &[RenderedSurface])
|
||||
a.surface_id != b.surface_id
|
||||
|| a.pos != b.pos
|
||||
|| a.size != b.size
|
||||
|| *a.image.image() != *b.image.image()
|
||||
|| a.has_alpha != b.has_alpha
|
||||
|| a.source_uv != b.source_uv
|
||||
|| !Arc::ptr_eq(a.image.image(), b.image.image())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn compute_transforms(inner_extent: [u32; 2]) -> (Affine2, RangeInclusive<f32>) {
|
||||
pub fn compute_transforms(
|
||||
inner_extent: [u32; 2],
|
||||
server_side_decorations: bool,
|
||||
) -> (Affine2, RangeInclusive<f32>) {
|
||||
if !server_side_decorations {
|
||||
return (Affine2::IDENTITY, 0.0..=1.0);
|
||||
}
|
||||
|
||||
let ix = inner_extent[0].max(1) as f32;
|
||||
let iy = inner_extent[1].max(1) as f32;
|
||||
|
||||
@@ -359,9 +369,14 @@ pub fn build_hit_context(
|
||||
toplevel: &WlSurface,
|
||||
_popup_manager: &PopupManager,
|
||||
inner_extent: [u32; 2],
|
||||
server_side_decorations: bool,
|
||||
) -> WvrHitContext {
|
||||
let (mouse_transform, uv_range) = compute_transforms(inner_extent);
|
||||
let panel_height = BORDER_SIZE * 2 + BAR_SIZE;
|
||||
let (mouse_transform, uv_range) = compute_transforms(inner_extent, server_side_decorations);
|
||||
let panel_height = if server_side_decorations {
|
||||
BORDER_SIZE * 2 + BAR_SIZE
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let surfaces = collect_rendered_surface_tree(toplevel);
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ use smithay::{
|
||||
single_pixel_buffer::SinglePixelBufferUserData,
|
||||
},
|
||||
};
|
||||
use vulkano::{format::Format, image::view::ImageView};
|
||||
use wgui::gfx::WGfx;
|
||||
use wgui::gfx::{CommandBufferUsage, Format, ImageView, WGfx};
|
||||
use wlx_capture::frame::{DmabufFrame, FrameFormat, Transform};
|
||||
|
||||
use crate::graphics::dmabuf::{WGfxDmabuf, fourcc_to_vk};
|
||||
@@ -34,17 +33,16 @@ impl ImageImporter {
|
||||
&mut self,
|
||||
spb: &SinglePixelBufferUserData,
|
||||
) -> anyhow::Result<Arc<ImageView>> {
|
||||
let mut cmd_buf = self.gfx.create_xfer_command_buffer(
|
||||
vulkano::command_buffer::CommandBufferUsage::OneTimeSubmit,
|
||||
)?;
|
||||
let mut cmd_buf = self
|
||||
.gfx
|
||||
.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
|
||||
let rgba = spb.rgba8888();
|
||||
let image = cmd_buf.upload_image(1, 1, Format::R8G8B8A8_UNORM, &rgba)?;
|
||||
|
||||
cmd_buf.build_and_execute_now()?; //TODO: async
|
||||
|
||||
let image_view = ImageView::new_default(image)?;
|
||||
Ok(image_view)
|
||||
self.gfx.create_image_view(image)
|
||||
}
|
||||
|
||||
pub fn import_shm(
|
||||
@@ -53,9 +51,9 @@ impl ImageImporter {
|
||||
size: usize,
|
||||
bd: BufferData,
|
||||
) -> anyhow::Result<Arc<ImageView>> {
|
||||
let mut cmd_buf = self.gfx.create_xfer_command_buffer(
|
||||
vulkano::command_buffer::CommandBufferUsage::OneTimeSubmit,
|
||||
)?;
|
||||
let mut cmd_buf = self
|
||||
.gfx
|
||||
.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
|
||||
let fourcc = shm_format_to_fourcc(bd.format)
|
||||
.with_context(|| format!("Could not convert {:?} to fourcc", bd.format))?;
|
||||
@@ -96,7 +94,7 @@ impl ImageImporter {
|
||||
let image = cmd_buf.upload_image(bd.width as _, bd.height as _, format, &packed)?;
|
||||
cmd_buf.build_and_execute_now()?;
|
||||
|
||||
Ok(ImageView::new_default(image)?)
|
||||
self.gfx.create_image_view(image)
|
||||
}
|
||||
|
||||
pub fn get_or_import_dmabuf(&mut self, dmabuf: Dmabuf) -> anyhow::Result<Arc<ImageView>> {
|
||||
@@ -132,8 +130,11 @@ impl ImageImporter {
|
||||
}
|
||||
|
||||
let image = self.gfx.dmabuf_texture(frame)?;
|
||||
let image_view = ImageView::new_default(image)?;
|
||||
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");
|
||||
self.dmabufs.insert(key, image_view.clone());
|
||||
log::info!("DMA-buf import: cached ImageView");
|
||||
|
||||
Ok(image_view)
|
||||
}
|
||||
|
||||
@@ -182,11 +182,16 @@ impl RestrictedState {
|
||||
if entry.device.is_grabbed() {
|
||||
Ok(false)
|
||||
} else {
|
||||
entry
|
||||
.device
|
||||
.grab()
|
||||
.map(|()| true)
|
||||
.map_err(|error| with_device_context("grab", &entry.path, error))
|
||||
match entry.device.grab() {
|
||||
Ok(()) => {
|
||||
log::debug!(
|
||||
"exclusively grabbed input device: {}",
|
||||
evdev_device_desc(&entry.path, &entry.device)
|
||||
);
|
||||
Ok(true)
|
||||
}
|
||||
Err(error) => Err(with_device_context("grab", &entry.path, error)),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -218,10 +223,18 @@ impl RestrictedState {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(error) = entry.device.ungrab()
|
||||
&& first_error.is_none()
|
||||
{
|
||||
first_error = Some(with_device_context("ungrab", &entry.path, error));
|
||||
match entry.device.ungrab() {
|
||||
Ok(()) => {
|
||||
log::debug!(
|
||||
"ungrabbed input device: {}",
|
||||
evdev_device_desc(&entry.path, &entry.device)
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
if first_error.is_none() {
|
||||
first_error = Some(with_device_context("ungrab", &entry.path, error));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +274,15 @@ impl LibinputInterface for RestrictedInterface {
|
||||
|
||||
if state.desired_grabbed {
|
||||
device.grab().map_err(io_errno)?;
|
||||
log::debug!(
|
||||
"exclusively grabbed input device on open: {}",
|
||||
evdev_device_desc(path, &device)
|
||||
);
|
||||
} else {
|
||||
log::debug!(
|
||||
"opened input device (non-exclusive): {}",
|
||||
evdev_device_desc(path, &device)
|
||||
);
|
||||
}
|
||||
|
||||
state.devices.insert(
|
||||
@@ -275,7 +297,13 @@ impl LibinputInterface for RestrictedInterface {
|
||||
}
|
||||
|
||||
fn close_restricted(&mut self, fd: OwnedFd) {
|
||||
self.state.borrow_mut().devices.remove(&fd.as_raw_fd());
|
||||
let removed = self.state.borrow_mut().devices.remove(&fd.as_raw_fd());
|
||||
if let Some(removed) = removed {
|
||||
log::debug!(
|
||||
"closed input device (releasing grab): {}",
|
||||
evdev_device_desc(&removed.path, &removed.device)
|
||||
);
|
||||
}
|
||||
drop(fd);
|
||||
}
|
||||
}
|
||||
@@ -316,6 +344,8 @@ fn worker_main(
|
||||
return;
|
||||
}
|
||||
|
||||
log::debug!("input capture worker started");
|
||||
|
||||
let mut grabbed = false;
|
||||
let mut pending_grab = false;
|
||||
let mut watchdog = Instant::now() + WATCHDOG_TIMEOUT;
|
||||
@@ -563,9 +593,11 @@ fn process_libinput_event(par: ProcessLibinputEventParams) -> ProcessResult {
|
||||
if !par.grabbed {
|
||||
// while ungrabbed, arm an automatic grab that triggers after release
|
||||
if par.allow_deferred_grab
|
||||
&& !*par.pending_grab
|
||||
&& key_combo_is_pressed(KeyCombo::GrabRelease, &state.pressed_keys)
|
||||
{
|
||||
*par.pending_grab = true;
|
||||
log::debug!("key combo triggered: GrabRelease (grab deferred until release)");
|
||||
}
|
||||
|
||||
return ProcessResult::Continue;
|
||||
@@ -591,6 +623,8 @@ fn process_libinput_event(par: ProcessLibinputEventParams) -> ProcessResult {
|
||||
continue;
|
||||
}
|
||||
|
||||
log::debug!("key combo triggered: {combo:?} pressed={combo_pressed}");
|
||||
|
||||
if combo_pressed {
|
||||
state.active_combos.insert(combo);
|
||||
} else {
|
||||
@@ -908,6 +942,17 @@ fn io_errno(error: io::Error) -> i32 {
|
||||
error.raw_os_error().unwrap_or(libc::EIO)
|
||||
}
|
||||
|
||||
fn evdev_device_desc(path: &Path, device: &EvdevDevice) -> String {
|
||||
let id = device.input_id();
|
||||
format!(
|
||||
"{} (vendor {:04x}, product {:04x}) {}",
|
||||
device.name().unwrap_or("<unknown>"),
|
||||
id.vendor(),
|
||||
id.product(),
|
||||
path.display()
|
||||
)
|
||||
}
|
||||
|
||||
fn with_device_context(action: &str, path: &Path, error: io::Error) -> io::Error {
|
||||
io::Error::new(
|
||||
error.kind(),
|
||||
|
||||
+134
-12
@@ -51,13 +51,16 @@ use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use vulkano::image::view::ImageView;
|
||||
use wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use wayvr_ipc::packet_client::PositionMode;
|
||||
use wgui::{gfx::WGfx, globals::WguiGlobals, log::LogErr};
|
||||
use wgui::{
|
||||
gfx::{ImageView, WGfx},
|
||||
globals::WguiGlobals,
|
||||
log::LogErr,
|
||||
};
|
||||
use wlx_capture::frame::Transform;
|
||||
use wlx_common::{
|
||||
audio::{AudioSystem, SamplePlayer},
|
||||
audio,
|
||||
config::{DefaultPositioning, GeneralConfig, InputCaptureMethod},
|
||||
desktop_finder::DesktopFinder,
|
||||
};
|
||||
@@ -74,10 +77,11 @@ use crate::{
|
||||
window::CreateWindowParams,
|
||||
},
|
||||
},
|
||||
graphics::{ExtentExt, WGfxExtras},
|
||||
graphics::WGfxExtras,
|
||||
ipc::{event_queue::SyncEventQueue, ipc_server},
|
||||
overlays::{
|
||||
anchor::ALTTAB_HELP_NAME,
|
||||
drag_item::{DRAG_ITEM_NAME, create_drag_item},
|
||||
keyboard::KEYBOARD_NAME,
|
||||
wayvr::{WvrCommand, create_wl_window_overlay},
|
||||
},
|
||||
@@ -132,6 +136,11 @@ pub enum WayVRTask {
|
||||
NewExternalProcess(ExternalProcessRequest),
|
||||
ProcessTerminationRequest(process::ProcessHandle, KillSignal),
|
||||
CloseWindowRequest(window::WindowHandle),
|
||||
DndStarted {
|
||||
icon: Option<WlSurface>,
|
||||
pointer: usize,
|
||||
},
|
||||
DndDropped,
|
||||
}
|
||||
|
||||
pub struct WvrServerState {
|
||||
@@ -168,7 +177,7 @@ pub enum TickTask {
|
||||
NewExternalProcess(ExternalProcessRequest), // Call WayVRCompositor::add_client after receiving this message
|
||||
}
|
||||
|
||||
const KEY_REPEAT_DELAY: i32 = 200;
|
||||
const KEY_REPEAT_DELAY: i32 = 500;
|
||||
const KEY_REPEAT_RATE: i32 = 50;
|
||||
const WAYVR_SCREEN_RES: [i32; 2] = [2560, 1440];
|
||||
|
||||
@@ -288,8 +297,12 @@ impl WvrServerState {
|
||||
popup_manager: PopupManager::default(),
|
||||
viewporter,
|
||||
redraw_requests: HashSet::new(),
|
||||
no_server_decorations: HashSet::new(),
|
||||
pending_frame_callbacks: HashMap::new(),
|
||||
cursor_image: CursorImageStatus::default_named(),
|
||||
vr_pointer: 0,
|
||||
pointer_button_owner: None,
|
||||
dnd_pointer: None,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
@@ -638,6 +651,37 @@ impl WvrServerState {
|
||||
);
|
||||
}
|
||||
}
|
||||
WayVRTask::DndStarted { icon, pointer } => {
|
||||
// don't freeze on drag start
|
||||
wvr_server.mouse_freeze = Instant::now();
|
||||
|
||||
let selector = OverlaySelector::Name(DRAG_ITEM_NAME.clone());
|
||||
|
||||
app.tasks
|
||||
.enqueue(TaskType::Overlay(OverlayTask::DropImmediate(
|
||||
selector.clone(),
|
||||
)));
|
||||
|
||||
if let Some(icon) = icon {
|
||||
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Spawn(
|
||||
selector,
|
||||
SpawnPos::FixedNoRealign,
|
||||
Box::new(move |app| {
|
||||
create_drag_item(app, icon, pointer)
|
||||
.inspect_err(|e| {
|
||||
log::warn!("Could not create drag item overlay: {e:?}")
|
||||
})
|
||||
.ok()
|
||||
}),
|
||||
)));
|
||||
}
|
||||
}
|
||||
WayVRTask::DndDropped => {
|
||||
app.tasks
|
||||
.enqueue(TaskType::Overlay(OverlayTask::DropImmediate(
|
||||
OverlaySelector::Name(DRAG_ITEM_NAME.clone()),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,8 +696,7 @@ impl WvrServerState {
|
||||
&mut app.input_state,
|
||||
&mut app.tasks,
|
||||
&mut app.hid_provider,
|
||||
&mut app.audio_sample_player,
|
||||
&mut app.audio_system,
|
||||
&mut app.audio,
|
||||
&app.wgui_globals,
|
||||
&app.session.config,
|
||||
);
|
||||
@@ -774,8 +817,7 @@ impl WvrServerState {
|
||||
input_state: &mut InputState,
|
||||
tasks: &mut TaskContainer,
|
||||
hid_wrapper: &mut HidWrapper,
|
||||
audio_sample_player: &mut SamplePlayer,
|
||||
audio_system: &mut AudioSystem,
|
||||
audio: &mut Box<dyn audio::AudioProvider>,
|
||||
globals: &WguiGlobals,
|
||||
config: &GeneralConfig,
|
||||
) {
|
||||
@@ -924,7 +966,7 @@ impl WvrServerState {
|
||||
}
|
||||
}
|
||||
input_capture::CapturedEvent::Grabbed => {
|
||||
audio_sample_player.play_sample(audio_system, "input_grab");
|
||||
audio.play_sample("input_grab");
|
||||
if !self.grab_toast_sent {
|
||||
self.grab_toast_sent = true;
|
||||
let text = &globals.i18n().translate("NOTIFICATION.WE_ARE_GRABBING");
|
||||
@@ -937,7 +979,7 @@ impl WvrServerState {
|
||||
self.manager.release_all_keys();
|
||||
self.has_input_focus = false;
|
||||
self.wm.keyboard_focus = None;
|
||||
audio_sample_player.play_sample(audio_system, "input_ungrab");
|
||||
audio.play_sample("input_ungrab");
|
||||
}
|
||||
input_capture::CapturedEvent::KeyCombo { combo, pressed } => match combo {
|
||||
input_capture::KeyCombo::CloseApp if pressed => {
|
||||
@@ -992,13 +1034,14 @@ impl WvrServerState {
|
||||
let toplevel = window.toplevel.wl_surface().clone();
|
||||
let inner_extent = with_states(&toplevel, |states| {
|
||||
SurfaceBufWithImage::get_from_surface(states)
|
||||
.map_or([1, 1], |s| s.image.extent_u32arr())
|
||||
.map_or([1, 1], |s| s.image.extent_2d())
|
||||
});
|
||||
|
||||
let hit_ctx = build_hit_context(
|
||||
&toplevel,
|
||||
&self.manager.state.popup_manager,
|
||||
inner_extent,
|
||||
self.manager.state.has_server_side_decorations(&toplevel),
|
||||
);
|
||||
|
||||
let new_x = (hover.pos.x + mouse_delta.x).clamp(0., inner_extent[0] as f64);
|
||||
@@ -1183,6 +1226,47 @@ impl WvrServerState {
|
||||
self.manager.send_pointer_button(index, pressed);
|
||||
}
|
||||
|
||||
pub fn set_vr_pointer(&mut self, pointer: usize) {
|
||||
self.manager.state.vr_pointer = pointer;
|
||||
}
|
||||
|
||||
pub fn set_vr_pointer_button(&mut self, pointer: usize, pressed: bool) {
|
||||
self.manager.state.vr_pointer = pointer;
|
||||
if pressed {
|
||||
self.manager.state.pointer_button_owner = Some(pointer);
|
||||
} else if self.manager.state.dnd_pointer.is_none()
|
||||
&& self.manager.state.pointer_button_owner == Some(pointer)
|
||||
{
|
||||
self.manager.state.pointer_button_owner = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dnd_active(&self) -> bool {
|
||||
self.manager.state.dnd_pointer.is_some()
|
||||
}
|
||||
|
||||
pub fn dnd_active_for_pointer(&self, pointer: usize) -> bool {
|
||||
self.manager.state.dnd_pointer == Some(pointer)
|
||||
}
|
||||
|
||||
pub fn clear_dnd_focus(&mut self, pointer: usize) {
|
||||
if !self.dnd_active_for_pointer(pointer) {
|
||||
return;
|
||||
}
|
||||
|
||||
let pos = self
|
||||
.wm
|
||||
.mouse
|
||||
.as_ref()
|
||||
.map_or(DVec2::ZERO, |mouse| mouse.pos);
|
||||
self.manager
|
||||
.send_mouse_move(None, pos, DVec2::ZERO, DVec2::ZERO);
|
||||
}
|
||||
|
||||
pub fn send_dnd_release(&mut self, index: MouseIndex) {
|
||||
self.manager.send_pointer_button(index, false);
|
||||
}
|
||||
|
||||
pub fn send_mouse_scroll(
|
||||
&mut self,
|
||||
hover_window: window::WindowHandle,
|
||||
@@ -1354,9 +1438,47 @@ pub struct SurfaceBufWithImage {
|
||||
pub transform: Transform,
|
||||
pub scale: i32,
|
||||
pub dmabuf: bool,
|
||||
pub has_alpha: bool,
|
||||
}
|
||||
|
||||
impl SurfaceBufWithImage {
|
||||
/// Logical size of wl_surface after buffer scaling and wp_viewport
|
||||
pub fn logical_size(&self, surface_data: &SurfaceData) -> Size<i32, Logical> {
|
||||
use smithay::wayland::viewporter::ViewportCachedState;
|
||||
|
||||
let extent = self.image.extent_2d();
|
||||
let scale = self.scale.max(1) as u32;
|
||||
let buffer_size = Size::new(
|
||||
(extent[0] / scale).max(1) as i32,
|
||||
(extent[1] / scale).max(1) as i32,
|
||||
);
|
||||
|
||||
let mut guard = surface_data.cached_state.get::<ViewportCachedState>();
|
||||
guard.current().size().unwrap_or(buffer_size)
|
||||
}
|
||||
|
||||
/// Normalized source rectangle in backing image cropped by wp_viewport
|
||||
pub fn source_uv(&self, surface_data: &SurfaceData) -> [f32; 4] {
|
||||
use smithay::wayland::viewporter::ViewportCachedState;
|
||||
|
||||
let extent = self.image.extent_2d();
|
||||
let scale = self.scale.max(1) as f64;
|
||||
let buffer_w = (extent[0] as f64 / scale).max(1.0);
|
||||
let buffer_h = (extent[1] as f64 / scale).max(1.0);
|
||||
|
||||
let mut guard = surface_data.cached_state.get::<ViewportCachedState>();
|
||||
let viewport = *guard.current();
|
||||
let Some(src) = viewport.src else {
|
||||
return [0.0, 0.0, 1.0, 1.0];
|
||||
};
|
||||
|
||||
let u0 = (src.loc.x / buffer_w).clamp(0.0, 1.0) as f32;
|
||||
let v0 = (src.loc.y / buffer_h).clamp(0.0, 1.0) as f32;
|
||||
let u1 = ((src.loc.x + src.size.w) / buffer_w).clamp(0.0, 1.0) as f32;
|
||||
let v1 = ((src.loc.y + src.size.h) / buffer_h).clamp(0.0, 1.0) as f32;
|
||||
[u0, v0, u1, v1]
|
||||
}
|
||||
|
||||
fn apply_to_surface(
|
||||
self,
|
||||
surface_data: &SurfaceData,
|
||||
|
||||
@@ -2,12 +2,18 @@ use std::rc::Rc;
|
||||
|
||||
use glam::DVec2;
|
||||
use slotmap::{DenseSlotMap, new_key_type};
|
||||
use smithay::utils::{Logical, Size};
|
||||
use smithay::utils::{Logical, Serial, Size};
|
||||
use smithay::wayland::shell::xdg::ToplevelSurface;
|
||||
use wayvr_ipc::packet_server;
|
||||
|
||||
use crate::backend::wayvr::process;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PendingConfigure {
|
||||
pub size: Size<i32, Logical>,
|
||||
pub serial: Serial,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Window {
|
||||
pub min_size: Size<i32, Logical>,
|
||||
@@ -19,7 +25,7 @@ pub struct Window {
|
||||
pub toplevel: Rc<ToplevelSurface>,
|
||||
pub process: process::ProcessHandle,
|
||||
|
||||
pub pending_configure_size: Option<Size<i32, Logical>>,
|
||||
pub pending_configure: Option<PendingConfigure>,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
@@ -39,7 +45,7 @@ impl Window {
|
||||
visible: true,
|
||||
toplevel,
|
||||
process,
|
||||
pending_configure_size: None,
|
||||
pending_configure: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +74,10 @@ impl Window {
|
||||
fn send_size_configure(&mut self, size: Size<i32, Logical>, bounds: Size<i32, Logical>) {
|
||||
let clamped_size = self.clamp_configure_size(size, bounds);
|
||||
|
||||
if self.pending_configure_size == Some(clamped_size) {
|
||||
if self
|
||||
.pending_configure
|
||||
.is_some_and(|pending| pending.size == clamped_size)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,10 +86,13 @@ impl Window {
|
||||
state.size = Some(clamped_size);
|
||||
});
|
||||
|
||||
self.toplevel.send_configure();
|
||||
let serial = self.toplevel.send_configure();
|
||||
|
||||
self.bounds = bounds;
|
||||
self.pending_configure_size = Some(clamped_size);
|
||||
self.pending_configure = Some(PendingConfigure {
|
||||
size: clamped_size,
|
||||
serial,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn checked_configure_size(&mut self, size: Size<i32, Logical>) {
|
||||
@@ -91,7 +103,11 @@ impl Window {
|
||||
self.send_size_configure(size, bounds);
|
||||
}
|
||||
|
||||
pub fn remember_committed_size(&mut self, size: Size<i32, Logical>) -> bool {
|
||||
pub fn remember_committed_size(
|
||||
&mut self,
|
||||
size: Size<i32, Logical>,
|
||||
configure_serial: Option<Serial>,
|
||||
) -> bool {
|
||||
let size_x = size.w.max(1) as u32;
|
||||
let size_y = size.h.max(1) as u32;
|
||||
|
||||
@@ -100,7 +116,12 @@ impl Window {
|
||||
self.size_x = size_x;
|
||||
self.size_y = size_y;
|
||||
|
||||
self.pending_configure_size = None;
|
||||
if self
|
||||
.pending_configure
|
||||
.is_some_and(|pending| Some(pending.serial) == configure_serial)
|
||||
{
|
||||
self.pending_configure = None;
|
||||
}
|
||||
|
||||
changed
|
||||
}
|
||||
@@ -155,7 +176,7 @@ impl WindowManager {
|
||||
par.min_size,
|
||||
par.max_size,
|
||||
);
|
||||
window.remember_committed_size(Size::new(par.size_x as i32, par.size_y as i32));
|
||||
window.remember_committed_size(Size::new(par.size_x as i32, par.size_y as i32), None);
|
||||
self.windows.insert(window)
|
||||
}
|
||||
|
||||
|
||||
@@ -198,6 +198,7 @@ pub struct AutoSettings {
|
||||
pub default_curvature: f32,
|
||||
pub default_opacity: f32,
|
||||
pub default_positioning: DefaultPositioning,
|
||||
pub middle_grabs_single: bool,
|
||||
}
|
||||
|
||||
fn get_settings_path() -> PathBuf {
|
||||
@@ -273,6 +274,7 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
|
||||
default_curvature: config.default_curvature,
|
||||
default_opacity: config.default_opacity,
|
||||
default_positioning: config.default_positioning,
|
||||
middle_grabs_single: config.middle_grabs_single,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
use image_dds::{ImageFormat, Surface};
|
||||
use std::{io::Read, sync::Arc};
|
||||
use vulkano::{
|
||||
DeviceSize,
|
||||
buffer::{Buffer, BufferCreateInfo, BufferUsage, Subbuffer},
|
||||
command_buffer::CopyBufferToImageInfo,
|
||||
format::Format,
|
||||
image::{Image, ImageCreateInfo, ImageType, ImageUsage},
|
||||
memory::allocator::{AllocationCreateInfo, MemoryTypeFilter},
|
||||
};
|
||||
use wgui::gfx::cmd::XferCommandBuffer;
|
||||
use wgui::gfx::{Format, Image, cmd::XferCommandBufferBuilder};
|
||||
|
||||
pub trait WlxCommandBufferDds {
|
||||
fn upload_image_dds<R>(&mut self, r: R) -> anyhow::Result<Arc<Image>>
|
||||
@@ -16,7 +8,7 @@ pub trait WlxCommandBufferDds {
|
||||
R: Read;
|
||||
}
|
||||
|
||||
impl WlxCommandBufferDds for XferCommandBuffer {
|
||||
impl WlxCommandBufferDds for XferCommandBufferBuilder {
|
||||
fn upload_image_dds<R>(&mut self, r: R) -> anyhow::Result<Arc<Image>>
|
||||
where
|
||||
R: Read,
|
||||
@@ -33,42 +25,16 @@ impl WlxCommandBufferDds for XferCommandBuffer {
|
||||
anyhow::bail!("Not a 2D texture.")
|
||||
}
|
||||
|
||||
let image = Image::new(
|
||||
self.graphics.memory_allocator.clone(),
|
||||
ImageCreateInfo {
|
||||
image_type: ImageType::Dim2d,
|
||||
format: dds_to_vk(surface.image_format)?,
|
||||
extent: [surface.width, surface.height, surface.depth],
|
||||
usage: ImageUsage::TRANSFER_DST | ImageUsage::TRANSFER_SRC | ImageUsage::SAMPLED,
|
||||
..Default::default()
|
||||
},
|
||||
AllocationCreateInfo::default(),
|
||||
)?;
|
||||
|
||||
let buffer: Subbuffer<[u8]> = Buffer::new_slice(
|
||||
self.graphics.memory_allocator.clone(),
|
||||
BufferCreateInfo {
|
||||
usage: BufferUsage::TRANSFER_SRC,
|
||||
..Default::default()
|
||||
},
|
||||
AllocationCreateInfo {
|
||||
memory_type_filter: MemoryTypeFilter::PREFER_HOST
|
||||
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
|
||||
..Default::default()
|
||||
},
|
||||
surface.data.len() as DeviceSize,
|
||||
)?;
|
||||
|
||||
buffer.write()?.copy_from_slice(surface.data);
|
||||
|
||||
self.command_buffer
|
||||
.copy_buffer_to_image(CopyBufferToImageInfo::buffer_image(buffer, image.clone()))?;
|
||||
|
||||
Ok(image)
|
||||
self.upload_image(
|
||||
surface.width,
|
||||
surface.height,
|
||||
dds_to_wgfx(surface.image_format)?,
|
||||
surface.data,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dds_to_vk(dds_fmt: ImageFormat) -> anyhow::Result<Format> {
|
||||
pub fn dds_to_wgfx(dds_fmt: ImageFormat) -> anyhow::Result<Format> {
|
||||
match dds_fmt {
|
||||
ImageFormat::R8Unorm => Ok(Format::R8_UNORM),
|
||||
ImageFormat::Rgba8Unorm => Ok(Format::R8G8B8A8_UNORM),
|
||||
|
||||
+58
-344
@@ -1,306 +1,47 @@
|
||||
use std::{
|
||||
mem::MaybeUninit,
|
||||
os::fd::{FromRawFd, IntoRawFd},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use vulkano::{
|
||||
VulkanError, VulkanObject,
|
||||
device::Device,
|
||||
format::Format,
|
||||
image::{
|
||||
Image, ImageAspect, ImageCreateInfo, ImageMemory, ImageTiling, ImageType, ImageUsage,
|
||||
SubresourceLayout, sys::RawImage, view::ImageView,
|
||||
},
|
||||
memory::{
|
||||
DedicatedAllocation, DeviceMemory, ExternalMemoryHandleType, ExternalMemoryHandleTypes,
|
||||
MemoryAllocateInfo, MemoryImportInfo, MemoryPropertyFlags, ResourceMemory,
|
||||
allocator::{
|
||||
AllocationCreateInfo, MemoryAllocatePreference, MemoryAllocator, MemoryTypeFilter,
|
||||
},
|
||||
},
|
||||
sync::Sharing,
|
||||
};
|
||||
use wgui::gfx::WGfx;
|
||||
use wgui::gfx::{DmabufImportInfo, DmabufPlaneLayout, Format, Image, ImageView, WGfx};
|
||||
use wlx_capture::{DrmFormat, DrmFourcc, DrmModifier, frame::DmabufFrame};
|
||||
|
||||
pub trait WGfxDmabuf {
|
||||
fn dmabuf_texture_ex(
|
||||
&self,
|
||||
frame: DmabufFrame,
|
||||
tiling: ImageTiling,
|
||||
layouts: Vec<SubresourceLayout>,
|
||||
modifiers: &[u64],
|
||||
) -> anyhow::Result<Arc<Image>>;
|
||||
|
||||
fn dmabuf_texture(&self, frame: DmabufFrame) -> anyhow::Result<Arc<Image>>;
|
||||
}
|
||||
|
||||
impl WGfxDmabuf for WGfx {
|
||||
fn dmabuf_texture_ex(
|
||||
&self,
|
||||
frame: DmabufFrame,
|
||||
tiling: ImageTiling,
|
||||
layouts: Vec<SubresourceLayout>,
|
||||
modifiers: &[u64],
|
||||
) -> anyhow::Result<Arc<Image>> {
|
||||
let extent = [frame.format.width, frame.format.height, 1];
|
||||
let format = fourcc_to_vk(frame.format.drm_format.code)?;
|
||||
|
||||
let image = unsafe {
|
||||
create_dmabuf_image(
|
||||
self.device.clone(),
|
||||
ImageCreateInfo {
|
||||
format,
|
||||
extent,
|
||||
usage: ImageUsage::SAMPLED,
|
||||
external_memory_handle_types: ExternalMemoryHandleTypes::DMA_BUF,
|
||||
tiling,
|
||||
drm_format_modifiers: modifiers.to_owned(),
|
||||
drm_format_modifier_plane_layouts: layouts,
|
||||
..Default::default()
|
||||
},
|
||||
)?
|
||||
};
|
||||
|
||||
let requirements = image.memory_requirements()[0];
|
||||
let memory_type_index = self
|
||||
.memory_allocator
|
||||
.find_memory_type_index(
|
||||
requirements.memory_type_bits,
|
||||
MemoryTypeFilter {
|
||||
required_flags: MemoryPropertyFlags::DEVICE_LOCAL,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.context("failed to get memory type index")?;
|
||||
|
||||
debug_assert!(self.device.enabled_extensions().khr_external_memory_fd);
|
||||
debug_assert!(self.device.enabled_extensions().khr_external_memory);
|
||||
debug_assert!(self.device.enabled_extensions().ext_external_memory_dma_buf);
|
||||
|
||||
// only do the 1st
|
||||
unsafe {
|
||||
let Some(fd) = frame.planes[0].fd else {
|
||||
anyhow::bail!("DMA-buf plane has no FD");
|
||||
};
|
||||
|
||||
let file = std::fs::File::from_raw_fd(fd);
|
||||
let new_file = file.try_clone()?;
|
||||
let _ = file.into_raw_fd();
|
||||
|
||||
let memory = DeviceMemory::allocate_unchecked(
|
||||
self.device.clone(),
|
||||
MemoryAllocateInfo {
|
||||
allocation_size: requirements.layout.size(),
|
||||
memory_type_index,
|
||||
dedicated_allocation: Some(DedicatedAllocation::Image(&image)),
|
||||
..Default::default()
|
||||
},
|
||||
Some(MemoryImportInfo::Fd {
|
||||
file: new_file,
|
||||
handle_type: ExternalMemoryHandleType::DmaBuf,
|
||||
}),
|
||||
)?;
|
||||
|
||||
let mem_alloc = ResourceMemory::new_dedicated(memory);
|
||||
match image.bind_memory_unchecked([mem_alloc]) {
|
||||
Ok(image) => Ok(Arc::new(image)),
|
||||
Err(e) => {
|
||||
anyhow::bail!("Failed to bind memory to image: {}", e.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dmabuf_texture(&self, frame: DmabufFrame) -> anyhow::Result<Arc<Image>> {
|
||||
let mut modifiers: SmallVec<[u64; 4]> = smallvec![];
|
||||
let mut tiling: ImageTiling = ImageTiling::Optimal;
|
||||
let mut layouts: Vec<SubresourceLayout> = vec![];
|
||||
|
||||
if !matches!(frame.format.drm_format.modifier, DrmModifier::Invalid) {
|
||||
(0..frame.num_planes).for_each(|i| {
|
||||
let plane = &frame.planes[i];
|
||||
layouts.push(SubresourceLayout {
|
||||
offset: plane.offset.into(),
|
||||
size: 0,
|
||||
row_pitch: plane.stride as _,
|
||||
array_pitch: None,
|
||||
depth_pitch: None,
|
||||
});
|
||||
modifiers.push(frame.format.drm_format.modifier.into());
|
||||
});
|
||||
tiling = ImageTiling::DrmFormatModifier;
|
||||
}
|
||||
|
||||
self.dmabuf_texture_ex(frame, tiling, layouts, &modifiers)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::all, clippy::pedantic)]
|
||||
pub(super) unsafe fn create_dmabuf_image(
|
||||
device: Arc<Device>,
|
||||
create_info: ImageCreateInfo,
|
||||
) -> Result<RawImage, VulkanError> {
|
||||
let &ImageCreateInfo {
|
||||
flags,
|
||||
image_type,
|
||||
format,
|
||||
ref view_formats,
|
||||
extent,
|
||||
array_layers,
|
||||
mip_levels,
|
||||
samples,
|
||||
tiling,
|
||||
usage,
|
||||
stencil_usage,
|
||||
ref sharing,
|
||||
initial_layout,
|
||||
ref drm_format_modifiers,
|
||||
ref drm_format_modifier_plane_layouts,
|
||||
external_memory_handle_types,
|
||||
_ne: _,
|
||||
} = &create_info;
|
||||
|
||||
let (sharing_mode, queue_family_index_count, p_queue_family_indices) = match sharing {
|
||||
Sharing::Exclusive => (ash::vk::SharingMode::EXCLUSIVE, 0, &[] as _),
|
||||
Sharing::Concurrent(queue_family_indices) => (
|
||||
ash::vk::SharingMode::CONCURRENT,
|
||||
queue_family_indices.len() as u32,
|
||||
queue_family_indices.as_ptr(),
|
||||
),
|
||||
};
|
||||
|
||||
let mut create_info_vk = ash::vk::ImageCreateInfo {
|
||||
flags: flags.into(),
|
||||
image_type: image_type.into(),
|
||||
format: format.into(),
|
||||
extent: ash::vk::Extent3D {
|
||||
width: extent[0],
|
||||
height: extent[1],
|
||||
depth: extent[2],
|
||||
},
|
||||
mip_levels,
|
||||
array_layers,
|
||||
samples: samples.into(),
|
||||
tiling: tiling.into(),
|
||||
usage: usage.into(),
|
||||
sharing_mode,
|
||||
queue_family_index_count,
|
||||
p_queue_family_indices,
|
||||
initial_layout: initial_layout.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut drm_format_modifier_explicit_info_vk = None;
|
||||
let drm_format_modifier_plane_layouts_vk: SmallVec<[_; 4]>;
|
||||
let mut drm_format_modifier_list_info_vk = None;
|
||||
let mut external_memory_info_vk = None;
|
||||
let mut format_list_info_vk = None;
|
||||
let format_list_view_formats_vk: Vec<_>;
|
||||
let mut stencil_usage_info_vk = None;
|
||||
|
||||
if drm_format_modifiers.len() == 1 {
|
||||
drm_format_modifier_plane_layouts_vk = drm_format_modifier_plane_layouts
|
||||
.iter()
|
||||
.map(|subresource_layout| {
|
||||
let &SubresourceLayout {
|
||||
offset,
|
||||
size,
|
||||
row_pitch,
|
||||
array_pitch,
|
||||
depth_pitch,
|
||||
} = subresource_layout;
|
||||
|
||||
ash::vk::SubresourceLayout {
|
||||
offset,
|
||||
size,
|
||||
row_pitch,
|
||||
array_pitch: array_pitch.unwrap_or(0),
|
||||
depth_pitch: depth_pitch.unwrap_or(0),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let next = drm_format_modifier_explicit_info_vk.insert(
|
||||
ash::vk::ImageDrmFormatModifierExplicitCreateInfoEXT {
|
||||
drm_format_modifier: drm_format_modifiers[0],
|
||||
drm_format_modifier_plane_count: drm_format_modifier_plane_layouts_vk.len() as u32,
|
||||
p_plane_layouts: drm_format_modifier_plane_layouts_vk.as_ptr(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
next.p_next = create_info_vk.p_next;
|
||||
create_info_vk.p_next = next as *const _ as *const _;
|
||||
} else if drm_format_modifiers.len() > 1 {
|
||||
let next = drm_format_modifier_list_info_vk.insert(
|
||||
ash::vk::ImageDrmFormatModifierListCreateInfoEXT {
|
||||
drm_format_modifier_count: drm_format_modifiers.len() as u32,
|
||||
p_drm_format_modifiers: drm_format_modifiers.as_ptr(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
next.p_next = create_info_vk.p_next;
|
||||
create_info_vk.p_next = next as *const _ as *const _;
|
||||
}
|
||||
|
||||
if !external_memory_handle_types.is_empty() {
|
||||
let next = external_memory_info_vk.insert(ash::vk::ExternalMemoryImageCreateInfo {
|
||||
handle_types: external_memory_handle_types.into(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
next.p_next = create_info_vk.p_next;
|
||||
create_info_vk.p_next = next as *const _ as *const _;
|
||||
}
|
||||
|
||||
if !view_formats.is_empty() {
|
||||
format_list_view_formats_vk = view_formats
|
||||
.iter()
|
||||
.copied()
|
||||
.map(ash::vk::Format::from)
|
||||
.collect();
|
||||
|
||||
let next = format_list_info_vk.insert(ash::vk::ImageFormatListCreateInfo {
|
||||
view_format_count: format_list_view_formats_vk.len() as u32,
|
||||
p_view_formats: format_list_view_formats_vk.as_ptr(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
next.p_next = create_info_vk.p_next;
|
||||
create_info_vk.p_next = next as *const _ as *const _;
|
||||
}
|
||||
|
||||
if let Some(stencil_usage) = stencil_usage {
|
||||
let next = stencil_usage_info_vk.insert(ash::vk::ImageStencilUsageCreateInfo {
|
||||
stencil_usage: stencil_usage.into(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
next.p_next = create_info_vk.p_next;
|
||||
create_info_vk.p_next = next as *const _ as *const _;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let handle = {
|
||||
let fns = device.fns();
|
||||
let mut output = MaybeUninit::uninit();
|
||||
(fns.v1_0.create_image)(
|
||||
device.handle(),
|
||||
&create_info_vk,
|
||||
std::ptr::null(),
|
||||
output.as_mut_ptr(),
|
||||
)
|
||||
.result()
|
||||
.map_err(VulkanError::from)?;
|
||||
output.assume_init()
|
||||
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));
|
||||
let plane_layouts = if modifier.is_some() {
|
||||
(0..frame.num_planes)
|
||||
.map(|i| DmabufPlaneLayout {
|
||||
offset: frame.planes[i].offset.into(),
|
||||
row_pitch: frame.planes[i].stride as u64,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let Some(fd) = frame.planes[0].fd else {
|
||||
anyhow::bail!("DMA-buf plane has no FD");
|
||||
};
|
||||
|
||||
RawImage::from_handle(device, handle, create_info)
|
||||
log::trace!(
|
||||
"DMA-buf import start: {}x{}, format={:?}, modifier={:?}, planes={}",
|
||||
frame.format.width,
|
||||
frame.format.height,
|
||||
format,
|
||||
modifier,
|
||||
frame.num_planes,
|
||||
);
|
||||
|
||||
self.import_dmabuf(&DmabufImportInfo {
|
||||
extent: [frame.format.width, frame.format.height, 1],
|
||||
format,
|
||||
fd,
|
||||
modifier,
|
||||
plane_layouts,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,52 +54,23 @@ pub struct ExportedDmabufImage {
|
||||
}
|
||||
|
||||
pub fn export_dmabuf_image(
|
||||
allocator: Arc<dyn MemoryAllocator>,
|
||||
gfx: &WGfx,
|
||||
extent: [u32; 3],
|
||||
format: Format,
|
||||
modifier: DrmModifier,
|
||||
) -> anyhow::Result<ExportedDmabufImage> {
|
||||
let image = Image::new(
|
||||
allocator.clone(),
|
||||
ImageCreateInfo {
|
||||
image_type: ImageType::Dim2d,
|
||||
format,
|
||||
extent,
|
||||
usage: ImageUsage::TRANSFER_DST | ImageUsage::TRANSFER_SRC | ImageUsage::SAMPLED,
|
||||
tiling: ImageTiling::DrmFormatModifier,
|
||||
drm_format_modifiers: vec![modifier.into()],
|
||||
external_memory_handle_types: ExternalMemoryHandleTypes::DMA_BUF,
|
||||
..Default::default()
|
||||
},
|
||||
AllocationCreateInfo {
|
||||
allocate_preference: MemoryAllocatePreference::AlwaysAllocate,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.context("Could not create image to export")?;
|
||||
|
||||
let fd = match image.memory() {
|
||||
ImageMemory::Normal(planes) if planes.len() == 1 => {
|
||||
let plane = planes.first().unwrap();
|
||||
plane
|
||||
.device_memory()
|
||||
.export_fd(ExternalMemoryHandleType::DmaBuf)?
|
||||
}
|
||||
_ => anyhow::bail!("Could not export DMA-buf: invalid ImageMemory"),
|
||||
};
|
||||
|
||||
let layout = unsafe { image.subresource_layout_unchecked(ImageAspect::MemoryPlane0, 0, 0) };
|
||||
let exported = gfx.export_dmabuf_image(extent, format, modifier.into())?;
|
||||
|
||||
Ok(ExportedDmabufImage {
|
||||
view: ImageView::new_default(image)?,
|
||||
fd,
|
||||
modifier,
|
||||
offset: layout.offset as _,
|
||||
stride: layout.row_pitch as _,
|
||||
view: exported.view,
|
||||
fd: exported.fd,
|
||||
modifier: DrmModifier::from(exported.modifier),
|
||||
offset: exported.offset as _,
|
||||
stride: exported.stride as _,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn get_drm_formats(device: Arc<Device>) -> Vec<DrmFormat> {
|
||||
pub(super) fn get_drm_formats(gfx: &WGfx) -> Vec<DrmFormat> {
|
||||
let possible_formats = [
|
||||
DrmFourcc::Abgr8888,
|
||||
DrmFourcc::Xbgr8888,
|
||||
@@ -371,26 +83,28 @@ pub(super) fn get_drm_formats(device: Arc<Device>) -> Vec<DrmFormat> {
|
||||
let mut out_formats = vec![];
|
||||
|
||||
for &code in &possible_formats {
|
||||
let Ok(vk_fmt) = fourcc_to_vk(code) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(props) = device.physical_device().format_properties(vk_fmt) else {
|
||||
let Ok(format) = fourcc_to_vk(code) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for m in props
|
||||
.drm_format_modifier_properties
|
||||
.iter()
|
||||
.filter(|m| m.drm_format_modifier_plane_count == 1)
|
||||
.map(|m| m.drm_format_modifier)
|
||||
{
|
||||
out_formats.push(DrmFormat {
|
||||
code,
|
||||
modifier: DrmModifier::from(m),
|
||||
});
|
||||
match gfx.dmabuf_import_modifiers(format) {
|
||||
Ok(modifiers) => {
|
||||
for modifier in modifiers {
|
||||
log::debug!(
|
||||
"DMA-buf format {code} modifier {modifier:#018x}: sampled + importable"
|
||||
);
|
||||
out_formats.push(DrmFormat {
|
||||
code,
|
||||
modifier: DrmModifier::from(modifier),
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to query DMA-buf modifiers for {code}: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
// accept implicit modifiers
|
||||
// always accept implicit modifier
|
||||
out_formats.push(DrmFormat {
|
||||
code,
|
||||
modifier: DrmModifier::Invalid,
|
||||
|
||||
+115
-650
@@ -1,743 +1,208 @@
|
||||
pub mod dds;
|
||||
pub mod dmabuf;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use glam::{Vec2, vec2};
|
||||
use smallvec::SmallVec;
|
||||
use vulkano::{
|
||||
buffer::{BufferCreateInfo, BufferUsage},
|
||||
command_buffer::{CommandBufferUsage, PrimaryAutoCommandBuffer, PrimaryCommandBufferAbstract},
|
||||
image::view::ImageView,
|
||||
memory::allocator::{AllocationCreateInfo, MemoryTypeFilter},
|
||||
sync::GpuFuture,
|
||||
use wgui::gfx::{
|
||||
BufferUsage, BuiltCommandBuffer, Format, ShaderModule, Vert2Buf, Vert2Uv, WGfx, WGfxCreateInfo,
|
||||
};
|
||||
use wgui::gfx::WGfx;
|
||||
|
||||
#[cfg(feature = "openvr")]
|
||||
use vulkano::instance::InstanceCreateFlags;
|
||||
use wlx_capture::DrmFormat;
|
||||
|
||||
use crate::shaders::{
|
||||
frag_color, frag_grid, frag_screen, frag_simple, frag_sky, frag_srgb, vert_quad,
|
||||
};
|
||||
|
||||
#[cfg(feature = "openxr")]
|
||||
use {ash::vk, std::os::raw::c_void};
|
||||
|
||||
use vulkano::{
|
||||
self, VulkanObject,
|
||||
buffer::{Buffer, BufferContents, Subbuffer},
|
||||
device::{
|
||||
DeviceCreateInfo, DeviceExtensions, DeviceFeatures, Queue, QueueCreateInfo, QueueFlags,
|
||||
physical::{PhysicalDevice, PhysicalDeviceType},
|
||||
},
|
||||
format::Format,
|
||||
instance::{Instance, InstanceCreateInfo, InstanceExtensions},
|
||||
pipeline::graphics::vertex_input::Vertex,
|
||||
shader::ShaderModule,
|
||||
frag_color, frag_grid, frag_screen, frag_simple, frag_sky, frag_srgb, frag_wayland, vert_quad,
|
||||
};
|
||||
|
||||
use dmabuf::get_drm_formats;
|
||||
|
||||
pub type Vert2Buf = Subbuffer<[Vert2Uv]>;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(BufferContents, Vertex, Copy, Clone, Debug)]
|
||||
pub struct Vert2Uv {
|
||||
#[format(R32G32_SFLOAT)]
|
||||
pub in_pos: [f32; 2],
|
||||
#[format(R32G32_SFLOAT)]
|
||||
pub in_uv: [f32; 2],
|
||||
}
|
||||
|
||||
pub struct WGfxExtras {
|
||||
pub shaders: HashMap<&'static str, Arc<ShaderModule>>,
|
||||
pub drm_formats: Arc<[DrmFormat]>,
|
||||
pub queue_capture: Option<Arc<Queue>>,
|
||||
pub quad_verts: Vert2Buf,
|
||||
pub fallback_image: Arc<ImageView>,
|
||||
pub drm_device: Option<(i64, i64)>,
|
||||
}
|
||||
|
||||
impl WGfxExtras {
|
||||
pub fn new(gfx: Arc<WGfx>, queue_capture: Option<Arc<Queue>>) -> anyhow::Result<Self> {
|
||||
pub fn new(gfx: &Arc<WGfx>) -> anyhow::Result<Self> {
|
||||
let mut shaders = HashMap::new();
|
||||
shaders.insert("vert_quad", vert_quad::load(gfx)?);
|
||||
shaders.insert("frag_color", frag_color::load(gfx)?);
|
||||
shaders.insert("frag_srgb", frag_srgb::load(gfx)?);
|
||||
shaders.insert("frag_sky", frag_sky::load(gfx)?);
|
||||
shaders.insert("frag_grid", frag_grid::load(gfx)?);
|
||||
shaders.insert("frag_screen", frag_screen::load(gfx)?);
|
||||
shaders.insert("frag_simple", frag_simple::load(gfx)?);
|
||||
shaders.insert("frag_wayland", frag_wayland::load(gfx)?);
|
||||
|
||||
let shader = vert_quad::load(gfx.device.clone())?;
|
||||
shaders.insert("vert_quad", shader);
|
||||
|
||||
let shader = frag_color::load(gfx.device.clone())?;
|
||||
shaders.insert("frag_color", shader);
|
||||
|
||||
let shader = frag_srgb::load(gfx.device.clone())?;
|
||||
shaders.insert("frag_srgb", shader);
|
||||
|
||||
let shader = frag_sky::load(gfx.device.clone())?;
|
||||
shaders.insert("frag_sky", shader);
|
||||
|
||||
let shader = frag_grid::load(gfx.device.clone())?;
|
||||
shaders.insert("frag_grid", shader);
|
||||
|
||||
let shader = frag_screen::load(gfx.device.clone())?;
|
||||
shaders.insert("frag_screen", shader);
|
||||
|
||||
let shader = frag_simple::load(gfx.device.clone())?;
|
||||
shaders.insert("frag_simple", shader);
|
||||
|
||||
let drm_formats = get_drm_formats(gfx.device.clone()).into();
|
||||
|
||||
let drm_formats = get_drm_formats(gfx).into();
|
||||
let vertices = [
|
||||
Vert2Uv {
|
||||
in_pos: [0., 0.],
|
||||
in_uv: [0., 0.],
|
||||
in_pos: [0.0, 0.0],
|
||||
in_uv: [0.0, 0.0],
|
||||
},
|
||||
Vert2Uv {
|
||||
in_pos: [1., 0.],
|
||||
in_uv: [1., 0.],
|
||||
in_pos: [1.0, 0.0],
|
||||
in_uv: [1.0, 0.0],
|
||||
},
|
||||
Vert2Uv {
|
||||
in_pos: [0., 1.],
|
||||
in_uv: [0., 1.],
|
||||
in_pos: [0.0, 1.0],
|
||||
in_uv: [0.0, 1.0],
|
||||
},
|
||||
Vert2Uv {
|
||||
in_pos: [1., 1.],
|
||||
in_uv: [1., 1.],
|
||||
in_pos: [1.0, 1.0],
|
||||
in_uv: [1.0, 1.0],
|
||||
},
|
||||
];
|
||||
let quad_verts = Buffer::from_iter(
|
||||
gfx.memory_allocator.clone(),
|
||||
BufferCreateInfo {
|
||||
usage: BufferUsage::VERTEX_BUFFER,
|
||||
..Default::default()
|
||||
},
|
||||
AllocationCreateInfo {
|
||||
memory_type_filter: MemoryTypeFilter::PREFER_DEVICE
|
||||
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
|
||||
..Default::default()
|
||||
},
|
||||
vertices,
|
||||
)?;
|
||||
let quad_verts = gfx.new_buffer(BufferUsage::VERTEX_BUFFER, &vertices)?;
|
||||
|
||||
let mut cmd_xfer = gfx.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
let fallback_image =
|
||||
cmd_xfer.upload_image(1, 1, Format::R8G8B8A8_SRGB, &[255, 0, 255, 255])?;
|
||||
cmd_xfer.build_and_execute_now()?;
|
||||
|
||||
let fallback_image = ImageView::new_default(fallback_image)?;
|
||||
|
||||
let p = gfx.device.physical_device().properties();
|
||||
|
||||
let drm_device = if let (Some(maj), Some(min)) = (p.render_major, p.render_minor) {
|
||||
log::info!("DRM render device: {maj} {min}");
|
||||
Some((maj, min))
|
||||
} else {
|
||||
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))
|
||||
});
|
||||
if drm_device.is_none() {
|
||||
log::warn!("No DRM device.");
|
||||
None
|
||||
};
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
shaders,
|
||||
drm_formats,
|
||||
queue_capture,
|
||||
quad_verts,
|
||||
fallback_image,
|
||||
drm_device,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const fn get_dmabuf_extensions() -> DeviceExtensions {
|
||||
DeviceExtensions {
|
||||
khr_external_memory: true,
|
||||
khr_external_memory_fd: true,
|
||||
ext_external_memory_dma_buf: true,
|
||||
..DeviceExtensions::empty()
|
||||
fn base_create_info() -> WGfxCreateInfo {
|
||||
WGfxCreateInfo {
|
||||
application_name: "wayvr".to_owned(),
|
||||
engine_name: "wayvr".to_owned(),
|
||||
surface_format: Format::R8G8B8A8_SRGB,
|
||||
..WGfxCreateInfo::default()
|
||||
}
|
||||
}
|
||||
|
||||
static VULKAN_LIBRARY: OnceLock<Arc<vulkano::VulkanLibrary>> = OnceLock::new();
|
||||
fn get_vulkan_library() -> &'static Arc<vulkano::VulkanLibrary> {
|
||||
VULKAN_LIBRARY.get_or_init(|| vulkano::VulkanLibrary::new().unwrap()) // want panic
|
||||
fn log_device(gfx: &WGfx) {
|
||||
log::info!("Using vkPhysicalDevice: {}", gfx.device_info().name);
|
||||
log::debug!(
|
||||
" DMA-buf supported: {}",
|
||||
gfx.capabilities().external_memory_dma_buf
|
||||
);
|
||||
log::debug!(
|
||||
" DRM format modifiers supported: {}",
|
||||
gfx.capabilities().image_drm_format_modifier
|
||||
);
|
||||
log::debug!(" Separate capture queue: {}", gfx.has_capture_queue());
|
||||
}
|
||||
|
||||
#[cfg(feature = "openxr")]
|
||||
unsafe extern "system" fn get_instance_proc_addr(
|
||||
instance: openxr::sys::platform::VkInstance,
|
||||
name: *const std::ffi::c_char,
|
||||
) -> Option<unsafe extern "system" fn()> {
|
||||
use vulkano::Handle;
|
||||
let instance = ash::vk::Instance::from_raw(instance as _);
|
||||
let library = get_vulkan_library();
|
||||
unsafe { library.get_instance_proc_addr(instance, name) }
|
||||
}
|
||||
|
||||
#[cfg(feature = "openxr")]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn init_openxr_graphics(
|
||||
xr_instance: openxr::Instance,
|
||||
system: openxr::SystemId,
|
||||
) -> anyhow::Result<(Arc<WGfx>, WGfxExtras)> {
|
||||
use std::ffi::{self, CString};
|
||||
let mut create_info = base_create_info();
|
||||
create_info
|
||||
.required_instance_extensions
|
||||
.push("VK_KHR_get_physical_device_properties2".to_owned());
|
||||
|
||||
use vulkano::{Handle, Version};
|
||||
|
||||
let instance_extensions = InstanceExtensions {
|
||||
khr_get_physical_device_properties2: true,
|
||||
..InstanceExtensions::empty()
|
||||
};
|
||||
|
||||
let instance_extensions_raw = instance_extensions
|
||||
.into_iter()
|
||||
.filter_map(|(name, enabled)| {
|
||||
if enabled {
|
||||
Some(ffi::CString::new(name).unwrap().into_raw().cast_const())
|
||||
// want panic
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let vk_target_version = vk::make_api_version(0, 1, 3, 0);
|
||||
let target_version = vulkano::Version::V1_3;
|
||||
let library = get_vulkan_library();
|
||||
|
||||
let vk_app_info_raw = vk::ApplicationInfo::default()
|
||||
.application_version(0)
|
||||
.engine_version(0)
|
||||
.api_version(vk_target_version);
|
||||
|
||||
let instance = unsafe {
|
||||
let vk_instance = xr_instance
|
||||
.create_vulkan_instance(
|
||||
let xr_for_instance = xr_instance.clone();
|
||||
let create_instance = move |get_instance_proc_addr: usize,
|
||||
create_info: *const std::ffi::c_void| {
|
||||
let get_instance_proc_addr: openxr::sys::platform::VkGetInstanceProcAddr =
|
||||
unsafe { std::mem::transmute(get_instance_proc_addr) };
|
||||
let result = unsafe {
|
||||
xr_for_instance.create_vulkan_instance(
|
||||
system,
|
||||
get_instance_proc_addr,
|
||||
std::ptr::from_ref(
|
||||
&vk::InstanceCreateInfo::default()
|
||||
.application_info(&vk_app_info_raw)
|
||||
.enabled_extension_names(&instance_extensions_raw),
|
||||
)
|
||||
.cast(),
|
||||
create_info.cast(),
|
||||
)
|
||||
.expect("XR error creating Vulkan instance")
|
||||
.map_err(vk::Result::from_raw)
|
||||
.expect("Vulkan error creating Vulkan instance");
|
||||
|
||||
Instance::from_handle(
|
||||
library.clone(),
|
||||
ash::vk::Instance::from_raw(vk_instance as _),
|
||||
InstanceCreateInfo {
|
||||
application_version: Version::major_minor(0, 0),
|
||||
engine_version: Version::major_minor(0, 0),
|
||||
max_api_version: Some(Version::V1_3),
|
||||
enabled_extensions: instance_extensions,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
.map_err(|e| anyhow::anyhow!("XR error creating Vulkan instance: {e:?}"))?;
|
||||
let instance =
|
||||
result.map_err(|e| anyhow::anyhow!("Vulkan error creating Vulkan instance: {e:?}"))?;
|
||||
Ok(instance as u64)
|
||||
};
|
||||
|
||||
let physical_device = unsafe {
|
||||
PhysicalDevice::from_handle(
|
||||
instance.clone(),
|
||||
vk::PhysicalDevice::from_raw(
|
||||
xr_instance.vulkan_graphics_device(system, instance.handle().as_raw() as _)? as _,
|
||||
),
|
||||
)
|
||||
}?;
|
||||
|
||||
let vk_device_properties = physical_device.properties();
|
||||
assert!(
|
||||
(vk_device_properties.api_version >= target_version),
|
||||
"Vulkan physical device doesn't support Vulkan {target_version}"
|
||||
);
|
||||
|
||||
log::info!(
|
||||
"Using vkPhysicalDevice: {}",
|
||||
physical_device.properties().device_name,
|
||||
);
|
||||
|
||||
let queue_families = try_all_queue_families(physical_device.as_ref())
|
||||
.expect("vkPhysicalDevice does not have a GRAPHICS / TRANSFER queue.");
|
||||
|
||||
let mut device_extensions = DeviceExtensions::empty();
|
||||
let dmabuf_extensions = get_dmabuf_extensions();
|
||||
|
||||
if physical_device
|
||||
.supported_extensions()
|
||||
.contains(&dmabuf_extensions)
|
||||
{
|
||||
device_extensions = device_extensions.union(&dmabuf_extensions);
|
||||
device_extensions.ext_image_drm_format_modifier = physical_device
|
||||
.supported_extensions()
|
||||
.ext_image_drm_format_modifier;
|
||||
}
|
||||
|
||||
if physical_device
|
||||
.supported_extensions()
|
||||
.ext_physical_device_drm
|
||||
{
|
||||
device_extensions.ext_physical_device_drm = true;
|
||||
}
|
||||
|
||||
let device_extensions_raw = device_extensions
|
||||
.into_iter()
|
||||
.filter_map(|(name, enabled)| {
|
||||
if enabled {
|
||||
Some(ffi::CString::new(name).unwrap().into_raw().cast_const())
|
||||
// want panic
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// If adding anything here, also add to the native device_create_info below
|
||||
let features = DeviceFeatures {
|
||||
dynamic_rendering: true,
|
||||
descriptor_binding_sampled_image_update_after_bind: true,
|
||||
..DeviceFeatures::empty()
|
||||
let xr_for_physical_device = xr_instance.clone();
|
||||
let get_physical_device = move |instance: u64| {
|
||||
unsafe { xr_for_physical_device.vulkan_graphics_device(system, instance as _) }
|
||||
.map(|physical_device| physical_device as u64)
|
||||
.map_err(|e| anyhow::anyhow!("XR error getting Vulkan graphics device: {e:?}"))
|
||||
};
|
||||
|
||||
let queue_create_infos = queue_families
|
||||
.iter()
|
||||
.map(|fam| {
|
||||
vk::DeviceQueueCreateInfo::default()
|
||||
.queue_family_index(fam.queue_family_index)
|
||||
.queue_priorities(&fam.priorities)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut device_create_info = vk::DeviceCreateInfo::default()
|
||||
.queue_create_infos(&queue_create_infos)
|
||||
.enabled_extension_names(&device_extensions_raw);
|
||||
|
||||
let mut dynamic_rendering =
|
||||
vk::PhysicalDeviceDynamicRenderingFeatures::default().dynamic_rendering(true);
|
||||
|
||||
let mut indexing_features = vk::PhysicalDeviceDescriptorIndexingFeatures::default()
|
||||
.descriptor_binding_sampled_image_update_after_bind(true);
|
||||
|
||||
dynamic_rendering.p_next = device_create_info.p_next.cast_mut();
|
||||
indexing_features.p_next = (&raw mut dynamic_rendering).cast::<c_void>();
|
||||
device_create_info.p_next = &raw mut indexing_features as *const c_void;
|
||||
|
||||
let (device, queues) = unsafe {
|
||||
let vk_device = xr_instance
|
||||
.create_vulkan_device(
|
||||
let xr_for_device = xr_instance;
|
||||
let create_device = move |get_instance_proc_addr: usize,
|
||||
physical_device: u64,
|
||||
create_info: *const std::ffi::c_void| {
|
||||
let get_instance_proc_addr: openxr::sys::platform::VkGetInstanceProcAddr =
|
||||
unsafe { std::mem::transmute(get_instance_proc_addr) };
|
||||
let result = unsafe {
|
||||
xr_for_device.create_vulkan_device(
|
||||
system,
|
||||
get_instance_proc_addr,
|
||||
physical_device.handle().as_raw() as _,
|
||||
(&raw const device_create_info).cast(),
|
||||
physical_device as _,
|
||||
create_info.cast(),
|
||||
)
|
||||
.expect("XR error creating Vulkan device")
|
||||
.map_err(vk::Result::from_raw)
|
||||
.expect("Vulkan error creating Vulkan device");
|
||||
|
||||
vulkano::device::Device::from_handle(
|
||||
physical_device,
|
||||
vk::Device::from_raw(vk_device as _),
|
||||
DeviceCreateInfo {
|
||||
queue_create_infos: queue_families
|
||||
.iter()
|
||||
.map(|fam| QueueCreateInfo {
|
||||
queue_family_index: fam.queue_family_index,
|
||||
queues: fam.priorities.clone(),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
enabled_extensions: device_extensions,
|
||||
enabled_features: features,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
.map_err(|e| anyhow::anyhow!("XR error creating Vulkan device: {e:?}"))?;
|
||||
let device =
|
||||
result.map_err(|e| anyhow::anyhow!("Vulkan error creating Vulkan device: {e:?}"))?;
|
||||
Ok(device as u64)
|
||||
};
|
||||
|
||||
log::debug!(
|
||||
" DMA-buf supported: {}",
|
||||
device.enabled_extensions().ext_external_memory_dma_buf
|
||||
);
|
||||
log::debug!(
|
||||
" DRM format modifiers supported: {}",
|
||||
device.enabled_extensions().ext_image_drm_format_modifier
|
||||
);
|
||||
|
||||
// Drop the CStrings
|
||||
device_extensions_raw
|
||||
.into_iter()
|
||||
.for_each(|c_string| unsafe {
|
||||
let _ = CString::from_raw(c_string.cast_mut());
|
||||
});
|
||||
|
||||
let (queue_gfx, queue_xfer, queue_capture) = unwrap_queues(queues.collect());
|
||||
|
||||
let gfx = WGfx::new_from_raw(
|
||||
instance,
|
||||
device,
|
||||
queue_gfx,
|
||||
queue_xfer,
|
||||
Format::R8G8B8A8_SRGB,
|
||||
);
|
||||
let extras = WGfxExtras::new(gfx.clone(), queue_capture)?;
|
||||
|
||||
let gfx = unsafe {
|
||||
WGfx::new_external_vulkan(
|
||||
&create_info,
|
||||
create_instance,
|
||||
get_physical_device,
|
||||
create_device,
|
||||
)?
|
||||
};
|
||||
log_device(&gfx);
|
||||
let extras = WGfxExtras::new(&gfx)?;
|
||||
Ok((gfx, extras))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[cfg(feature = "openvr")]
|
||||
pub fn init_openvr_graphics(
|
||||
mut vk_instance_extensions: InstanceExtensions,
|
||||
mut vk_device_extensions_fn: impl FnMut(&PhysicalDevice) -> DeviceExtensions,
|
||||
mut instance_extensions: Vec<String>,
|
||||
required_device_extensions: impl FnMut(u64) -> Vec<String>,
|
||||
) -> anyhow::Result<(Arc<WGfx>, WGfxExtras)> {
|
||||
use vulkano::device::Device;
|
||||
const PHYSICAL_DEVICE_PROPERTIES_2: &str = "VK_KHR_get_physical_device_properties2";
|
||||
if !instance_extensions
|
||||
.iter()
|
||||
.any(|extension| extension == PHYSICAL_DEVICE_PROPERTIES_2)
|
||||
{
|
||||
instance_extensions.push(PHYSICAL_DEVICE_PROPERTIES_2.to_owned());
|
||||
}
|
||||
|
||||
//#[cfg(debug_assertions)]
|
||||
//let layers = vec!["VK_LAYER_KHRONOS_validation".to_owned()];
|
||||
//#[cfg(not(debug_assertions))]
|
||||
|
||||
let layers = vec![];
|
||||
|
||||
log::debug!("Instance exts for runtime: {vk_instance_extensions:?}");
|
||||
|
||||
vk_instance_extensions.khr_get_physical_device_properties2 = true;
|
||||
|
||||
let instance = Instance::new(
|
||||
get_vulkan_library().clone(),
|
||||
InstanceCreateInfo {
|
||||
flags: InstanceCreateFlags::ENUMERATE_PORTABILITY,
|
||||
enabled_extensions: vk_instance_extensions,
|
||||
enabled_layers: layers,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
let dmabuf_extensions = get_dmabuf_extensions();
|
||||
|
||||
let (physical_device, my_extensions, queue_families) = instance
|
||||
.enumerate_physical_devices()?
|
||||
.filter_map(|p| {
|
||||
let mut my_extensions = vk_device_extensions_fn(&p);
|
||||
|
||||
if !p.supported_extensions().contains(&my_extensions) {
|
||||
log::debug!(
|
||||
"Not using {} due to missing extensions:",
|
||||
p.properties().device_name,
|
||||
);
|
||||
for (ext, missing) in p.supported_extensions().difference(&my_extensions) {
|
||||
if missing {
|
||||
log::debug!(" {ext}");
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
if p.supported_extensions().contains(&dmabuf_extensions) {
|
||||
my_extensions = my_extensions.union(&dmabuf_extensions);
|
||||
my_extensions.ext_image_drm_format_modifier =
|
||||
p.supported_extensions().ext_image_drm_format_modifier;
|
||||
}
|
||||
|
||||
if p.supported_extensions().ext_filter_cubic {
|
||||
my_extensions.ext_filter_cubic = true;
|
||||
}
|
||||
|
||||
if p.supported_extensions().ext_physical_device_drm {
|
||||
// needed for wayland_server
|
||||
my_extensions.ext_physical_device_drm = true;
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"Device exts for {}: {:?}",
|
||||
p.properties().device_name,
|
||||
my_extensions
|
||||
);
|
||||
Some((p, my_extensions))
|
||||
})
|
||||
.filter_map(|(p, my_extensions)| {
|
||||
try_all_queue_families(p.as_ref()).map(|families| (p, my_extensions, families))
|
||||
})
|
||||
.min_by_key(|(p, _, families)| prio_from_device_type(p) * 10 + prio_from_families(families))
|
||||
.expect("no suitable physical device found");
|
||||
|
||||
log::info!(
|
||||
"Using vkPhysicalDevice: {}",
|
||||
physical_device.properties().device_name,
|
||||
);
|
||||
|
||||
let (device, queues) = Device::new(
|
||||
physical_device,
|
||||
DeviceCreateInfo {
|
||||
enabled_extensions: my_extensions,
|
||||
enabled_features: DeviceFeatures {
|
||||
dynamic_rendering: true,
|
||||
descriptor_binding_sampled_image_update_after_bind: true,
|
||||
..DeviceFeatures::empty()
|
||||
},
|
||||
queue_create_infos: queue_families
|
||||
.iter()
|
||||
.map(|fam| QueueCreateInfo {
|
||||
queue_family_index: fam.queue_family_index,
|
||||
queues: fam.priorities.clone(),
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
log::debug!(
|
||||
" DMA-buf supported: {}",
|
||||
device.enabled_extensions().ext_external_memory_dma_buf
|
||||
);
|
||||
log::debug!(
|
||||
" DRM format modifiers supported: {}",
|
||||
device.enabled_extensions().ext_image_drm_format_modifier
|
||||
);
|
||||
|
||||
let (queue_gfx, queue_xfer, queue_capture) = unwrap_queues(queues.collect());
|
||||
|
||||
let gfx = WGfx::new_from_raw(
|
||||
instance,
|
||||
device,
|
||||
queue_gfx,
|
||||
queue_xfer,
|
||||
Format::R8G8B8A8_SRGB,
|
||||
);
|
||||
let extras = WGfxExtras::new(gfx.clone(), queue_capture)?;
|
||||
log::debug!("Instance exts for runtime: {instance_extensions:?}");
|
||||
|
||||
let mut create_info = base_create_info();
|
||||
create_info.required_instance_extensions = instance_extensions;
|
||||
let gfx = WGfx::new_with_device_extensions(&create_info, required_device_extensions)?;
|
||||
log_device(&gfx);
|
||||
let extras = WGfxExtras::new(&gfx)?;
|
||||
Ok((gfx, extras))
|
||||
}
|
||||
|
||||
pub fn upload_quad_vertices(
|
||||
buf: &mut Subbuffer<[Vert2Uv]>,
|
||||
width: f32,
|
||||
height: f32,
|
||||
x: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
) -> anyhow::Result<()> {
|
||||
let rw = width;
|
||||
let rh = height;
|
||||
|
||||
let x0 = x / rw;
|
||||
let y0 = y / rh;
|
||||
|
||||
let x1 = w / rw + x0;
|
||||
let y1 = h / rh + y0;
|
||||
|
||||
let data = [
|
||||
Vert2Uv {
|
||||
in_pos: [x0, y0],
|
||||
in_uv: [0.0, 0.0],
|
||||
},
|
||||
Vert2Uv {
|
||||
in_pos: [x0, y1],
|
||||
in_uv: [0.0, 1.0],
|
||||
},
|
||||
Vert2Uv {
|
||||
in_pos: [x1, y0],
|
||||
in_uv: [1.0, 0.0],
|
||||
},
|
||||
Vert2Uv {
|
||||
in_pos: [x1, y1],
|
||||
in_uv: [1.0, 1.0],
|
||||
},
|
||||
];
|
||||
|
||||
buf.write()?[0..4].copy_from_slice(&data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct QueueFamilyLayout {
|
||||
queue_family_index: u32,
|
||||
priorities: Vec<f32>,
|
||||
}
|
||||
|
||||
fn prio_from_device_type(physical_device: &PhysicalDevice) -> u32 {
|
||||
match physical_device.properties().device_type {
|
||||
PhysicalDeviceType::DiscreteGpu => 0,
|
||||
PhysicalDeviceType::IntegratedGpu => 1,
|
||||
PhysicalDeviceType::VirtualGpu => 2,
|
||||
PhysicalDeviceType::Cpu => 3,
|
||||
_ => 4,
|
||||
}
|
||||
}
|
||||
|
||||
const fn prio_from_families(families: &[QueueFamilyLayout]) -> u32 {
|
||||
match families.len() {
|
||||
2 | 3 => 0,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_queues(queues: Vec<Arc<Queue>>) -> (Arc<Queue>, Arc<Queue>, Option<Arc<Queue>>) {
|
||||
match queues[..] {
|
||||
[ref g, ref t, ref c] => (g.clone(), t.clone(), Some(c.clone())),
|
||||
[ref gt, ref c] => (gt.clone(), gt.clone(), Some(c.clone())),
|
||||
[ref gt] => (gt.clone(), gt.clone(), None),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_all_queue_families(physical_device: &PhysicalDevice) -> Option<Vec<QueueFamilyLayout>> {
|
||||
queue_families_priorities(
|
||||
physical_device,
|
||||
vec![
|
||||
// main-thread graphics + uploads
|
||||
QueueFlags::GRAPHICS | QueueFlags::TRANSFER,
|
||||
// capture-thread uploads
|
||||
QueueFlags::TRANSFER,
|
||||
],
|
||||
)
|
||||
.or_else(|| {
|
||||
queue_families_priorities(
|
||||
physical_device,
|
||||
vec![
|
||||
// main thread graphics
|
||||
QueueFlags::GRAPHICS,
|
||||
// main thread uploads
|
||||
QueueFlags::TRANSFER,
|
||||
// capture thread uploads
|
||||
QueueFlags::TRANSFER,
|
||||
],
|
||||
)
|
||||
})
|
||||
.or_else(|| {
|
||||
queue_families_priorities(
|
||||
physical_device,
|
||||
// main thread-only. software capture not supported.
|
||||
vec![QueueFlags::GRAPHICS | QueueFlags::TRANSFER],
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn queue_families_priorities(
|
||||
physical_device: &PhysicalDevice,
|
||||
mut requested_queues: Vec<QueueFlags>,
|
||||
) -> Option<Vec<QueueFamilyLayout>> {
|
||||
let mut result = Vec::with_capacity(3);
|
||||
|
||||
for (idx, props) in physical_device.queue_family_properties().iter().enumerate() {
|
||||
let mut remaining = props.queue_count;
|
||||
let mut want = 0usize;
|
||||
|
||||
requested_queues.retain(|requested| {
|
||||
if props.queue_flags.intersects(*requested) && remaining > 0 {
|
||||
remaining -= 1;
|
||||
want += 1;
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if want > 0 {
|
||||
result.push(QueueFamilyLayout {
|
||||
queue_family_index: idx as u32,
|
||||
priorities: std::iter::repeat_n(1.0, want).collect(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if requested_queues.is_empty() {
|
||||
log::debug!("Selected GPU queue families: {result:?}");
|
||||
Some(result)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RenderResult {
|
||||
pub queue: Arc<Queue>,
|
||||
pub cmd_buf: Arc<PrimaryAutoCommandBuffer>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GpuFutures {
|
||||
futures: Vec<Box<dyn GpuFuture>>,
|
||||
command_buffers: Vec<Arc<BuiltCommandBuffer>>,
|
||||
}
|
||||
|
||||
impl GpuFutures {
|
||||
pub fn execute(
|
||||
&mut self,
|
||||
queue: Arc<Queue>,
|
||||
cmd_buf: Arc<PrimaryAutoCommandBuffer>,
|
||||
) -> anyhow::Result<()> {
|
||||
self.futures.push(cmd_buf.execute(queue)?.boxed());
|
||||
Ok(())
|
||||
pub fn execute(&mut self, command_buffer: Arc<BuiltCommandBuffer>) {
|
||||
self.command_buffers.push(command_buffer);
|
||||
}
|
||||
|
||||
pub fn execute_results(&mut self, results: SmallVec<[RenderResult; 2]>) -> anyhow::Result<()> {
|
||||
for (i, res) in results.into_iter().enumerate() {
|
||||
if i == 0 {
|
||||
let future = res.cmd_buf.execute(res.queue)?;
|
||||
self.futures.push(Box::new(future));
|
||||
} else {
|
||||
let future = self.futures.pop().unwrap();
|
||||
let future = future.then_execute(res.queue, res.cmd_buf)?;
|
||||
self.futures.push(Box::new(future));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
pub fn execute_results(&mut self, results: SmallVec<[Arc<BuiltCommandBuffer>; 2]>) {
|
||||
self.command_buffers.extend(results);
|
||||
}
|
||||
|
||||
pub fn wait(self) -> anyhow::Result<()> {
|
||||
let mut it = self.futures.into_iter();
|
||||
let Some(mut all) = it.next() else {
|
||||
return Ok(());
|
||||
};
|
||||
for f in it {
|
||||
all = all.join(f).boxed();
|
||||
for command_buffer in self.command_buffers {
|
||||
command_buffer.submit_and_wait()?;
|
||||
}
|
||||
|
||||
let finished = all.then_signal_fence_and_flush()?;
|
||||
finished.wait(None)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtentExt {
|
||||
fn extent_f32(&self) -> [f32; 2];
|
||||
#[allow(dead_code)]
|
||||
fn extent_vec2(&self) -> Vec2;
|
||||
fn extent_u32arr(&self) -> [u32; 2];
|
||||
}
|
||||
|
||||
impl ExtentExt for Arc<ImageView> {
|
||||
fn extent_f32(&self) -> [f32; 2] {
|
||||
let [w, h, _] = self.image().extent();
|
||||
[w as _, h as _]
|
||||
}
|
||||
fn extent_vec2(&self) -> Vec2 {
|
||||
let [w, h, _] = self.image().extent();
|
||||
vec2(w as _, h as _)
|
||||
}
|
||||
fn extent_u32arr(&self) -> [u32; 2] {
|
||||
let [w, h, _] = self.image().extent();
|
||||
[w, h]
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtentExt for [u32; 3] {
|
||||
fn extent_f32(&self) -> [f32; 2] {
|
||||
let [w, h, _] = *self;
|
||||
[w as _, h as _]
|
||||
}
|
||||
fn extent_vec2(&self) -> Vec2 {
|
||||
let [w, h, _] = *self;
|
||||
Vec2 {
|
||||
x: w as _,
|
||||
y: h as _,
|
||||
}
|
||||
}
|
||||
fn extent_u32arr(&self) -> [u32; 2] {
|
||||
let [w, h, _] = *self;
|
||||
[w, h]
|
||||
}
|
||||
}
|
||||
@@ -244,7 +244,7 @@ pub(super) fn setup_custom_button<S: 'static>(
|
||||
template_name: template_name.clone(),
|
||||
template_params: template_params.clone(),
|
||||
},
|
||||
position: data.metadata.get_mouse_pos_absolute().unwrap(), //want panic
|
||||
position: data.metadata.get_mouse_pos_absolute().unwrap().into(), // want panic
|
||||
});
|
||||
Ok(EventResult::Consumed)
|
||||
}
|
||||
@@ -625,12 +625,12 @@ pub(super) fn setup_custom_button<S: 'static>(
|
||||
)
|
||||
.with_timeout(1.0)
|
||||
.with_lerp_amount(1.0)
|
||||
.with_animate(false)
|
||||
.with_sound(i == 0)
|
||||
.submit_at(app, now + Duration::from_secs(i as _));
|
||||
}
|
||||
|
||||
app.audio_sample_player
|
||||
.play_sample(&mut app.audio_system, "fix_floor");
|
||||
app.audio.play_sample("fix_floor");
|
||||
|
||||
let deadline = now + Duration::from_secs(duration_secs);
|
||||
|
||||
|
||||
Loaded 100 of 195 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user