diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml index 218adfe..aed02a9 100644 --- a/.github/workflows/artifacts.yml +++ b/.github/workflows/artifacts.yml @@ -23,12 +23,14 @@ jobs: sudo apt-get update -qq sudo apt-get install -y --no-install-recommends \ gcc-multilib libc6-dev-i386 python3-venv python3-dev \ - flatpak ostree patchelf + flatpak ostree - name: Get version id: get-version shell: bash - run: echo "VERSION_NAME=${GITHUB_REF_NAME//\//-}-${GITHUB_SHA::8}" >> $GITHUB_ENV + run: | + echo "VERSION_NAME=${GITHUB_REF_NAME//\//-}-${GITHUB_SHA::8}" >> $GITHUB_ENV + echo "VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)" >> $GITHUB_ENV - name: Build layer and gui id: build-all @@ -38,21 +40,14 @@ jobs: id: build-flatpak run: make flatpak - - name: Stage install tree - id: stage-install - run: make install DESTDIR="$PWD/staging/volt-gui-$VERSION_NAME" PREFIX=/usr - - - name: Collect flatpak bundles - id: collect-bundles - shell: bash - run: | - mkdir -p "staging/volt-gui-${VERSION_NAME}/bundles" - cp build/bundles/*.flatpak "staging/volt-gui-${VERSION_NAME}/bundles/" + - name: Build dist tree + id: build-dist + run: make dist - name: Upload artifacts id: upload-artifacts uses: actions/upload-artifact@v4 with: name: volt-gui-${{ env.VERSION_NAME }} - path: staging/volt-gui-${{ env.VERSION_NAME }} + path: build/dist/volt-gui-${{ env.VERSION }} if-no-files-found: error diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 32f537e..bf7e131 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - rust: ['1.77.0', 'stable'] + rust: ['1.85.1', 'stable'] steps: - name: Checkout code @@ -44,7 +44,7 @@ jobs: - name: Run checks id: run-checks - run: cd volt && cargo +${{ matrix.rust }} test --target x86_64-unknown-linux-gnu + run: cargo +${{ matrix.rust }} test --target x86_64-unknown-linux-gnu check-gui: runs-on: ubuntu-22.04 @@ -62,17 +62,17 @@ jobs: - name: Compile sources id: compile-sources - run: python -m compileall -q volt-gui + run: python -m compileall -q src/volt-gui - name: Import pure modules id: import-modules - run: cd volt-gui && python -c "import probe, profiles, presets, database" + run: cd src/volt-gui && python -c "import probe, profiles, presets, database" - name: Check version sync id: check-version shell: bash run: | - layer=$(sed -n 's/^version = "\(.*\)"/\1/p' volt/Cargo.toml | head -n1) - gui=$(cd volt-gui && python -c "import database; print(database.APP_VERSION)") + layer=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1) + gui=$(cd src/volt-gui && python -c "import database; print(database.APP_VERSION)") echo "layer=${layer} gui=${gui}" test "${layer}" = "${gui}" diff --git a/.gitignore b/.gitignore index 448cd03..f70bb52 100644 --- a/.gitignore +++ b/.gitignore @@ -10,13 +10,10 @@ build/ *.onefile-build/ # Rust -volt/target/ +target/ # Build outputs bin/ releases/ bundles/ .flatpak-work* -AppDir/ -*.AppImage -appimagetool-x86_64.AppImage diff --git a/volt/Cargo.lock b/Cargo.lock similarity index 60% rename from volt/Cargo.lock rename to Cargo.lock index fae331d..ac944f7 100644 --- a/volt/Cargo.lock +++ b/Cargo.lock @@ -1,12 +1,12 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ash" -version = "0.37.3+1.3.251" +version = "0.38.0+1.3.281" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" dependencies = [ "libloading", ] @@ -31,9 +31,9 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" dependencies = [ "equivalent", "hashbrown", @@ -47,20 +47,14 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libloading" -version = "0.7.4" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "winapi", + "windows-link", ] -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - [[package]] name = "proc-macro2" version = "1.0.107" @@ -79,15 +73,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", -] - [[package]] name = "serde_core" version = "1.0.229" @@ -110,18 +95,18 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "serde", + "serde_core", ] [[package]] name = "syn" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -130,44 +115,42 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ - "serde", + "indexmap", + "serde_core", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.11" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ - "serde", + "serde_core", ] [[package]] -name = "toml_edit" -version = "0.22.27" +name = "toml_parser" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", "winnow", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_writer" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "unicode-ident" @@ -177,7 +160,7 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "volt" -version = "2.0.0" +version = "2.4.2" dependencies = [ "ash", "libc", @@ -185,32 +168,13 @@ dependencies = [ ] [[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "winnow" -version = "0.7.15" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" diff --git a/volt/Cargo.toml b/Cargo.toml similarity index 53% rename from volt/Cargo.toml rename to Cargo.toml index 153f5dd..6ceacdb 100644 --- a/volt/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,26 @@ [package] name = "volt" -version = "2.0.0" +version = "2.4.2" edition = "2021" -rust-version = "1.77" +rust-version = "1.85.1" [lib] name = "volt" crate-type = ["cdylib", "rlib"] -path = "lib.rs" +path = "src/volt/lib.rs" [[bin]] name = "volt" -path = "main.rs" +path = "src/volt/volt.rs" + +[[bin]] +name = "volt-probe" +path = "src/volt-probe/volt-probe.rs" [dependencies] libc = "0.2" -toml = "0.8" -ash = "=0.37.3" +toml = "1.1.4" +ash = "=0.38.0" [profile.release] opt-level = 3 diff --git a/Makefile b/Makefile index f50eef6..1a6b2d5 100644 --- a/Makefile +++ b/Makefile @@ -5,14 +5,12 @@ datadir ?= $(PREFIX)/share OUT ?= build RELEASES ?= releases -GUI ?= pyinstaller CARGO ?= cargo RUSTUP ?= rustup PYTHON3 ?= python3 OSTREE ?= ostree FLATPAK ?= flatpak -WGET ?= wget TAR ?= tar CC32 ?= gcc @@ -43,9 +41,9 @@ STATE_DIR := /var/lib/volt MANIFEST := VkLayer_volt.json DESKTOP_FILE := volt-gui.desktop ICON_FILE := volt-gui.png -ICON_SOURCE := images/1.png +ICON_SOURCE := images/icon.png -VERSION := $(shell sed -n 's/^version = "\(.*\)"/\1/p' volt/Cargo.toml | head -n1) +VERSION := $(shell sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1) TRIPLE_64 := x86_64-unknown-linux-gnu TRIPLE_32 := i686-unknown-linux-gnu @@ -55,19 +53,24 @@ BUNDLE_DIR := $(OUT)/bundles SHARE_DIR := $(OUT)/share VENV := $(OUT)/py_env +DIST_DIR := $(OUT)/dist +DIST_NAME := volt-gui-$(VERSION) +DIST := $(DIST_DIR)/$(DIST_NAME) +DIST_STAMP := $(OUT)/.dist + CARGO_TARGET_DIR := $(abspath $(TARGET_DIR)) -APPIMAGE_EXTRACT_AND_RUN := 1 -export CARGO_TARGET_DIR APPIMAGE_EXTRACT_AND_RUN +export CARGO_TARGET_DIR LAYER_64 := $(TARGET_DIR)/$(TRIPLE_64)/release/libvolt.so LAYER_32 := $(TARGET_DIR)/$(TRIPLE_32)/release/libvolt.so LAUNCHER := $(TARGET_DIR)/$(TRIPLE_64)/release/volt -GUI_BIN := $(BIN_DIR)/volt-gui-$(GUI) +PROBE := $(TARGET_DIR)/$(TRIPLE_64)/release/volt-probe +GUI_BIN := $(BIN_DIR)/volt-gui DESKTOP := $(SHARE_DIR)/$(DESKTOP_FILE) -RUST_SOURCES := volt/Cargo.toml volt/Cargo.lock $(wildcard volt/*.rs) -GUI_SOURCES := $(wildcard volt-gui/*.py) -VENV_STAMP := $(VENV)/.requirements +RUST_SOURCES := Cargo.toml Cargo.lock $(wildcard src/volt/*.rs) $(wildcard src/volt-probe/*.rs) +GUI_SOURCES := $(wildcard src/volt-gui/*.py) +VENV_STAMP := $(OUT)/.venv DESKTOP_NAME := volt-gui DESKTOP_COMMENT := My AMD Adrenaline / NVIDIA Settings Linux Alternative @@ -77,38 +80,38 @@ DESKTOP_KEYWORDS := vulkan;vsync;gpu;gaming; FLATPAK_RUNTIMES := 23.08 24.08 25.08 FLATPAK_EXT_ID := org.freedesktop.Platform.VulkanLayer.volt FLATPAK_ARCH := x86_64 +LIBDIR_64_ARCH := x86_64-linux-gnu +LIBDIR_32_ARCH := i386-linux-gnu FLATPAK_BUNDLES := $(foreach rt,$(FLATPAK_RUNTIMES),\ $(BUNDLE_DIR)/$(FLATPAK_EXT_ID)-$(rt).flatpak) -APPIMAGE_TOOL := $(OUT)/appimagetool-x86_64.AppImage -APPIMAGE_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage - -RELEASE_FILES := \ - $(RELEASES)/volt-gui-$(VERSION)-pyinstaller-x86_64.AppImage \ - $(RELEASES)/volt-gui-$(VERSION)-nuitka-x86_64.AppImage \ - $(RELEASES)/volt-gui-$(VERSION)-pyinstaller.tar.gz \ - $(RELEASES)/volt-gui-$(VERSION)-nuitka.tar.gz +RELEASE_FILES := $(RELEASES)/volt-gui-$(VERSION).tar.gz CONTAINER ?= $(shell command -v podman 2>/dev/null || command -v docker 2>/dev/null || echo podman) -CONTAINER_BASE ?= rust:1.82-bookworm +CONTAINER_BASE ?= rust:1.85.1-bookworm CONTAINER_IMAGE ?= volt-gui-build CONTAINER_OUT ?= $(OUT)/container CONTAINER_STAMP := $(OUT)/.container-image NO_SUDO = @test -z "$$SUDO_USER" || { echo "error: do not build with sudo — run 'make' as your user, then 'sudo make install'"; exit 1; } -PACK = $(OUT)/pack.$*/volt-gui-$(VERSION)-$* +DIST_TREES := Cargo.toml Cargo.lock src images flatpak container .github ifeq ($(DESTDIR),) ROOT_GUARD := check-root +LAYER_GUARD := check-no-user-layer LIVE_SYSTEM := 1 else ROOT_GUARD := +LAYER_GUARD := LIVE_SYSTEM := endif +USER_MANIFEST_HOME = $${SUDO_USER:+$$(getent passwd "$$SUDO_USER" | cut -d: -f6)} + INSTALL_FILES := \ $(DESTDIR)$(bindir)/volt \ + $(DESTDIR)$(bindir)/volt-probe \ $(DESTDIR)$(bindir)/volt-gui \ $(DESTDIR)$(LIBDIR_64)/libvolt.so \ $(DESTDIR)$(LIBDIR_32)/libvolt.so \ @@ -116,53 +119,71 @@ INSTALL_FILES := \ $(DESTDIR)$(DESKTOP_DIR)/$(DESKTOP_FILE) \ $(DESTDIR)$(ICON_DIR)/$(ICON_FILE) -.PHONY: all layer-64 layer-32 gui-pyinstaller gui-nuitka desktop flatpak \ +USER_PREFIX ?= $(HOME)/.local +USER_BIN := $(USER_PREFIX)/bin +USER_LIB := $(USER_PREFIX)/lib/volt +USER_DATA := $(USER_PREFIX)/share +USER_LAYER_DIR := $(USER_DATA)/vulkan/implicit_layer.d +USER_DESK_DIR := $(USER_DATA)/applications +USER_ICON_DIR := $(USER_DATA)/icons/hicolor/256x256/apps +USER_STATE := $(USER_DATA)/volt + +USER_FILES := \ + $(USER_BIN)/volt \ + $(USER_BIN)/volt-probe \ + $(USER_BIN)/volt-gui \ + $(USER_LIB)/$(LIBDIR_64_ARCH)/libvolt.so \ + $(USER_LIB)/$(LIBDIR_32_ARCH)/libvolt.so \ + $(USER_LAYER_DIR)/$(MANIFEST) \ + $(USER_DESK_DIR)/$(DESKTOP_FILE) \ + $(USER_ICON_DIR)/$(ICON_FILE) + +BUILT_ARTIFACTS := $(LAYER_64) $(LAYER_32) $(LAUNCHER) $(PROBE) $(GUI_BIN) $(DESKTOP) + +.DELETE_ON_ERROR: + +.PHONY: all layer-64 layer-32 gui desktop flatpak dist \ release release-container container-image install flatpak-install \ - uninstall clean help check-root check-sudo-user + install-user flatpak-install-user setup-user uninstall-user \ + uninstall clean help check-root check-sudo-user check-not-root \ + check-no-user-layer check-no-system-layer check-built check-bundles -all: $(LAYER_64) $(LAYER_32) $(LAUNCHER) $(GUI_BIN) $(DESKTOP) +all: $(LAYER_64) $(LAYER_32) $(LAUNCHER) $(PROBE) $(GUI_BIN) $(DESKTOP) -layer-64: $(LAYER_64) $(LAUNCHER) +layer-64: $(LAYER_64) $(LAUNCHER) $(PROBE) layer-32: $(LAYER_32) -gui-pyinstaller: $(BIN_DIR)/volt-gui-pyinstaller -gui-nuitka: $(BIN_DIR)/volt-gui-nuitka +gui: $(GUI_BIN) desktop: $(DESKTOP) flatpak: $(FLATPAK_BUNDLES) +dist: $(DIST_STAMP) release: $(RELEASE_FILES) container-image: $(CONTAINER_STAMP) -$(OUT) $(BIN_DIR) $(BUNDLE_DIR) $(SHARE_DIR) $(RELEASES) $(OUT)/pyinstaller $(OUT)/nuitka: +$(OUT) $(BIN_DIR) $(BUNDLE_DIR) $(SHARE_DIR) $(RELEASES) $(OUT)/pyinstaller: @mkdir -p $@ -$(LAYER_64) $(LAUNCHER) &: $(RUST_SOURCES) +$(LAYER_64) $(LAUNCHER) $(PROBE) &: $(RUST_SOURCES) $(NO_SUDO) - cd volt && $(CARGO) build --release --target $(TRIPLE_64) + $(CARGO) build --release --target $(TRIPLE_64) $(LAYER_32): $(RUST_SOURCES) $(NO_SUDO) -@$(RUSTUP) target add $(TRIPLE_32) - cd volt && CARGO_TARGET_I686_UNKNOWN_LINUX_GNU_LINKER=$(CC32) \ - $(CARGO) build --release --target $(TRIPLE_32) + CARGO_TARGET_I686_UNKNOWN_LINUX_GNU_LINKER=$(CC32) \ + $(CARGO) build --release --lib --target $(TRIPLE_32) -$(VENV)/bin/python: | $(OUT) +$(VENV_STAMP): requirements.txt | $(OUT) + $(NO_SUDO) $(PYTHON3) -m venv $(VENV) - -$(VENV_STAMP): requirements.txt $(VENV)/bin/python $(VENV)/bin/pip install --upgrade pip -q $(VENV)/bin/pip install --no-cache-dir -r requirements.txt -q @touch $@ -$(BIN_DIR)/volt-gui-pyinstaller: $(GUI_SOURCES) $(VENV_STAMP) | $(BIN_DIR) $(OUT)/pyinstaller +$(GUI_BIN): $(GUI_SOURCES) $(VENV_STAMP) | $(BIN_DIR) $(OUT)/pyinstaller $(NO_SUDO) $(VENV)/bin/pyinstaller --onefile --name=$(@F) -y --log-level WARN \ --distpath $(BIN_DIR) --workpath $(OUT)/pyinstaller --specpath $(OUT)/pyinstaller \ - volt-gui/volt-gui.py - -$(BIN_DIR)/volt-gui-nuitka: $(GUI_SOURCES) $(VENV_STAMP) | $(BIN_DIR) $(OUT)/nuitka - $(NO_SUDO) - $(VENV)/bin/nuitka --onefile --assume-yes-for-downloads --enable-plugin=pyside6 \ - --output-dir=$(OUT)/nuitka --output-filename=$(@F) volt-gui/volt-gui.py - @cp $(OUT)/nuitka/$(@F) $@ + src/volt-gui/volt-gui.py $(DESKTOP): Makefile | $(SHARE_DIR) @printf '%s\n' \ @@ -179,61 +200,47 @@ $(DESKTOP): Makefile | $(SHARE_DIR) 'StartupNotify=true' \ 'StartupWMClass=volt-gui' > $@ -$(BUNDLE_DIR)/$(FLATPAK_EXT_ID)-%.flatpak: $(LAYER_64) $(LAYER_32) $(MANIFEST) LICENSE \ - flatpak/volt-flatpak flatpak/commit.py | $(BUNDLE_DIR) +$(BUNDLE_DIR)/$(FLATPAK_EXT_ID)-%.flatpak: $(LAYER_64) $(LAYER_32) $(LAUNCHER) \ + $(MANIFEST) LICENSE flatpak/volt-flatpak flatpak/commit.py | $(BUNDLE_DIR) rm -rf $(OUT)/flatpak.$* install -Dm755 $(LAYER_64) $(OUT)/flatpak.$*/stage/files/lib/x86_64-linux-gnu/libvolt.so install -Dm755 $(LAYER_32) $(OUT)/flatpak.$*/stage/files/lib/i386-linux-gnu/libvolt.so install -Dm644 $(MANIFEST) $(OUT)/flatpak.$*/stage/files/share/vulkan/implicit_layer.d/$(MANIFEST) install -Dm644 LICENSE $(OUT)/flatpak.$*/stage/files/share/doc/volt/LICENSE install -Dm755 flatpak/volt-flatpak $(OUT)/flatpak.$*/stage/files/bin/volt-flatpak + install -Dm755 $(LAUNCHER) $(OUT)/flatpak.$*/stage/files/bin/volt $(OSTREE) init --repo=$(OUT)/flatpak.$*/repo --mode=archive-z2 $(PYTHON3) flatpak/commit.py "$*" "$(OUT)/flatpak.$*/repo" "$(OUT)/flatpak.$*/stage" $(FLATPAK) build-bundle --arch=$(FLATPAK_ARCH) $(OUT)/flatpak.$*/repo $@ \ $(FLATPAK_EXT_ID) "$*" --runtime rm -rf $(OUT)/flatpak.$* -$(APPIMAGE_TOOL): | $(OUT) - $(WGET) -q -O $@ $(APPIMAGE_URL) - chmod +x $@ - -$(RELEASES)/volt-gui-$(VERSION)-%-x86_64.AppImage: $(BIN_DIR)/volt-gui-% \ - $(DESKTOP) $(APPIMAGE_TOOL) $(ICON_SOURCE) | $(RELEASES) - rm -rf $(OUT)/AppDir.$* - install -Dm755 $< $(OUT)/AppDir.$*/volt-gui - install -Dm644 $(DESKTOP) $(OUT)/AppDir.$*/$(DESKTOP_FILE) - install -Dm644 $(ICON_SOURCE) $(OUT)/AppDir.$*/$(ICON_FILE) - install -Dm644 $(ICON_SOURCE) \ - $(OUT)/AppDir.$*/usr/share/icons/hicolor/256x256/apps/$(ICON_FILE) - @printf '%s\n' '#!/bin/sh' \ - 'HERE="$$(dirname "$$(readlink -f "$$0")")"' \ - 'export APPDIR="$$HERE"' \ - 'cd "$$HOME" 2>/dev/null || cd /tmp' \ - 'exec "$$HERE/volt-gui" "$$@"' > $(OUT)/AppDir.$*/AppRun - chmod +x $(OUT)/AppDir.$*/AppRun - $(APPIMAGE_TOOL) $(OUT)/AppDir.$* $@ - rm -rf $(OUT)/AppDir.$* - -$(RELEASES)/volt-gui-$(VERSION)-%.tar.gz: $(BIN_DIR)/volt-gui-% \ - $(LAYER_64) $(LAYER_32) $(LAUNCHER) $(DESKTOP) $(FLATPAK_BUNDLES) \ - $(MANIFEST) Makefile LICENSE README.md requirements.txt | $(RELEASES) - rm -rf $(OUT)/pack.$* - install -Dm755 $< $(PACK)/build/bin/volt-gui-$* - install -Dm755 $(LAYER_64) $(PACK)/build/target/$(TRIPLE_64)/release/libvolt.so - install -Dm755 $(LAUNCHER) $(PACK)/build/target/$(TRIPLE_64)/release/volt - install -Dm755 $(LAYER_32) $(PACK)/build/target/$(TRIPLE_32)/release/libvolt.so - install -Dm644 $(DESKTOP) $(PACK)/build/share/$(DESKTOP_FILE) - install -Dm644 $(MANIFEST) $(PACK)/$(MANIFEST) - install -Dm644 Makefile $(PACK)/Makefile - install -Dm644 LICENSE $(PACK)/LICENSE - install -Dm644 README.md $(PACK)/README.md - install -Dm644 requirements.txt $(PACK)/requirements.txt - cp -r volt volt-gui images flatpak $(PACK)/ - mkdir -p $(PACK)/build/bundles - cp $(FLATPAK_BUNDLES) $(PACK)/build/bundles/ - touch $(PACK)/build/bin/* $(PACK)/build/share/* $(PACK)/build/target/*/release/* - $(TAR) -czf $@ -C $(OUT)/pack.$* volt-gui-$(VERSION)-$* - rm -rf $(OUT)/pack.$* +$(DIST_STAMP): $(GUI_BIN) \ + $(LAYER_64) $(LAYER_32) $(LAUNCHER) $(PROBE) $(DESKTOP) $(FLATPAK_BUNDLES) \ + $(MANIFEST) Makefile Cargo.toml Cargo.lock LICENSE README.md requirements.txt | $(OUT) + rm -rf $(DIST) + install -Dm755 $(GUI_BIN) $(DIST)/build/bin/volt-gui + install -Dm755 $(LAYER_64) $(DIST)/build/target/$(TRIPLE_64)/release/libvolt.so + install -Dm755 $(LAUNCHER) $(DIST)/build/target/$(TRIPLE_64)/release/volt + install -Dm755 $(PROBE) $(DIST)/build/target/$(TRIPLE_64)/release/volt-probe + install -Dm755 $(LAYER_32) $(DIST)/build/target/$(TRIPLE_32)/release/libvolt.so + install -Dm644 $(DESKTOP) $(DIST)/build/share/$(DESKTOP_FILE) + install -Dm644 $(MANIFEST) $(DIST)/$(MANIFEST) + install -Dm644 Makefile $(DIST)/Makefile + install -Dm644 LICENSE $(DIST)/LICENSE + install -Dm644 README.md $(DIST)/README.md + install -Dm644 requirements.txt $(DIST)/requirements.txt + cp -r $(DIST_TREES) $(DIST)/ + rm -rf $(DIST)/src/volt/target + mkdir -p $(DIST)/build/bundles + cp $(FLATPAK_BUNDLES) $(DIST)/build/bundles/ + touch $(DIST)/build/.venv + touch $(DIST)/build/bin/* $(DIST)/build/share/* \ + $(DIST)/build/bundles/* $(DIST)/build/target/*/release/* + @touch $@ + +$(RELEASES)/volt-gui-$(VERSION).tar.gz: $(DIST_STAMP) | $(RELEASES) + $(TAR) -czf $@ -C $(DIST_DIR) $(DIST_NAME) $(CONTAINER_STAMP): container/Containerfile | $(OUT) $(CONTAINER) build --build-arg BASE=$(CONTAINER_BASE) -t $(CONTAINER_IMAGE) -f $< container @@ -244,10 +251,17 @@ release-container: $(CONTAINER_STAMP) --user "$$(id -u):$$(id -g)" \ -e HOME=/tmp \ -e CARGO_HOME=/src/$(CONTAINER_OUT)/cargo \ - -e NUITKA_CACHE_DIR=/src/$(CONTAINER_OUT)/nuitka-cache \ $(CONTAINER_IMAGE) make release OUT=$(CONTAINER_OUT) -install: $(INSTALL_FILES) +install: | $(ROOT_GUARD) check-built $(LAYER_GUARD) + install -Dm755 $(LAUNCHER) $(DESTDIR)$(bindir)/volt + install -Dm755 $(PROBE) $(DESTDIR)$(bindir)/volt-probe + install -Dm755 $(GUI_BIN) $(DESTDIR)$(bindir)/volt-gui + install -Dm755 $(LAYER_64) $(DESTDIR)$(LIBDIR_64)/libvolt.so + install -Dm755 $(LAYER_32) $(DESTDIR)$(LIBDIR_32)/libvolt.so + install -Dm644 $(MANIFEST) $(DESTDIR)$(VK_LAYER_DIR)/$(MANIFEST) + install -Dm644 $(DESKTOP) $(DESTDIR)$(DESKTOP_DIR)/$(DESKTOP_FILE) + install -Dm644 $(ICON_SOURCE) $(DESTDIR)$(ICON_DIR)/$(ICON_FILE) @test -z "$(LIVE_SYSTEM)" || ldconfig 2>/dev/null || true @test -z "$(LIVE_SYSTEM)" || update-desktop-database $(DESKTOP_DIR) 2>/dev/null || true @test -z "$(LIVE_SYSTEM)" || gtk-update-icon-cache -qtf $(datadir)/icons/hicolor 2>/dev/null || true @@ -259,23 +273,50 @@ install: $(INSTALL_FILES) @echo " launcher: $(DESKTOP_DIR)/$(DESKTOP_FILE)" @echo " icon: $(ICON_DIR)/$(ICON_FILE)" -$(DESTDIR)$(bindir)/volt: $(LAUNCHER) | $(ROOT_GUARD) ; install -Dm755 $< $@ -$(DESTDIR)$(bindir)/volt-gui: $(GUI_BIN) | $(ROOT_GUARD) ; install -Dm755 $< $@ -$(DESTDIR)$(LIBDIR_64)/libvolt.so: $(LAYER_64) | $(ROOT_GUARD) ; install -Dm755 $< $@ -$(DESTDIR)$(LIBDIR_32)/libvolt.so: $(LAYER_32) | $(ROOT_GUARD) ; install -Dm755 $< $@ -$(DESTDIR)$(VK_LAYER_DIR)/$(MANIFEST): $(MANIFEST) | $(ROOT_GUARD) ; install -Dm644 $< $@ -$(DESTDIR)$(DESKTOP_DIR)/$(DESKTOP_FILE): $(DESKTOP) | $(ROOT_GUARD) ; install -Dm644 $< $@ -$(DESTDIR)$(ICON_DIR)/$(ICON_FILE): $(ICON_SOURCE) | $(ROOT_GUARD) ; install -Dm644 $< $@ - -INSTALLED_STAMPS := $(FLATPAK_BUNDLES:$(BUNDLE_DIR)/%.flatpak=$(STATE_DIR)/.installed-%) - -flatpak-install: $(INSTALLED_STAMPS) +flatpak-install: | check-root check-sudo-user check-bundles + @for b in $(FLATPAK_BUNDLES); do \ + case "$$b" in /*) p="$$b";; *) p="$(CURDIR)/$$b";; esac; \ + s="$(STATE_DIR)/.installed-$$(basename "$$b" .flatpak)"; \ + if [ ! -e "$$s" ] || [ "$$b" -nt "$$s" ]; then \ + su - "$$SUDO_USER" -c "$(FLATPAK) install --user -y --reinstall '$$p'" || exit 1; \ + mkdir -p "$(STATE_DIR)" && touch "$$s"; \ + fi; done @echo "flatpak extensions installed." -$(STATE_DIR)/.installed-%: $(BUNDLE_DIR)/%.flatpak | check-root check-sudo-user - @mkdir -p $(@D) - su - "$$SUDO_USER" -c "$(FLATPAK) install --user -y --reinstall '$(abspath $<)'" - @touch $@ +install-user: | check-not-root check-built check-no-system-layer + install -Dm755 $(LAUNCHER) $(USER_BIN)/volt + install -Dm755 $(PROBE) $(USER_BIN)/volt-probe + install -Dm755 $(GUI_BIN) $(USER_BIN)/volt-gui + install -Dm755 $(LAYER_64) $(USER_LIB)/$(LIBDIR_64_ARCH)/libvolt.so + install -Dm755 $(LAYER_32) $(USER_LIB)/$(LIBDIR_32_ARCH)/libvolt.so + install -Dm644 $(MANIFEST) $(USER_LAYER_DIR)/$(MANIFEST) + install -Dm644 $(DESKTOP) $(USER_DESK_DIR)/$(DESKTOP_FILE) + install -Dm644 $(ICON_SOURCE) $(USER_ICON_DIR)/$(ICON_FILE) + @echo "user install complete, no root used." + @echo " bin: $(USER_BIN)" + @echo " lib: $(USER_LIB)" + @echo " manifest: $(USER_LAYER_DIR)" + @echo "$(USER_BIN) must be on PATH: volt-gui runs volt to read your hardware." + +flatpak-install-user: | check-not-root check-bundles + @for b in $(FLATPAK_BUNDLES); do \ + case "$$b" in /*) p="$$b";; *) p="$(CURDIR)/$$b";; esac; \ + s="$(USER_STATE)/.installed-$$(basename "$$b" .flatpak)"; \ + if [ ! -e "$$s" ] || [ "$$b" -nt "$$s" ]; then \ + $(FLATPAK) install --user -y --reinstall "$$p" || exit 1; \ + mkdir -p "$(USER_STATE)" && touch "$$s"; \ + fi; done + @echo "flatpak extensions installed for this user." + +setup-user: install-user flatpak-install-user + @echo "ready: launch volt-gui from your menu, or run $(USER_BIN)/volt-gui." + +uninstall-user: | check-not-root + rm -f $(USER_FILES) + rm -rf $(USER_STATE) + $(FLATPAK) uninstall --user -y $(FLATPAK_EXT_ID) 2>/dev/null || true + rm -rf "$(HOME)/.config/volt-gui" + @echo "user uninstall complete." uninstall: | $(ROOT_GUARD) rm -f $(INSTALL_FILES) @@ -290,7 +331,7 @@ uninstall: | $(ROOT_GUARD) @echo "uninstall complete." clean: - rm -rf $(OUT) $(RELEASES) bin bundles py_env volt/target + rm -rf $(OUT) $(RELEASES) bin bundles py_env target check-root: @test "$$(id -u)" -eq 0 || { echo "error: needs root — run: sudo make $(MAKECMDGOALS)"; exit 1; } @@ -298,17 +339,58 @@ check-root: check-sudo-user: @test -n "$$SUDO_USER" || { echo "error: SUDO_USER not set — run via 'sudo make ...' from your user shell"; exit 1; } +check-not-root: + @test "$$(id -u)" -ne 0 || { echo "error: this installs into \$$HOME — run as your user, no sudo"; exit 1; } + +check-built: + @missing=; for f in $(BUILT_ARTIFACTS); do \ + test -e "$$f" || missing="$$missing $$f"; done; \ + test -z "$$missing" || { \ + echo "error: build artifacts missing:"; \ + for f in $$missing; do echo " $$f"; done; \ + echo "run 'make' as your user first, then re-run this target"; \ + exit 1; } + +check-bundles: + @missing=; for b in $(FLATPAK_BUNDLES); do \ + test -e "$$b" || missing="$$missing $$b"; done; \ + test -z "$$missing" || { \ + echo "error: flatpak bundles missing:"; \ + for b in $$missing; do echo " $$b"; done; \ + echo "run 'make flatpak' as your user first"; \ + exit 1; } + +check-no-user-layer: + @home="$(USER_MANIFEST_HOME)"; \ + test -z "$$home" -o ! -e "$$home/.local/share/vulkan/implicit_layer.d/$(MANIFEST)" || { \ + echo "error: a user install already owns the layer at"; \ + echo " $$home/.local/share/vulkan/implicit_layer.d/$(MANIFEST)"; \ + echo "two manifests naming the same layer is undefined: run 'make uninstall-user' first"; \ + exit 1; } + +check-no-system-layer: + @test ! -e "$(VK_LAYER_DIR)/$(MANIFEST)" || { \ + echo "error: a system install already owns the layer at"; \ + echo " $(VK_LAYER_DIR)/$(MANIFEST)"; \ + echo "two manifests naming the same layer is undefined: run 'sudo make uninstall' first"; \ + exit 1; } + help: - @echo "make layer (64 + 32), launcher, gui ($(GUI)), desktop entry" - @echo "make layer-64 64-bit layer and the volt launcher" + @echo "make layer (64 + 32), launcher, gui, desktop entry" + @echo "make layer-64 64-bit layer, the volt launcher and volt-probe" @echo "make layer-32 32-bit layer" - @echo "make gui-pyinstaller gui binary via PyInstaller" - @echo "make gui-nuitka gui binary via Nuitka" + @echo "make gui gui binary via PyInstaller" @echo "make desktop desktop entry only" @echo "make flatpak flatpak runtime extension bundles" + @echo "make dist source + build tree in $(DIST_DIR)/" @echo "make release full release into $(RELEASES)/ (host toolchain)" @echo "make release-container the same, built inside $(CONTAINER_BASE)" - @echo "sudo make install install (GUI=$(GUI))" + @echo "install targets never build: run 'make' (and 'make flatpak') as your user first" + @echo "sudo make install" @echo "sudo make flatpak-install" @echo "sudo make uninstall" + @echo "make setup-user install-user + flatpak-install-user, no root" + @echo "make install-user layer, launcher and gui into ~/.local" + @echo "make flatpak-install-user the extensions, flatpak --user" + @echo "make uninstall-user" @echo "make clean" diff --git a/README.md b/README.md index ec63ac9..2295ee6 100644 --- a/README.md +++ b/README.md @@ -1,345 +1,512 @@ > [!NOTE] > Bug reports and pull requests are welcome, but please understand that development happens in my free time and progress may be slow at times. The project is still maintained even if the last commit was made a while ago. +> +> Also English isn't my first language. I mainly speak Spanish (and some Portuguese), so I usually write the docs in Spanish, run them through a translator, and then edit the result. Some parts may sound a bit stiff or unnatural because of that. If anything is unclear, feel free to open an issue and I'll fix it. # volt-gui -> **My AMD Adrenaline / NVIDIA Settings Linux Alternative** +Control panel for Vulkan games on Linux. Settings are applied by **volt**, an implicit Vulkan layer written in Rust, so they work on every driver: RADV, ANV, NVK, AMDVLK, NVIDIA proprietary. -volt-gui is a graphical control panel for Vulkan games on Linux. Settings are -applied by **volt**, a Vulkan implicit layer written in Rust, so they work on -every Vulkan driver: RADV, ANV, NVK, AMDVLK, the NVIDIA proprietary driver. +Vulkan 1.0+. The layer requests nothing beyond VK_KHR_swapchain, so behaviour never splits between drivers. -The floor is **Vulkan 1.0**, and so is the ceiling: the layer requests no -extension beyond `VK_KHR_swapchain`, so every setting behaves the same on -every conformant driver. `VOLT_LOG=info` shows what was applied. +volt-gui originally carried kernel settings too. To keep both projects easier to ship and maintain, and each with one objective, they were split: volt stays a Vulkan control panel, and the kernel side now lives in [ohm-gui](https://github.com/pythonlover02/ohm-gui). ![](/images/1.png) ![](/images/2.png) ![](/images/3.png) +## Quick Start + +``` +git clone https://github.com/pythonlover02/volt-gui.git +cd volt-gui +make +make install-user + +volt-gui # set what you want, press Apply +volt -- ./game +``` + +That covers native, Steam, Wine and Proton. For a system-wide install use `sudo make install` instead. Pick one, never both. + +Steam launch options: + +``` +volt -- %command% +``` + +Flatpak games need some extra work, see [Flatpak](#flatpak). + ## Table of Contents -- [What you can do?](#what-you-can-do) +- [Settings](#settings) - [How It Works](#how-it-works) - [Requirements](#requirements) - [Installation](#installation) +- [Install paths](#install-paths) +- [Uninstalling](#uninstalling) +- [Immutable Systems](#immutable-systems) +- [FEX-Emu / Box64](#fex-emu--box64) - [Building Releases](#building-releases) - [Usage](#usage) +- [Environment Variables](#environment-variables) +- [Files](#files) - [Flatpak](#flatpak) -- [Profiles & Presets](#profiles--presets) +- [Profiles, Presets & Options](#profiles-presets--options) - [What volt will never do](#what-volt-will-never-do) - [Contributing](#contributing) -## What you can do? +## Settings + +20 settings across 5 tabs. Every one defaults to `default`, which leaves the game's own choice alone. A profile with everything on default does nothing. -Every setting defaults to **default**, meaning the layer does not touch that -value and the game keeps its own choice. A profile with everything on default does nothing. +Each setting is a single value. No ranges, no ordering, nothing to get backwards. -Every setting is one value: the value volt forces, or `default`. There is no -range, no ordering, and nothing to get backwards. +| Tab | Section | Count | Covers | +|-----|---------|------:|--------| +| GPU | `[gpu]` | 1 | which device the game sees | +| Display | `[display]` | 4 | present mode, image count, compositing, clipping | +| Textures | `[textures]` | 7 | filtering, mips, anisotropy, LOD | +| Rendering | `[rendering]` | 4 | sample shading, alpha to coverage, alpha to one, depth clamp | +| Framerate | `[framerate]` | 4 | limit, cadence, method, pacing | -The values each setting offers come from your own hardware, not from a list -built into volt-gui. Present modes, colour depths, colour spaces, transfer -functions and alpha modes come from what the surface reports, the GPU list -from what the driver enumerates, and mip levels and LOD bias run up to the -limits the device gives. A setting whose feature the device lacks holds -nothing but `default`. Only the three **Framerate** settings have a fixed -list, since they are volt's own. +Most option lists are read from your hardware, not from a table in volt-gui. Present modes, image counts, alpha modes, GPU names, anisotropy, mip levels and LOD bias all come from a probe of your own device. A setting your hardware lacks holds only `default`. -Settings are read once, when the game starts, and never change while it -runs. Press Apply, then start the game again. +Fixed lists exist where there is nothing to read. `nearest` and `linear` are core Vulkan with no query behind them. The Framerate settings have nothing to read either, since a game never tells Vulkan what frame rate it wants. -volt-gui learns this by keeping a small `vkgears` window running under the -profile you are editing, which also serves as a live preview. Close it and -volt-gui carries on with what it learned last time. +Settings are read once at game start. Press Apply, then restart the game. + +### The probe + +volt-gui runs `volt-probe` under the profile you are editing. For each backend it can reach it opens a 1px window that is never mapped and creates a surface and swapchain; it creates one sampler, records what the device reported, and exits. Nothing appears on screen. ``` -volt --probe myprofile -- vkgears +volt --probe myprofile -- volt-probe ``` -A value volt has no name for still appears in the list, still saves to a -profile, and still applies. Where the specification admits only what a query -returned, a value the device did not report is not forced: volt keeps the -game's own value and logs a warning. +It exits 0 on success, 1 where the instance or the device could not be created, and 2 where the device lists the portability subset and the instance support that extension depends on is missing. The layer records device facts at enumeration, so the device-backed cards fill either way. -Where the specification bounds a value — LOD bias against the device limit, -image count against what the surface allows — volt clamps what it passes -down. That clamp is correctness, not a setting. +It opens X11 and Wayland, each through a library loaded at runtime, so a machine missing one reports the other and a machine missing both leaves those settings on `default`. This only affects present modes, image counts and alpha modes. The file carries one section per backend that opened, and the card offers the union with every value naming the backends that reported it: `mailbox (xcb, wayland)`, `immediate (xcb)`. The tag is a label only, profiles store the value. Games may open gamescope or a Flatpak surface instead, and where the game's surface refuses a value the layer handles it at runtime: image count is clamped against the real surface, and a rejected present or alpha mode leaves the game's value with a warning. ### GPU -- **Physical Device**: pick which GPU the game sees, listed by name. The - layer hides the rest during device enumeration, so it works on every Vulkan - driver. If nothing matches, the full list comes back and a warning is - logged. + +**Physical Device** pick which GPU the game sees. volt hides the rest during enumeration. If nothing matches, the full list comes back with a warning. + +This is the only setting volt cannot force. Nothing in Vulkan names the device a swapchain runs on, so a game that ignores enumeration order keeps what it picked. ### Display -- **VSync / Present Mode**: whatever the surface supports. `immediate` turns - vsync off, `mailbox` is low latency vsync, `fifo` is classic vsync, - `fifo_relaxed` tears only below refresh. Every other mode is hidden from - the list the game is shown, so a game's own vsync menu cannot offer one you - ruled out. That filtering is what makes the setting hold wherever the game - asks Vulkan what it may use: a swapchain may only switch between modes the - surface reported, and a present may only name one the swapchain was built - with. A mode the surface does not support falls back to the game's own - choice with a warning. - - A game that enables `VK_KHR_swapchain_maintenance1` can switch present mode - without rebuilding the swapchain. It can only switch among the modes the - surface offered it, which volt has already filtered, so the setting still - holds. -- **Swapchain Images**: how many images the swapchain holds. Fewer images - lower display latency, more images smooth frame delivery. The list is what - the surface allows, and the choice is reported back to the game, so a game - that derives its count from the surface honours it on its own. -- **Color Depth**: the bits per colour channel this surface offers, usually - 8-bit and 10-bit. The layer hides the formats you did not pick, so a game - that takes the first supported format ends up with yours. If nothing - matches, the full list comes back and a warning is logged. -- **Color Space**: filtered out of the same list. Everything past - `srgb_nonlinear` needs the stack around the game to have enabled it, through - DXVK_HDR, PROTON_ENABLE_HDR or gamescope, so on most setups this card holds - one entry. -- **Transfer Function**: whether the game is shown `srgb` formats, plain - `unorm` ones, or float ones, filtered out of the same list again. Getting it - wrong looks washed out or crushed rather than broken, so set it back to - default if the image looks off. No preset touches it. -- **Composite Alpha**: how the compositor treats the finished image's alpha. - Forcing `opaque` skips compositor blending on Wayland. A value the surface - does not report falls back to the game's own choice with a warning. -- **Clipped Presentation**: whether the driver may skip pixels another window - covers. On or off. + +**VSync / Present Mode** `immediate` off, `mailbox` low-latency vsync, `fifo` classic vsync, `fifo_relaxed` tears below refresh. Only these four are offered or forced; the ones you ruled out are hidden from the game's own vsync menu, and a mode an extension defines is left where the driver put it. + +**Swapchain Images** frames in flight. More lets the game run ahead of the GPU, smoothing delivery at the cost of input lag. Fewer holds it closer to the display. This is the anti-lag setting. + +**Composite Alpha** how the compositor treats the finished image's alpha. `opaque` skips compositor blending on Wayland. + +**Clipped Presentation** whether the driver may skip pixels another window covers. ### Textures -- **Texture Filtering**: retro (sharp pixels), bilinear, trilinear. A sampler - that matches none of the three exactly counts as the closest one below it. -- **Mipmap Mode**: a hard cut between mip levels, or a blend across them, - independently of the filter choice. -- **Anisotropic Filtering**: off up to whatever your GPU reports. Needs the - `samplerAnisotropy` feature; volt never enables one, so where a game left it - off this card holds nothing but `default`. Nearly every game enables it. -- **LOD Bias**: shift mipmap selection, sharper or blurrier, across the range - the device allows. -- **Mip Floor** and **Mip Ceiling**: the lowest and highest mip levels - samplers may use, called minimum and maximum LOD in Vulkan. + +**Magnification Filter** sampling where a texture is drawn larger than its own size, so anything close to the camera. `nearest` is sharp pixels, `linear` smooths. The one filter a screenshot shows you. + +**Minification Filter** the same where it's drawn smaller, which is most of the screen. `nearest` shimmers as the camera moves, `linear` settles. Leave on `linear` unless you want the crawl. + +**Mipmap Mode** hard cut between mip levels, or a blend. + +An unnormalized, subsampled, image-processing or converted sampler keeps every field its shape restricts, so a setting naming one of those fields leaves it alone and logs a line. + +Three sampler fields, three settings. `nearest` and `linear` are core with no query behind them, so `linear` is forced only where the sampler's own magnification filter, minification filter or mipmap mode already uses linear; otherwise that setting leaves the sampler alone and logs a line. Retro is `nearest`/`nearest`/`nearest`. Bilinear is `linear`/`linear`/`nearest`. Trilinear is `linear`/`linear`/`linear`. + +**Anisotropic Filtering** `off` up to whatever your GPU reports. volt never enables `samplerAnisotropy`; where the game left it off the setting is ignored and logged. Nearly every game enables it. + +**LOD Bias** shift mipmap selection sharper or blurrier. + +**Mip Floor / Mip Ceiling** lowest and highest mip levels samplers may use. A forced bound that would cross the value the other field holds is dropped rather than swapped, and logs a line. ### Rendering -- **Sample Shading**: shade at sample rate inside MSAA targets to reduce - shimmer. Needs the `sampleRateShading` feature, which most deferred - renderers never enable, so expect this card to be empty more often than not. -- **Alpha To Coverage**: turn fragment alpha into coverage. Softens cutout - edges on foliage and fences, and only does anything where the game already - renders to an MSAA target. + +**Sample Shading** shade at sample rate inside MSAA targets to cut shimmer. volt never enables `sampleRateShading`; most deferred renderers never ask for it. + +**Alpha To Coverage** turns it off, whatever the game asked for. volt never forces it on: on needs the fragment shader to write alpha at location 0, and volt never reads a shader. Only does something where the game already renders to MSAA. + +**Alpha To One** force fragment alpha to 1 after the shader. volt never enables the feature. + +**Depth Clamp** keep fragments outside the near and far planes and pin their depth instead of discarding them. Stops weapon models being sliced open against walls. Same toggle covers the far plane, where geometry flattens onto it instead of vanishing, so test per game. Most games never enable `depthClamp` and volt won't enable it for them, so this usually does nothing and says so in the log. ### Framerate -- **Frame Limit**: cap the frame rate at present time, from a fixed list of - common caps. Deadlines follow a fixed target timeline rather than the - last present, unless the method is reactive, so scheduler jitter does not - build up into a drift below the rate you asked for. -- **Frame Limit Method**: early holds the frame back so presents leave on a - fixed cadence; late lets the present through and waits afterwards, so the - game starts its next frame later and samples input closer to display time; - reactive waits where early does but measures from the frame just shown - instead of a fixed timeline, so a slow frame is never chased with a fast - one. -- **Frame Pacing**: how the limiter kills time, from cheapest to tightest. - `sleep` hands the whole wait to the kernel. `sliced` sleeps in short steps - and rechecks the clock, correcting for the kernel waking late. `precise` - sleeps most of the interval then busy waits half a millisecond. `spin` - busy waits the whole interval, the steadiest option and the only one that - keeps a core awake. + +Most limiters give you a cap and a method. volt gives you four settings. Nothing else on Linux covers all four. + +**Frame Limit** cap at present time. Deadlines follow a fixed timeline rather than the last present, so scheduler jitter doesn't drift you below the rate you asked for. A frame that misses its deadline by more than one interval is released at once and the timeline reanchors from there, instead of waiting out the rest of the interval it already missed. Kept per swapchain. + +**Frame Limit Cadence** which rate the limiter paces at. + +- `fixed` is your cap and nothing else. +- `smooth` paces at the slowest of the last few frames, so fast frames wait for slow ones and the cadence comes out even at whatever the machine holds. +- `dynamic` reads the same and rounds down to a quarter step of the cap. A 60 cap steps 60, 48, 40, 34, 30. A 240 cap steps 240, 192, 160, 137, 120. + +Both take the idea from consoles: pick a rate the machine can hold and stay on it. Neither reads the average, because a limiter can only make frames later and a frame slower than the average could never be paced up to it. Both climb back on their own and neither exceeds your cap. + +You're trading frames for evenness. `fixed` does nothing once the machine falls under the cap, so you get whatever it produced, one frame long and the next short. A rate sitting on one of `dynamic`'s steps can bounce between two, which is what rounding costs; `smooth` is the same reading without it. Use `fixed` if the machine holds the cap, or if you want every frame you can get. + +**Frame Limit Method** `early` holds the frame back so presents leave on a fixed cadence. `late` lets the present through and waits after, so the game samples input closer to display time. This is the equivalent of Reflex and Anti-Lag. `reactive` waits where early does but measures from the frame just shown, so a slow frame is never chased with a fast one. + +**Frame Pacing** how the limiter kills time. `sleep` hands the wait to the kernel. `sliced` sleeps in short steps and rechecks. `precise` sleeps most of it then busy waits half a millisecond. `spin` busy waits throughout, steadiest and the only one that keeps a core awake. ## How It Works -volt registers as an implicit Vulkan layer, gated by `VOLT_ENABLE=1` which the -`volt` launcher sets on the target process only. The layer reads the selected -profile from `~/.config/volt-gui/.toml` once at startup and rewrites -the Vulkan calls the game makes, and the tabs run in the order the layer acts: -device enumeration for GPU selection, the surface queries and swapchain -creation for the display settings, `vkCreateSampler` for texture settings, -`vkCreateGraphicsPipelines` for the rendering toggles, and `vkQueuePresentKHR` -for the frame limiter. Device creation is read and never modified: volt learns -which features the game enabled so a setting that needs one applies only where -the game asked for it, and it enables nothing the game left off. - -Every setting is hooked on each path that reaches it, not just the obvious -one. A game that queries formats through -`vkGetPhysicalDeviceSurfaceFormats2KHR`, enumerates through -`vkEnumeratePhysicalDeviceGroups`, creates samplers inline through -`vkWriteSamplerDescriptorsEXT` or sets alpha to coverage as dynamic state gets -the same treatment as one that takes the core path. - -Settings are frozen for the life of the process. Press Apply, then start the -game again. - -volt-gui is the PySide6 front end. It edits the same profile files the layer -reads Apply just saves the profile, no elevated permissions, no scripts. +volt registers as an implicit layer (`VK_LAYER_VOLT_settings`). The manifest declares `enable_environment = VOLT_ENABLE`, so the loader always finds it but only activates it when the `volt` launcher sets that variable on the child process. + +The layer reads `~/.config/volt-gui/.toml` once at startup and rewrites the calls the game makes: + +| Tab | Where the layer acts | +|-----|----------------------| +| GPU | `vkEnumeratePhysicalDevices`, `vkEnumeratePhysicalDeviceGroups(KHR)` | +| Display | `vkGetPhysicalDeviceSurfacePresentModesKHR`, `...SurfaceCapabilities(2)KHR`, `vkCreateSwapchainKHR`, `vkCreateSharedSwapchainsKHR`, `vkQueuePresentKHR` | +| Textures | `vkCreateSampler`, `vkWriteSamplerDescriptorsEXT`, `vkCreateGraphicsPipelines`, `vkCreateComputePipelines`, `vkCreateShadersEXT`, `vkCreateRayTracingPipelines(KHR/NV)`, `vkGetPipelineIndirectMemoryRequirementsNV`, `vkGetPipelineKeyKHR`, `vkCreatePipelineBinariesKHR` | +| Rendering | `vkCreateGraphicsPipelines`, `vkGetPipelineKeyKHR`, `vkCreatePipelineBinariesKHR`, `vkCmdSetAlphaToCoverageEnableEXT`, `vkCmdSetAlphaToOneEnableEXT`, `vkCmdSetDepthClampEnableEXT` | +| Framerate | `vkQueuePresentKHR` | + +Device creation is read, never modified. volt learns which features the game enabled so feature-gated settings apply only where the game asked, and enables nothing itself. + +Every setting is hooked on each path that reaches it. `2`/`EXT` query variants, device groups, shared swapchains, inline sampler writes and dynamic alpha-to-coverage get the same treatment as the core calls. A present mode list the driver fills is filtered in place; a list the game supplies at swapchain creation is rebuilt as volt's own copy holding the forced mode, and the mode named at present is rebuilt to match. + +An entry point for an extension the game never enabled is unreachable, and the layer only returns a hook when the call resolves further down the chain. + +volt-gui is the PySide6 front end. Apply just saves the profile. No elevated permissions, no scripts. ## Requirements | Component | Requirement | |-----------|-------------| -| **Layer** | Vulkan 1.0+ with `VK_KHR_swapchain`, Linux x86_64 (optionally i686 for 32-bit games) | -| **Build** | Rust 1.77+ with rustup, GNU make 4.3+ | -| **GUI** | Python 3.10+, PySide6 (a venv is created under `build/` by the make targets) | -| **Flatpak bundles** | `flatpak`, `ostree` | -| **Container release** | `podman` or `docker` | -| **Preview** | `vkgears` from mesa-demos, optional: without it the option lists fall back to defaults | +| Layer | Vulkan 1.0+ with `VK_KHR_swapchain`, Linux x86_64 (plus i686 for 32-bit games) | +| Build | Rust 1.85.1+ with rustup, GNU make 4.3+ | +| 32-bit layer | 32-bit glibc and libgcc development files | +| GUI | Python 3.10+, PySide6 | +| Flatpak bundles | `flatpak`, `ostree` | +| Container release | `podman` or `docker` | +| Probe | `libxcb` or `libwayland-client` at runtime, neither required | + +No native aarch64 build. See [FEX-Emu / Box64](#fex-emu--box64). ## Installation -Every build target is a file, so make only rebuilds what actually changed. -Everything lands under `build/`; `make clean` is a single `rm -rf`. +### Arch Linux (AUR) + +There's an unofficial [volt-gui](https://aur.archlinux.org/packages/volt-gui) package. I don't maintain it, but the packager has been good to deal with, so I won't steer you away. + +Read the `PKGBUILD` first. Not because of the packager, but because the AUR lets anyone submit anything. + +### NixOS + +There's an unofficial [volt-gui-nix](https://github.com/keygenesis/volt-gui-nix) flake. I don't maintain it either, but the packaging was well put together when it was offered here, so I won't steer you away. + +Read the `flake.nix` first. Not because of the packager, but because the same applies to any packaging you didn't write or doesn't come from your distro repo. + +### From source -| Command | What it builds | -|---------|----------------| -| `make` | 64-bit layer, 32-bit layer, the `volt` launcher, and the GUI | -| `make layer-64` | `build/target/x86_64-unknown-linux-gnu/release/{libvolt.so,volt}` | -| `make layer-32` | `build/target/i686-unknown-linux-gnu/release/libvolt.so` | -| `make gui-pyinstaller` | `build/bin/volt-gui-pyinstaller` | -| `make gui-nuitka` | `build/bin/volt-gui-nuitka` | +Every build target is a file, so make only rebuilds what changed. Everything lands under `build/`. + +| Command | What it does | +|---------|--------------| +| `make` | both layers, launcher, GUI, desktop entry | +| `make layer-64` | 64-bit layer, launcher, probe | +| `make layer-32` | 32-bit layer | +| `make gui` | `build/bin/volt-gui` | | `make flatpak` | `build/bundles/*.flatpak` | -| `make release` | the full release matrix in `releases/`, host toolchain | -| `make release-container` | the same, built inside a pinned container image | -| `sudo make install` | launcher, GUI, both layers and the manifest | -| `sudo make flatpak-install` | the built extension bundles, for the invoking user | -| `sudo make uninstall` | everything, including the user Flatpak extension and `~/.config/volt-gui` | +| `make dist` | sources with `build/` populated | +| `make release` | archive in `releases/`, host toolchain | +| `make release-container` | same, inside the build image | +| `sudo make install` | system-wide | +| `make install-user` | into `~/.local`, no root | +| `sudo make flatpak-install` | extension bundles | +| `make flatpak-install-user` | same, `--user` | +| `make setup-user` | `install-user` + `flatpak-install-user` | +| `sudo make uninstall` | everything | +| `make uninstall-user` | the rootless install | | `make clean` | `rm -rf build releases` | +| `make help` | this list | + +A bare `make` builds both architectures. The 32-bit layer isn't optional, any Steam library has 32-bit titles. `make layer-32` exists for working on that one piece and adds the Rust target if missing. + +Flatpak bundles are the opposite: optional, built only by `make flatpak`, and neither install target touches them. + +Actions artifacts are `make dist` trees. Unpack one and `sudo make install` installs without compiling. + +Building with `sudo` is refused, so you never end up with a root-owned `build/`. Install targets only copy what's already built and name what's missing if you skipped a step. volt-gui also refuses to start under `sudo`. + +Packagers can stage without root: ``` -git clone https://github.com/pythonlover02/volt-gui.git -cd volt-gui make -sudo make install +make install DESTDIR="$PWD/pkg" PREFIX=/usr ``` -`make` picks PyInstaller for the GUI. For Nuitka, set `GUI` on both halves so -install picks up the binary you built: +With `DESTDIR` set the install skips `ldconfig`, the desktop database, the icon cache, and the competing-install check. + +## Install paths + +| File | System | User | +|------|--------|------| +| Launcher | `/usr/bin/volt` | `~/.local/bin/volt` | +| Probe | `/usr/bin/volt-probe` | `~/.local/bin/volt-probe` | +| GUI | `/usr/bin/volt-gui` | `~/.local/bin/volt-gui` | +| Library 64 | `/usr/lib/x86_64-linux-gnu/libvolt.so` | `~/.local/lib/volt/x86_64-linux-gnu/libvolt.so` | +| Library 32 | `/usr/lib/i386-linux-gnu/libvolt.so` | `~/.local/lib/volt/i386-linux-gnu/libvolt.so` | +| Manifest | `/usr/share/vulkan/implicit_layer.d/VkLayer_volt.json` | `~/.local/share/vulkan/implicit_layer.d/VkLayer_volt.json` | +| Desktop entry | `/usr/share/applications/volt-gui.desktop` | `~/.local/share/applications/volt-gui.desktop` | +| Icon | `/usr/share/icons/hicolor/256x256/apps/volt-gui.png` | `~/.local/share/icons/hicolor/256x256/apps/volt-gui.png` | +| Install stamps | `/var/lib/volt` | `~/.local/share/volt` | + +The library directory follows what your distribution uses. Because the manifest lands in the implicit layer directory and the libraries in standard paths, 32-bit games find the 32-bit layer and 64-bit games the 64-bit one with no `VK_LAYER_PATH` mapping. + +> [!WARNING] +> Don't change `PREFIX` away from `/usr` or `/usr/local`. The loader only scans a fixed set of manifest directories. Installing to `/opt/volt` puts the manifest where nothing reads it and the launcher off `$PATH`. + +## Uninstalling ``` -make GUI=nuitka -sudo make install GUI=nuitka +sudo make uninstall # system +make uninstall-user # ~/.local ``` -Building with `sudo` is refused: the build targets stop with an error rather -than leaving a root-owned `build/`. Build as your user, install as root. +Both remove the binaries, libraries, manifest, desktop entry, icon, install stamps, the user-scope Flatpak extension and `~/.config/volt-gui`. Run directly as root there's no `SUDO_USER` to work from, so the user-scope steps are skipped. -Packagers can skip root entirely — with `DESTDIR` set, `make install` stages -into the given prefix without needing it: +Neither touches a 1.x install. 1.x lived in `/usr/local/bin`, 2.0 lives in `/usr/bin`. Remove 1.x first: + +``` +sudo rm -f /usr/local/bin/volt /usr/local/bin/volt-gui /usr/local/bin/volt-helper +sudo rm -f /usr/share/applications/volt-gui.desktop +sudo update-desktop-database /usr/share/applications +``` + +Do it before installing 2.0. `/usr/local/bin` comes first on most distributions, so a leftover 1.x `volt` shadows the new launcher, never sets `VOLT_ENABLE`, and every setting silently does nothing. If 2.0 looks dead, run `which volt`. + +`make clean` removes `build/` and `releases/` plus stray directories from older layouts. + +## Immutable Systems + +On SteamOS, Bazzite, Silverblue and anything with a read-only `/usr`, skip the system install: ``` make -make install DESTDIR="$PWD/pkg" PREFIX=/usr +make install-user ``` -## Building Releases +Plus the Flatpak extension if you want it: + +``` +make flatpak +make flatpak-install-user +``` + +`~/.local/bin` has to be on your `PATH`, because volt-gui runs `volt` and `volt-probe` to read your hardware. + +Pick one install, not both. The loader scans system and user directories alike, so two manifests naming the same layer leave it undefined which is used, or whether the layer is inserted twice. Both install targets refuse to run while the other owns the layer. -Both release targets produce the same four files in `releases/`: +The GUI is one self-contained binary, so unpacking a release and double-clicking `build/bin/volt-gui` opens the editor with nothing installed. Enough to write and copy profiles, not enough to use them: with no layer on disk the probe can't run, so every device-backed card holds only `default`. + +The Flatpak extension never covers native Steam games, which run under the Steam Linux Runtime. The native install does reach them: Steam expands `%command%` on the host, and the runtime container bind-mounts your home directory and imports host implicit layers. + +## FEX-Emu / Box64 + +On aarch64, x86_64 games run through FEX-Emu or Box64. There's no native aarch64 build because every shipping Vulkan game on Linux has an x86_64 build. + +Translation layers run the game inside their own root: a tree of x86_64 binaries separate from the host `/usr`. The layer goes into that tree. + +**If your kernel routes x86_64 ELFs through `binfmt_misc`,** an x86_64 Flatpak runtime behaves normally: ``` -volt-gui--pyinstaller-x86_64.AppImage -volt-gui--nuitka-x86_64.AppImage -volt-gui--pyinstaller.tar.gz -volt-gui--nuitka.tar.gz +flatpak install org.freedesktop.Platform//24.08 --arch=x86_64 +make flatpak +make flatpak-install-user ``` -Each archive is a ready-to-install tree: the compiled layers sit at the paths -the Makefile expects, so unpacking and running `sudo make install` installs -without compiling anything. The sources, the Flatpak bundles and the Makefile -ride along. The Nuitka archive installs with `sudo make install GUI=nuitka`. +**Otherwise, install into the translation root:** -**`make release`** builds against your own toolchain and glibc. Fast, and -right for a local build, but the binaries inherit your system's glibc floor. +``` +make +make install DESTDIR=/path/to/translation-root +``` -**`make release-container`** builds everything inside `rust:1.82-bookworm` -(Debian 12, glibc 2.36, Python 3.11) from `container/Containerfile`, so the -floor is fixed regardless of what you run. It builds into `build/container/`, -so it never collides with your host build and neither invalidates the other. -The container runs as your own uid, so nothing it writes is root-owned. +Needs no root and touches nothing on the host. Clear it with `make DESTDIR=/path/to/translation-root uninstall`. -Override the base if you want a different floor: +## Building Releases + +Both targets produce `releases/volt-gui-.tar.gz`, a ready-to-install tree. Unpack and `sudo make install` without compiling. + +`make release` uses your toolchain and inherits your glibc floor. + +`make release-container` builds inside `rust:1.85.1-bookworm` (glibc 2.36, Python 3.11), so the floor is fixed. Builds into `build/container/` and runs as your uid. ``` -make release-container CONTAINER_BASE=rust:1.82-bullseye +make release-container CONTAINER_BASE=rust:1.85.1-bullseye make release-container CONTAINER=docker ``` -Bullseye drops the floor to glibc 2.31 but ships Python 3.9, which is below -what the GUI needs — use it only for `make layer-64 layer-32` inside the -container, not for a full release. +Bullseye drops the floor to glibc 2.31 but ships Python 3.9, below what the GUI needs. Use it for `make layer-64 layer-32` only. ## Usage -Prepend the launcher to your game command: +``` +volt [--probe] [PROFILE] -- COMMAND [ARGS...] +volt -- COMMAND [ARGS...] # default profile +volt --help +``` + +Everything before `--` is launcher options, everything after is the command: ``` -volt -- %command% # Steam, default profile -volt myprofile -- %command% # Steam, named profile -volt -- ./game # terminal +volt -- %command% # Steam +volt myprofile -- %command% # named profile +volt -- ./game volt -- flatpak run com.example.Game ``` -The launch command for the selected profile is always shown next to the Apply -button in volt-gui, ready to copy. +The launch command for the selected profile is shown at the top of the window, ready to copy. + +Profile names must be non-empty graphic ASCII with no space, no path separator, no `..` and no null byte. Anything else falls back to default with a warning. The launcher writes a commented profile on first use. + +To see what applied: + +``` +VOLT_LOG=info volt -- ./game +``` + +Every line is prefixed `[volt]` and goes to stderr. + +At `info` every setting gets a line, naming what the game asked for and what +volt wrote in its place. + +``` +[volt] gpu device: asked 2 +[volt] present_mode: asked fifo, applied mailbox +[volt] image_count: asked 3, applied 3 +[volt] mag_filter: asked linear, applied nearest +[volt] anisotropy: the application did not enable samplerAnisotropy +[volt] depth_clamp: the application did not enable depthClamp +[volt] frame_limit: applied 60 +[volt] frame_pacing: the profile did not set it +``` + +Every setting line names the setting first, then either `asked A, applied B` +or the reason the setting did not land. The applied value is the one volt +wrote, so a setting the device clamped shows what landed rather than what +the profile says. + +The four Framerate settings have no asked value, since a game never tells +Vulkan what frame rate it wants. They report what volt applied, or say the +profile did not set them. + +The GPU line reads `asked N, applied M` when the profile sets a gpu: N is +the device the game used, M the profile's pick. With no gpu in the profile +it reads `asked N` alone. + +Each distinct asked and applied pair prints once per device however many +samplers, pipelines or swapchains the game creates, and a reason line prints +each time a value is kept. + +## Environment Variables + +| Variable | Purpose | Values | Default | +|----------|---------|--------|---------| +| `VOLT_CONFIG_NAME` | which profile to load | any profile name | `default` | +| `VOLT_LOG` | log verbosity, to stderr | `off`, `error`, `warn`, `info` | `warn` | +| `VOLT_PROBE` | write `probe.toml` from this device and its surfaces | any non-empty value | unset | +| `VOLT_ENABLE` | activates the layer | `1` | unset | +| `VOLT_DISABLE` | the loader's off switch | `1` | unset | + +`HOME` decides where profiles live and falls back to `/tmp` with a warning. `LD_LIBRARY_PATH` is extended by the launcher with both layer directories, preserving what was there. + +There's no environment override for the settings themselves. A profile file is the only way to set them, which keeps the panel and the layer describing the same thing. + +## Files + +| Path | What it is | +|------|------------| +| `~/.config/volt-gui/default.toml` | default profile | +| `~/.config/volt-gui/.toml` | named profiles | +| `~/.config/volt-gui/probe.toml` | what the last probe read, one section per backend | +| `~/.config/volt-gui/options.toml` | volt-gui preferences and last active profile | + +Profiles are plain TOML, one section per tab and one string per setting, so you can edit them by hand or keep them in a dotfiles repo. `probe.toml` is written by the layer and watched by the GUI, so a freshly probed device fills the panel without a restart. Deleting it costs a re-probe. ## Flatpak -Flatpak games run sandboxed, so the layer ships as a **Flatpak runtime -extension** for `org.freedesktop.Platform` 23.08, 24.08 and 25.08. Build with -`make flatpak`, which writes to `build/bundles/`, then install with -`sudo make flatpak-install` (or grab the bundles from a release archive). The -`volt` launcher detects `flatpak run` commands and routes activation through -the in-sandbox wrapper automatically. +Flatpak games can't see host paths, so the layer ships as a runtime extension for `org.freedesktop.Platform` 23.08, 24.08 and 25.08. + +Separate and optional. Neither `make` nor the install targets produce or touch the bundles: + +``` +make flatpak +make flatpak-install-user # or: sudo make flatpak-install +``` + +One bundle per runtime. Install the one matching yours, run `flatpak list` if unsure. Multiple versions can coexist. Every bundle carries the 32-bit library too. + +``` +flatpak install --user build/bundles/org.freedesktop.Platform.VulkanLayer.volt-24.08.flatpak +flatpak uninstall --user org.freedesktop.Platform.VulkanLayer.volt +``` + +The launcher detects `flatpak run` and routes through the in-sandbox wrapper. It also mounts `~/.config/volt-gui` into the sandbox, read-only, or read-write under `--probe`: + +``` +volt -- flatpak run com.example.Game +volt -- flatpak run --branch=stable com.example.Game +volt myprofile -- flatpak run com.example.Game +``` + +There's no Flatpak build of volt-gui itself, only the layer. + +### Without the launcher + +Call the wrapper yourself, useful where only the extension is installed. Mount the profiles yourself too. The wrapper runs inside the sandbox and can't do it from there: + +``` +flatpak run --filesystem=xdg-config/volt-gui:ro --command=/usr/lib/extensions/vulkan/volt/bin/volt-flatpak com.example.Game +VOLT_CONFIG_NAME=myprofile flatpak run --filesystem=xdg-config/volt-gui:ro --command=/usr/lib/extensions/vulkan/volt/bin/volt-flatpak com.example.Game +``` + +A Steam launch option has no room for that flag, so grant it once instead: + +``` +flatpak override --user --filesystem=xdg-config/volt-gui:ro com.example.Game +``` + +Then the launch option is just the wrapper: + +``` +/usr/lib/extensions/vulkan/volt/bin/volt-flatpak %command% +``` -There is no Flatpak build of volt-gui itself, only the layer extension. +Without the grant the layer still loads, it just finds no profile and leaves every setting alone. It logs the path it couldn't read. -## Profiles & Presets +## Profiles, Presets & Options -Profiles are TOML files in `~/.config/volt-gui/`, one per configuration. -Create and switch them from the GUI or the system tray; select one at launch -with `volt -- ...`. +**Profiles** are TOML files in `~/.config/volt-gui/`, one per configuration. Create and switch from the GUI, the tray, or `volt -- ...`. Switching saves the one you were on and restarts the probe. -Presets populate the active profile with curated values, from **Quality** -(trilinear, 16x anisotropy, blended mips, 10 bit colour, classic vsync) down -to **Potato Low Latency** (bilinear, anisotropy off, hard mip cuts, immediate -present, 2 image swapchain). -A preset writes every value in the profile, so anything it does not set goes -back to default. That includes the frame limit and the colour space, transfer -function, composite alpha and clipped settings: those depend on your display -and your compositor, so those choices stay yours. +**Presets** fill the active profile with curated values, from Quality (trilinear, 16x anisotropy, blended mips, classic vsync) down to Potato Low Latency (bilinear, anisotropy off, hard mip cuts, immediate present, 2 images). A preset writes every value, so anything it doesn't set goes back to default. No preset sets frame limit, composite alpha or clipped presentation, since those depend on your display. A preset naming something your hardware lacks resets that one to default and says which. -A preset can name something your hardware does not offer. That setting resets -to default and volt-gui says which ones, so the rest of the preset still -lands. +**Options** holds volt-gui's own preferences, not anything the layer reads: theme, transparency, display backend, scale, start maximised or in tray, tray icon, welcome window. They save as you change them and take effect on restart. One instance at a time. ## What volt will never do -volt changes what the game asks Vulkan for; it never draws. Anything that -requires injecting shaders or processing the image is out of scope: - -- Sharpening, FSR or any upscaling, frame generation, post processing. -- Forced MSAA or SSAA. Render passes and sample counts belong to the game. -- Overlays and HUDs. Use MangoHud for that. -- Overclocking, fan curves, power limits. That is sysfs territory, not the - Vulkan API. Use CoreCtrl for that. -- OpenGL. The per driver environment variable maze that OpenGL support - requires is exactly what this rewrite retired. -- Enable a Vulkan device feature or extension the game did not request. Where - a setting needs a feature, volt reads what the game enabled and applies the - setting only where the game enabled it. -- Require a Vulkan extension. Core 1.0 and `VK_KHR_swapchain` are the whole - surface volt asks for, so behaviour never splits between drivers. volt may - intercept an extension command the game itself calls, so a setting keeps - applying where the game moved that state elsewhere; a hook for an extension - the game never enabled is unreachable and its entry point is never handed - out. -- Change a setting under a running game. The profile is read once at startup. -- Write into memory the game owns. volt patches the structures it passes on - and fills the arrays a query asks it to fill; a `pNext` chain the game built - is read and never written. A setting reaches the game through the lists volt - hands it, not by reaching into what the game already built. +volt changes what the game asks Vulkan for. It never draws. Anything needing shader injection or image processing is out of scope. + +- **Sharpening, FSR, upscaling, frame generation, post processing.** +- **Forced MSAA or SSAA.** Adding samples means recreating every render target, adding resolves and rewriting pipelines and shaders. That's the game's frame graph, not a value passing by. +- **Colour depth, colour space, transfer function.** Every 10-bit surface format is UNORM, so forcing 8 to 10 drops hardware sRGB encoding and washes the picture out, and a game that hardcoded its format ends up with image views that don't match. None of it makes a game render wider content anyway. A game that wants HDR asks for it through DXVK_HDR, PROTON_ENABLE_HDR or gamescope. +- **Cubic filtering.** Needs `VK_EXT_filter_cubic`, admitted per format, while a sampler names no format at all. There's no moment where volt can tell whether it'd be legal. +- **Overlays and HUDs.** Use MangoHud. +- **Overclocking, fan curves, power limits.** That's sysfs, not Vulkan. Use LACT, or CoreCtrl if you want CPU controls too. +- **OpenGL.** The per-driver environment variable maze is exactly what this rewrite retired. +- **Enable a feature or extension the game didn't request.** +- **Require a Vulkan extension.** Core 1.0 and `VK_KHR_swapchain` is the whole surface. +- **Resolution scaling.** Needs `VK_KHR_surface_maintenance1` and `VK_KHR_swapchain_maintenance1`, and volt enables neither. Use gamescope. +- **Frame pacing tighter than the limiter gives.** Deadlines measured against the display need `VK_KHR_present_wait` or `VK_EXT_present_timing`. `late` is as close as core Vulkan reaches. +- **Change a setting under a running game.** +- **Write into memory the game owns.** volt patches the structures it passes on and fills the arrays a query asks it to fill. A `pNext` chain the game built is read, never written. ## Contributing -Contributions are welcome. The layer is plain Rust with no build scripts; the -GUI is PySide6 only. Please keep changes working on core Vulkan 1.0 with no -extensions that floor is the point of the project. +Contributions welcome. The layer is plain Rust with no build scripts, the GUI is PySide6 only. Keep changes working on core Vulkan 1.0 with no extensions. That floor is the point of the project. diff --git a/VkLayer_volt.json b/VkLayer_volt.json index 2c11d03..062bfa7 100644 --- a/VkLayer_volt.json +++ b/VkLayer_volt.json @@ -6,7 +6,7 @@ "library_path": "libvolt.so", "api_version": "1.0.0", "implementation_version": "1", - "description": "Driver style Vulkan settings layer for Linux.", + "description": "My AMD Adrenaline / NVIDIA Settings Linux Alternative", "functions": { "vkNegotiateLoaderLayerInterfaceVersion": "vkNegotiateLoaderLayerInterfaceVersion" }, diff --git a/container/Containerfile b/container/Containerfile index a2facf9..29c02c2 100644 --- a/container/Containerfile +++ b/container/Containerfile @@ -1,4 +1,4 @@ -ARG BASE=rust:1.82-bookworm +ARG BASE FROM ${BASE} RUN apt-get update -qq \ @@ -6,7 +6,7 @@ RUN apt-get update -qq \ gcc-multilib libc6-dev-i386 \ python3 python3-venv python3-dev \ flatpak ostree \ - wget ca-certificates file zsync desktop-file-utils patchelf make \ + ca-certificates make \ && rm -rf /var/lib/apt/lists/* RUN rustup target add i686-unknown-linux-gnu \ diff --git a/flatpak/volt-flatpak b/flatpak/volt-flatpak index a226798..c1f3d6d 100644 --- a/flatpak/volt-flatpak +++ b/flatpak/volt-flatpak @@ -1,14 +1,103 @@ #!/bin/sh -BASE="/usr/lib/extensions/vulkan/volt" -export VOLT_ENABLE=1 -export VK_ADD_LAYER_PATH="${BASE}/share/vulkan/implicit_layer.d${VK_ADD_LAYER_PATH:+:$VK_ADD_LAYER_PATH}" -LIB="${BASE}/lib/x86_64-linux-gnu" -[ -d "${BASE}/lib/i386-linux-gnu" ] && LIB="${LIB}:${BASE}/lib/i386-linux-gnu" -export LD_LIBRARY_PATH="${LIB}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" - -if [ $# -eq 0 ] && [ -r /app/manifest.json ]; then - APP_CMD=$(sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' /app/manifest.json | head -n1) - [ -n "$APP_CMD" ] && set -- "$APP_CMD" -fi - -exec "$@" + +VOLT_BASE="/usr/lib/extensions/vulkan/volt" +LIB_DIR_64="${VOLT_BASE}/lib/x86_64-linux-gnu" +LIB_DIR_32="${VOLT_BASE}/lib/i386-linux-gnu" +APP_MANIFEST="/app/manifest.json" +FLATPAK_INFO="/.flatpak-info" +MANIFEST_COMMAND_EXPR='s/.*"command"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' +INFO_COMMAND_EXPR='s/^[[:space:]]*command[[:space:]]*=[[:space:]]*//p' +EXIT_NO_COMMAND=1 + +classify_dir_status() { + case "$1" in + 0) echo present ;; + *) echo absent ;; + esac +} + +classify_command() { + case "$1" in + "") echo missing ;; + *) echo found ;; + esac +} + +classify_arguments() { + case "$1" in + 0) echo empty ;; + *) echo given ;; + esac +} + +select_library_path() { + case "$1" in + present) echo "${LIB_DIR_64}:${LIB_DIR_32}" ;; + absent) echo "${LIB_DIR_64}" ;; + esac +} + +call_dir_status() { + test -d "$1" + echo "$?" +} + +call_read_manifest_command() { + sed -n "$MANIFEST_COMMAND_EXPR" "$APP_MANIFEST" 2>/dev/null | head -n1 +} + +call_read_info_command() { + sed -n "$INFO_COMMAND_EXPR" "$FLATPAK_INFO" 2>/dev/null | head -n1 +} + +call_export_environment() { + VOLT_ENABLE=1 + XDG_DATA_DIRS="${VOLT_BASE}/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" + PATH="${VOLT_BASE}/bin${PATH:+:$PATH}" + LD_LIBRARY_PATH="${1}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + export VOLT_ENABLE XDG_DATA_DIRS PATH LD_LIBRARY_PATH +} + +call_exec_rest() { + shift + exec "$@" +} + +call_exec_command() { + exec "$1" +} + +call_report_missing() { + echo "volt-flatpak: no application command in ${APP_MANIFEST} or ${FLATPAK_INFO}" >&2 + echo "volt-flatpak: name the binary yourself, after the application id" >&2 + exit "$EXIT_NO_COMMAND" +} + +choose_command() { + case "$(classify_command "$1")" in + found) echo "$1" ;; + missing) call_read_info_command ;; + esac +} + +launch_resolved() { + case "$(classify_command "$1")" in + found) call_exec_command "$1" ;; + missing) call_report_missing ;; + esac +} + +run_target() { + case "$1" in + given) call_exec_rest "$@" ;; + empty) launch_resolved "$(choose_command "$(call_read_manifest_command)")" ;; + esac +} + +main() { + call_export_environment \ + "$(select_library_path "$(classify_dir_status "$(call_dir_status "$LIB_DIR_32")")")" + run_target "$(classify_arguments "$#")" "$@" +} + +main "$@" diff --git a/images/1.png b/images/1.png index 9191e12..bad9949 100644 Binary files a/images/1.png and b/images/1.png differ diff --git a/images/2.png b/images/2.png index 0ca61a7..c7d5770 100644 Binary files a/images/2.png and b/images/2.png differ diff --git a/images/3.png b/images/3.png index 7c9b456..b86855b 100644 Binary files a/images/3.png and b/images/3.png differ diff --git a/images/icon.png b/images/icon.png new file mode 100644 index 0000000..2b09ab1 Binary files /dev/null and b/images/icon.png differ diff --git a/requirements.txt b/requirements.txt index 8d95f33..677ac0c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -Nuitka==4.0.8 -pyinstaller==6.19.0 -PySide6==6.11.0 +pyinstaller==6.22.2 +PySide6==6.11.2 diff --git a/volt-gui/database.py b/src/volt-gui/database.py similarity index 50% rename from volt-gui/database.py rename to src/volt-gui/database.py index de88be1..355dc55 100644 --- a/volt-gui/database.py +++ b/src/volt-gui/database.py @@ -1,9 +1,10 @@ from typing import Final +from probe import alpha_one_options from probe import alpha_options from probe import aniso_options from probe import call_read_probe -from probe import depth_options +from probe import clamp_options from probe import frametime_pairs from probe import gpu_options from probe import image_count_options @@ -12,11 +13,10 @@ from probe import plain_pairs from probe import present_options from probe import shading_options -from probe import space_options -from probe import transfer_options +from probe import stepped_values -APP_VERSION: Final[str] = "2.0.0" +APP_VERSION: Final[str] = "2.4.2" APP_AUTHOR: Final[str] = "pythonlover02" APP_LICENSE: Final[str] = "GPL 3.0 License" APP_DESCRIPTION: Final[str] = "My AMD Adrenaline / NVIDIA Settings Linux Alternative" @@ -26,6 +26,10 @@ PROFILE_TABS: Final[tuple] = ("GPU", "Display", "Textures", "Rendering", "Framerate") ALL_TABS: Final[tuple] = ("GPU", "Display", "Textures", "Rendering", "Framerate", "Options", "About") +FRAME_LIMIT_FIRST: Final[int] = 1 +FRAME_LIMIT_LAST: Final[int] = 1000 +SCALE_LOW: Final[float] = 0.8 +SCALE_HIGH: Final[float] = 2.0 SETTINGS_DB: Final[dict] = { @@ -33,7 +37,7 @@ "device": { "section": "gpu", "label": "Physical Device", - "description": "Which GPU the game sees, listed by name as this machine reports them. The layer hides every other device from enumeration, so a game that takes the first one it is offered gets yours. If your choice matches nothing, the full list comes back and a warning is logged.", + "description": "Which GPU the game sees. The layer hides every other device from enumeration, so a game that takes the first one it is offered gets yours. If nothing matches, the full list comes back and a warning is logged.", "options": (DEFAULT_VALUE,), }, }, @@ -41,37 +45,20 @@ "present_mode": { "section": "display", "label": "VSync / Present Mode", - "description": "How finished frames reach the screen. immediate turns vsync off, mailbox is low latency vsync, fifo is classic vsync, fifo_relaxed tears only below refresh. The layer hides every other mode from the list the game is shown, so a game's own vsync menu cannot offer one you ruled out, whatever route it takes to ask. A mode the surface does not support falls back to the game's own choice with a warning.", + "description": "How finished frames reach the screen. immediate turns vsync off, mailbox is low latency vsync, fifo is classic vsync, fifo_relaxed tears only below refresh. Only these four are offered or forced; a mode an extension defines is left wherever the driver reported it. A mode the surface lacks falls back to the game's own choice with a warning.", "options": (DEFAULT_VALUE,), }, "image_count": { "section": "display", + "step": 1, "label": "Swapchain Images", - "description": "How many images the swapchain holds. Fewer images lower display latency, more images smooth frame delivery. The list is what this surface allows, and the choice is reported back to the game as well, so a game that picks its count from what the surface offers honours it on its own.", - "options": (DEFAULT_VALUE,), - }, - "color_depth": { - "section": "display", - "label": "Color Depth", - "description": "Bits per colour channel, grouped out of the surface formats this surface offers. The layer hides every other format, so a game that takes the first supported one ends up with yours. If nothing matches, the full list comes back and a warning is logged.", - "options": (DEFAULT_VALUE,), - }, - "color_space": { - "section": "display", - "label": "Color Space", - "description": "Which color space the game is allowed to see, filtered out of the same surface format list as Color Depth. Everything past srgb_nonlinear comes from a swapchain colorspace extension and only appears when the stack around the game enabled it, through DXVK_HDR, PROTON_ENABLE_HDR or gamescope, so on most setups this card holds one entry. A space volt has no name for still appears and still applies.", - "options": (DEFAULT_VALUE,), - }, - "transfer_function": { - "section": "display", - "label": "Transfer Function", - "description": "Whether the game is shown srgb surface formats, plain unorm ones, or float ones, filtered out of the same list again. srgb formats have the encoding curve applied by the display hardware, unorm formats leave it to whatever the game does in its own shaders. Getting this wrong looks washed out or crushed rather than broken, so set it back to default if the image looks wrong. No preset touches it.", + "description": "How many images the swapchain holds, which is the frames in flight control and the closest thing here to an anti-lag setting. More lets the game run further ahead of the GPU, smoothing frame delivery and costing input lag. Fewer holds it closer to the display. The list is what this surface allows.", "options": (DEFAULT_VALUE,), }, "composite_alpha": { "section": "display", "label": "Composite Alpha", - "description": "How the compositor treats the alpha channel of the finished image. opaque tells the compositor to skip blending the window altogether, which is the cheapest path on Wayland. The list is what this surface allows, and a value the surface turns down falls back to the game's own choice with a warning.", + "description": "How the compositor treats the alpha channel of the finished image. opaque skips blending the window altogether, the cheapest path on Wayland. A value the surface turns down falls back to the game's own choice with a warning.", "options": (DEFAULT_VALUE,), }, "clipped": { @@ -84,73 +71,103 @@ "Framerate": { "frame_limit": { "section": "framerate", + "step": 1, "label": "Frame Limit", - "description": "Cap the frame rate at present time, shown with the frame budget each rate gives you. This one is volt's own, so the list is fixed rather than read from the device. Past about 500 the interval gets shorter than the kernel wakes reliably, so sleep pacing drifts above the cap and holding the rate needs sliced, precise or spin.", - "options": (DEFAULT_VALUE, "20", "24", "30", "36", "40", "45", "48", "50", "60", "72", "75", "90", "100", "120", "144", "165", "180", "240", "300", "360", "540", "600", "720", "900", "1000"), + "description": "Cap the frame rate at present time, shown with the frame budget each rate gives you. Past about 500 the interval is shorter than the kernel wakes reliably, so sleep pacing drifts above the cap and holding the rate needs sliced, precise or spin.", + "options": (DEFAULT_VALUE,), + }, + "frame_limit_cadence": { + "section": "framerate", + "label": "Frame Limit Cadence", + "description": "Which rate the limiter paces at. fixed uses your cap and nothing else. smooth paces at the slowest of the last few frames, so the fast frames wait for the slow ones and the cadence comes out even at whatever the machine is holding. dynamic reads the same and rounds it down to a quarter step of your cap, so it sits on a set rate: a 60 cap steps 60, 48, 40, 34, 30. Both trade frames for even spacing, and neither goes faster than your cap. Set fixed if the machine holds the cap, or if you want every frame you can get for the input latency. Only does something when Frame Limit is set.", + "options": (DEFAULT_VALUE, "fixed", "smooth", "dynamic"), }, "frame_limit_method": { "section": "framerate", "label": "Frame Limit Method", - "description": "When the limiter waits. early holds the frame back so presents leave on a fixed cadence. late lets the present through right away and waits before handing control back, so the game starts its next frame later and reads input closer to display time. reactive waits where early does, but measures each interval from the frame just shown rather than from a fixed timeline, so a slow frame is never chased with a fast one. Only does something when Frame Limit is set.", + "description": "When the limiter waits. early holds the frame back so presents leave on a fixed cadence. late lets the present through and waits before handing control back, so the game reads input closer to display time, which is what Reflex and Anti-Lag do. reactive waits where early does but measures from the frame just shown, so a slow frame is never chased with a fast one. Only does something when Frame Limit is set.", "options": (DEFAULT_VALUE, "early", "late", "reactive"), }, "frame_pacing": { "section": "framerate", "label": "Frame Pacing", - "description": "How the limiter waits, from cheapest to tightest. sleep hands the whole wait to the kernel and costs nothing. sliced sleeps in short steps and re-checks the clock, which corrects for the kernel waking late. precise sleeps most of the interval then busy waits half a millisecond. spin busy waits the whole thing, which is the steadiest and the only one that keeps a core awake. Only does something when Frame Limit is set.", + "description": "How the limiter waits. sleep hands the whole wait to the kernel. sliced sleeps in short steps and re-checks the clock, correcting for the kernel waking late. precise sleeps most of the interval then busy waits half a millisecond. spin busy waits throughout, the steadiest and the only one that keeps a core awake. Only does something when Frame Limit is set.", "options": (DEFAULT_VALUE, "sleep", "sliced", "precise", "spin"), }, }, "Textures": { - "filtering": { + "mag_filter": { "section": "textures", - "label": "Texture Filtering", - "description": "The sampler filter mode. retro gives sharp unfiltered pixels, bilinear smooths within a mip level, trilinear also blends between mip levels. All three are core Vulkan, so the list never changes.", - "options": (DEFAULT_VALUE, "retro", "bilinear", "trilinear"), + "label": "Magnification Filter", + "description": "How a texture is sampled when it is drawn larger than its own size, which is anything close to the camera. nearest gives sharp unfiltered pixels, linear smooths between them. linear is forced only onto a sampler already using linear somewhere; otherwise that sampler is left alone with a line in the log. This is the one filter a still screenshot shows you. Core Vulkan, so the list never changes.", + "options": (DEFAULT_VALUE, "nearest", "linear"), + }, + "min_filter": { + "section": "textures", + "label": "Minification Filter", + "description": "How a texture is sampled when it is drawn smaller than its own size, which is most of the screen. nearest takes one texel and shimmers as the camera moves. linear averages and settles, and is where mipmaps and anisotropic filtering do their work. linear is forced only onto a sampler already using linear somewhere; otherwise that sampler is left alone with a line in the log. Core Vulkan, so the list never changes.", + "options": (DEFAULT_VALUE, "nearest", "linear"), }, "mipmap_mode": { "section": "textures", "label": "Mipmap Mode", - "description": "How samplers move between mip levels. nearest cuts hard from one mip to the next, linear blends across them. Both are core Vulkan, so the list never changes. Applied after Texture Filtering, so it overrides the mip behaviour that choice implies. Only affects textures that have mips.", + "description": "How samplers move between mip levels. nearest cuts hard from one mip to the next, which shows as a band on the ground. linear blends across them, the third linear in trilinear, and is forced only onto a sampler already using linear somewhere. Core Vulkan, so the list never changes. Only affects textures that have mips.", "options": (DEFAULT_VALUE, "nearest", "linear"), }, "anisotropy": { "section": "textures", + "step": 1, "label": "Anisotropic Filtering", - "description": "Sharpen textures viewed at steep angles. Higher values look better at a small cost. The list runs in steps of two up to what your GPU reports. This one needs the samplerAnisotropy device feature, and volt never enables a feature the game left off: where the game did not ask for it the card holds nothing but default and the layer logs a line saying so. Nearly every game asks for it, so this is rare.", + "description": "Sharpen textures viewed at steep angles. Higher values look better at a small cost. The list runs up to what your GPU reports. volt never enables the feature: where the game left it off the setting is ignored and a line is logged. Nearly every game asks for it.", "options": (DEFAULT_VALUE,), }, "lod_bias": { "section": "textures", + "step": 0.1, "label": "LOD Bias", - "description": "Shift mipmap selection. Negative values sharpen at the cost of shimmer, positive values blur but render faster. A negative bias is the nearest volt gets to sharpening textures seen at a steep angle. The list runs in steps of 0.2 across the range your GPU reports, and volt clamps what it passes down to that range.", + "description": "Shift mipmap selection. Negative sharpens at the cost of shimmer, positive blurs but renders faster. A negative bias is the nearest volt gets to sharpening. The list runs in steps of 0.1 across the range your GPU reports, up to 4 either way.", "options": (DEFAULT_VALUE,), }, "mip_floor": { "section": "textures", + "step": 1, "label": "Mip Floor", - "description": "The lowest mip level samplers may use, called minimum LOD in Vulkan. Raising it forces smaller mips everywhere, trading detail for speed. The list runs in steps of two up to the largest image your GPU can address, and a level past the last mip a texture has simply lands on that last mip.", + "description": "The lowest mip level samplers may use, called minimum LOD in Vulkan. Raising it forces smaller mips everywhere, trading detail for speed. The list runs up to the largest image your GPU can address, and a level past the last mip a texture has simply lands on that last mip.", "options": (DEFAULT_VALUE,), }, "mip_ceiling": { "section": "textures", + "step": 1, "label": "Mip Ceiling", - "description": "The highest mip level samplers may use, called maximum LOD in Vulkan. Lowering it keeps distant textures sharper than the game intended. The list matches Mip Floor, and a ceiling that lands below the floor is swapped with it rather than dropped.", + "description": "The highest mip level samplers may use, called maximum LOD in Vulkan. Lowering it keeps distant textures sharper than the game intended. The list matches Mip Floor. A forced bound that would cross the value the other field holds is dropped, with a line in the log.", "options": (DEFAULT_VALUE,), }, }, "Rendering": { "sample_shading": { "section": "rendering", + "step": 0.1, "label": "Sample Shading", - "description": "Shade at sample rate inside MSAA render targets to reduce shimmer. The value is the smallest fraction of samples shaded, and off counts as zero. The list runs in steps of 0.2. This one needs the sampleRateShading device feature, and volt never enables a feature the game left off: where the game did not ask for it the card holds nothing but default and the layer logs a line saying so. Most modern renderers are deferred and never ask, so expect this card to be empty more often than not. Only does something in a game already using MSAA.", + "description": "Shade at sample rate inside MSAA render targets to reduce shimmer. The value is the smallest fraction of samples shaded, and off counts as zero. volt never enables the feature: most modern renderers are deferred and never ask, and where the game left it off the setting is ignored and a line is logged.", "options": (DEFAULT_VALUE,), }, "alpha_to_coverage": { "section": "rendering", "label": "Alpha To Coverage", - "description": "Turn fragment alpha into coverage, which softens cutout edges on foliage and fences. Core Vulkan, so the list never changes. Only does something where the game already renders to an MSAA target.", - "options": (DEFAULT_VALUE, "on", "off"), + "description": "Turn fragment alpha to coverage off, whatever the game asked for. volt never forces it on: on requires the fragment shader to write alpha at location 0 and volt never reads a shader. Core Vulkan, so the list never changes. Only does something where the game already renders to an MSAA target.", + "options": (DEFAULT_VALUE, "off"), + }, + "alpha_to_one": { + "section": "rendering", + "label": "Alpha To One", + "description": "Force fragment alpha to 1 after the shader runs. volt never enables the feature: where the game left it off the setting is ignored and a line is logged. Only does something where the game already renders to an MSAA target.", + "options": (DEFAULT_VALUE,), + }, + "depth_clamp": { + "section": "rendering", + "label": "Depth Clamp", + "description": "Keep fragments outside the near and far planes and pin their depth to the plane instead of discarding them. Stops weapon models being sliced open when the camera backs into a wall. The same toggle covers the far plane, where distant geometry flattens onto it instead of disappearing, which can look worse, so try it per game. volt never enables the feature, and most games leave it off, so expect this to do nothing in most of them. Run with VOLT_LOG=info to see which case you are in.", + "options": (DEFAULT_VALUE,), }, }, } @@ -162,16 +179,23 @@ "options": (DEFAULT_VALUE, "cachyos", "amd", "intel", "nvidia"), "fallback": "cachyos", }, + "qt_platform": { + "label": "Display Backend Preference", + "description": "Which Qt platform plugin the interface prefers. default lets Qt pick: wayland on a Wayland session, xcb on X11. Either choice falls back to the other where the one you pick is unavailable, so xcb on a session without XWayland still opens a window. Takes effect on program restart.", + "options": (DEFAULT_VALUE, "xcb", "wayland"), + "fallback": "", + }, "window_transparency": { "label": "Window Transparency", - "description": "Window background transparency. default is off. Takes effect on program restart.", + "description": "Window background transparency. default is off. Only does something under xcb: Wayland has no window opacity protocol, so set Display Backend Preference to xcb for this to land. Takes effect on program restart.", "options": (DEFAULT_VALUE, "on", "off"), "fallback": "off", }, "interface_scale_factor": { "label": "Interface Scale Factor", - "description": "UI scaling multiplier, in steps of 0.2. default is 1.0. Takes effect on program restart.", - "options": (DEFAULT_VALUE, "0.6", "0.8", "1.0", "1.2", "1.4", "1.6", "1.8", "2.0", "2.2", "2.4", "2.6", "2.8", "3.0"), + "description": "UI scaling multiplier, in steps of 0.1. default is 1.0. Takes effect on program restart.", + "step": 0.1, + "options": (DEFAULT_VALUE,), "fallback": "1.0", }, "start_window_maximized": { @@ -198,58 +222,83 @@ "options": (DEFAULT_VALUE, "on", "off"), "fallback": "on", }, - "automatic_update_check": { - "label": "Automatic Update Check", - "description": "Check for updates on startup. default is off. Takes effect on program restart.", - "options": (DEFAULT_VALUE, "on", "off"), - "fallback": "off", - }, -} - -def find_settings_for_tab(tab_name: str) -> dict: - return SETTINGS_DB.get(tab_name, {}) +} -def get_setting_label(tab_name: str, setting_key: str) -> str: - return SETTINGS_DB[tab_name][setting_key]["label"] +OPTION_BUILDERS: Final[dict] = { + "device": gpu_options, + "present_mode": present_options, + "composite_alpha": alpha_options, + "alpha_to_one": alpha_one_options, + "depth_clamp": clamp_options, +} -def get_setting_description(tab_name: str, setting_key: str) -> str: - return SETTINGS_DB[tab_name][setting_key]["description"] +def _frame_limit_options(data: tuple, step: float) -> tuple: + return frametime_pairs(stepped_values(FRAME_LIMIT_FIRST, FRAME_LIMIT_LAST, step)) -OPTION_BUILDERS: Final[dict] = { - "device": gpu_options, - "present_mode": present_options, +STEPPED_BUILDERS: Final[dict] = { "image_count": image_count_options, - "color_depth": depth_options, - "color_space": space_options, - "transfer_function": transfer_options, - "composite_alpha": alpha_options, "anisotropy": aniso_options, "lod_bias": lod_bias_options, "mip_floor": mip_options, "mip_ceiling": mip_options, "sample_shading": shading_options, - "frame_limit": lambda _: frametime_pairs( - SETTINGS_DB["Framerate"]["frame_limit"]["options"][1:]), + "frame_limit": _frame_limit_options, } +def _scale_options(step: float) -> tuple: + return stepped_values(SCALE_LOW, SCALE_HIGH, step) + + +STEPPED_OPTIONS: Final[dict] = { + "interface_scale_factor": _scale_options, +} + + +ACCENT_COLORS: Final[dict] = { + "amd": ("#E31937", "#FF2D4A", "#B81430"), + "intel": ("#0068B5", "#1A8CFF", "#004D87"), + "nvidia": ("#76B900", "#8ED11A", "#5A8F00"), +} +DEFAULT_ACCENT: Final[tuple] = ("#80dbcb", "#9ae4d8", "#66b0a2") + + +def find_settings_for_tab(tab_name: str) -> dict: + return SETTINGS_DB.get(tab_name, {}) + + +def get_setting_label(tab_name: str, setting_key: str) -> str: + return SETTINGS_DB[tab_name][setting_key]["label"] + + +def get_setting_description(tab_name: str, setting_key: str) -> str: + return SETTINGS_DB[tab_name][setting_key]["description"] + + def _static_options(tab_name: str, setting_key: str) -> tuple: return plain_pairs(SETTINGS_DB[tab_name][setting_key]["options"]) +def get_setting_step(tab_name: str, setting_key: str) -> float: + return SETTINGS_DB[tab_name][setting_key]["step"] + + def find_setting_options(tab_name: str, setting_key: str, data: dict) -> tuple: - match OPTION_BUILDERS.get(setting_key): - case None: + match (OPTION_BUILDERS.get(setting_key), STEPPED_BUILDERS.get(setting_key)): + case (None, None): return _static_options(tab_name, setting_key) - case builder: + case (None, stepped): + return ((DEFAULT_VALUE, DEFAULT_VALUE),) + stepped( + data, get_setting_step(tab_name, setting_key)) + case (builder, _): return ((DEFAULT_VALUE, DEFAULT_VALUE),) + builder(data) -def get_setting_options(tab_name: str, setting_key: str) -> tuple: +def call_setting_options(tab_name: str, setting_key: str) -> tuple: return find_setting_options(tab_name, setting_key, call_read_probe()) @@ -261,12 +310,12 @@ def build_widget_key(tab_name: str, setting_key: str) -> str: return tab_name + ":" + setting_key -def find_cards_for_tab(tab_name: str) -> tuple: +def call_cards_for_tab(tab_name: str) -> tuple: return tuple( (build_widget_key(tab_name, setting_key), get_setting_label(tab_name, setting_key), get_setting_description(tab_name, setting_key), - get_setting_options(tab_name, setting_key)) + call_setting_options(tab_name, setting_key)) for setting_key in find_settings_for_tab(tab_name)) @@ -277,7 +326,7 @@ def _tab_option_sources(tab_name: str, data: dict) -> tuple: for setting_key in find_settings_for_tab(tab_name)) -def find_option_sources() -> tuple: +def call_option_sources() -> tuple: data = call_read_probe() return tuple( entry @@ -302,8 +351,17 @@ def get_option_description(option_key: str) -> str: return OPTIONS_DB[option_key]["description"] +def get_option_step(option_key: str) -> float: + return OPTIONS_DB[option_key]["step"] + + def get_option_options(option_key: str) -> tuple: - return plain_pairs(OPTIONS_DB[option_key]["options"]) + match STEPPED_OPTIONS.get(option_key): + case None: + return plain_pairs(OPTIONS_DB[option_key]["options"]) + case stepped: + return plain_pairs( + OPTIONS_DB[option_key]["options"] + stepped(get_option_step(option_key))) def get_option_default_value(option_key: str) -> str: @@ -327,15 +385,7 @@ def resolve_option_value(option_key: str, raw_value: str) -> str: def get_accent_colors(theme_name: str) -> tuple: - match theme_name: - case "amd": - return ("#E31937", "#FF2D4A", "#B81430") - case "intel": - return ("#0068B5", "#1A8CFF", "#004D87") - case "nvidia": - return ("#76B900", "#8ED11A", "#5A8F00") - case _: - return ("#80dbcb", "#9ae4d8", "#66b0a2") + return ACCENT_COLORS.get(theme_name, DEFAULT_ACCENT) def get_about_data() -> dict: diff --git a/volt-gui/presets.py b/src/volt-gui/presets.py similarity index 73% rename from volt-gui/presets.py rename to src/volt-gui/presets.py index 748f555..88916ef 100644 --- a/volt-gui/presets.py +++ b/src/volt-gui/presets.py @@ -1,3 +1,5 @@ +from functools import partial +from typing import Any from typing import Final from database import DEFAULT_VALUE @@ -13,21 +15,21 @@ "Quality": { "Display:present_mode": "fifo", "Display:image_count": "4", - "Display:color_depth": "10bit", "Framerate:frame_limit_method": "early", "Framerate:frame_pacing": "precise", - "Textures:filtering": "trilinear", + "Textures:mag_filter": "linear", + "Textures:min_filter": "linear", "Textures:mipmap_mode": "linear", "Textures:anisotropy": "16", "Textures:lod_bias": "-0.6", "Textures:mip_floor": "0", - "Rendering:alpha_to_coverage": "on", }, "Balanced": { "Display:present_mode": "mailbox", "Framerate:frame_limit_method": "early", "Framerate:frame_pacing": "sliced", - "Textures:filtering": "trilinear", + "Textures:mag_filter": "linear", + "Textures:min_filter": "linear", "Textures:mipmap_mode": "linear", "Textures:anisotropy": "8", "Textures:mip_floor": "0", @@ -35,32 +37,32 @@ "Performance FPS": { "Display:present_mode": "mailbox", "Display:image_count": "4", - "Display:color_depth": "8bit", "Framerate:frame_limit_method": "early", "Framerate:frame_pacing": "sleep", - "Textures:filtering": "bilinear", - "Textures:mipmap_mode": "linear", + "Textures:mag_filter": "linear", + "Textures:min_filter": "linear", + "Textures:mipmap_mode": "nearest", "Textures:anisotropy": "4", "Textures:lod_bias": "0.6", }, "Performance Low Latency": { "Display:present_mode": "immediate", "Display:image_count": "2", - "Display:color_depth": "8bit", "Framerate:frame_limit_method": "late", "Framerate:frame_pacing": "spin", - "Textures:filtering": "bilinear", - "Textures:mipmap_mode": "linear", + "Textures:mag_filter": "linear", + "Textures:min_filter": "linear", + "Textures:mipmap_mode": "nearest", "Textures:anisotropy": "4", "Textures:lod_bias": "0.6", }, "Potato FPS": { "Display:present_mode": "mailbox", "Display:image_count": "4", - "Display:color_depth": "8bit", "Framerate:frame_limit_method": "early", "Framerate:frame_pacing": "sleep", - "Textures:filtering": "bilinear", + "Textures:mag_filter": "linear", + "Textures:min_filter": "linear", "Textures:mipmap_mode": "nearest", "Textures:anisotropy": "off", "Textures:lod_bias": "1.0", @@ -70,10 +72,10 @@ "Potato Low Latency": { "Display:present_mode": "immediate", "Display:image_count": "2", - "Display:color_depth": "8bit", "Framerate:frame_limit_method": "late", "Framerate:frame_pacing": "sleep", - "Textures:filtering": "bilinear", + "Textures:mag_filter": "linear", + "Textures:min_filter": "linear", "Textures:mipmap_mode": "nearest", "Textures:anisotropy": "off", "Textures:lod_bias": "1.0", @@ -101,23 +103,28 @@ def build_preset_values(preset_name: str) -> dict: **PRESET_OVERRIDES.get(preset_name, {})} -def build_preset_combo_items(combo_widget) -> None: +def process_preset_combo_items(combo_widget: Any) -> None: combo_widget.blockSignals(True) combo_widget.clear() - combo_widget.addItem(get_preset_placeholder_label()) - combo_widget.insertSeparator(combo_widget.count()) + combo_widget.addItem(get_preset_placeholder_label(), get_preset_placeholder_label()) for preset_name in get_preset_names(): - combo_widget.addItem(preset_name) + combo_widget.addItem(preset_name, preset_name) combo_widget.blockSignals(False) return None +def _widget_dropped(widget_collection: dict, item: tuple) -> bool: + widget_key, setting_value = item + match widget_collection.get(widget_key): + case None: + return False + case widget: + return not process_widget_value_update(widget, setting_value) + + def _preset_dropped(widget_collection: dict, values: dict) -> tuple: - return tuple( - widget_key - for widget_key, setting_value in values.items() - if widget_collection.get(widget_key) is not None - and not process_widget_value_update(widget_collection[widget_key], setting_value)) + dropped = filter(partial(_widget_dropped, widget_collection), values.items()) + return tuple(widget_key for widget_key, _ in dropped) def process_preset_apply(widget_collection: dict, preset_name: str) -> tuple: diff --git a/src/volt-gui/probe.py b/src/volt-gui/probe.py new file mode 100644 index 0000000..770d125 --- /dev/null +++ b/src/volt-gui/probe.py @@ -0,0 +1,307 @@ +import os + +from functools import partial +from functools import reduce +from math import ceil +from math import floor +from pathlib import Path +from typing import Final +from typing import Optional + +PROBE_FILE: Final[str] = "probe.toml" +PROBE_SEP: Final[str] = ";" +PROBE_ON: Final[str] = "on" +PAIR_SEP: Final[str] = "=" +DEVICE_SECTION: Final[str] = "probe" +TAG_OPEN: Final[str] = " (" +TAG_SEP: Final[str] = ", " +TAG_CLOSE: Final[str] = ")" + +MS_PER_S: Final[float] = 1000.0 +FRAMETIME_DIGITS: Final[int] = 1 +ANISO_FIRST: Final[int] = 2 +UNIT_EPSILON: Final[float] = 1e-9 +STEP_DIGITS: Final[int] = 6 +COUNT_SPAN: Final[int] = 6 +BIAS_CEILING: Final[float] = 4.0 +SHADING_CEILING: Final[float] = 1.0 +OFF_VALUE: Final[str] = "off" +TOGGLE_VALUES: Final[tuple] = ("off", "on") + + +def build_probe_path() -> Path: + return Path(os.path.expanduser("~/.config/volt-gui")) / PROBE_FILE + + +def _classify_line(line: str) -> tuple: + match (line.startswith("["), PAIR_SEP in line, line.startswith("#"), line): + case (_, _, True, _) | (_, _, _, ""): + return ("skip",) + case (True, _, _, _): + return ("section", line.strip("[]").strip()) + case (False, True, _, _): + return ("pair", + line.split(PAIR_SEP, 1)[0].strip(), + line.split(PAIR_SEP, 1)[1].strip().strip('"')) + case _: + return ("skip",) + + +def _with_section(sections: tuple, name: str) -> tuple: + return sections + ((name, {}),) + + +def _with_pair(sections: tuple, key: str, value: str) -> tuple: + match sections: + case (): + return () + case _: + return sections[:-1] + ((sections[-1][0], {**sections[-1][1], key: value}),) + + +def _fold_line(state: tuple, line: str) -> tuple: + match _classify_line(line.strip()): + case ("section", name): + return _with_section(state, name) + case ("pair", key, value): + return _with_pair(state, key, value) + case _: + return state + + +def parse_probe_text(text: str) -> tuple: + return reduce(_fold_line, text.splitlines(), ()) + + +def call_read_probe() -> tuple: + match build_probe_path().exists(): + case False: + return () + case True: + return parse_probe_text(build_probe_path().read_text(encoding="utf-8")) + + +def call_probe_stamp() -> float: + match build_probe_path().exists(): + case False: + return 0.0 + case True: + return build_probe_path().stat().st_mtime + + +def probe_device(data: tuple) -> dict: + return next((values for name, values in data if name == DEVICE_SECTION), {}) + + +def probe_surfaces(data: tuple) -> tuple: + return tuple((name, values) for name, values in data if name != DEVICE_SECTION) + + +def probe_text(values: dict, key: str) -> str: + return values.get(key, "") + + +def _is_number(text: str) -> bool: + return text.replace("-", "", 1).replace(".", "", 1).isdigit() + + +def probe_number(values: dict, key: str) -> Optional[float]: + match _is_number(probe_text(values, key)): + case True: + return float(probe_text(values, key)) + case False: + return None + + +def probe_flag(values: dict, key: str) -> bool: + return probe_text(values, key) == PROBE_ON + + +def probe_list(values: dict, key: str) -> tuple: + return tuple(v.lower() for v in probe_text(values, key).split(PROBE_SEP) if v != "") + + +def plain_pairs(values: tuple) -> tuple: + return tuple((v, v) for v in values) + + +def _tag_label(value: str, tags: tuple) -> str: + return value + TAG_OPEN + TAG_SEP.join(tags) + TAG_CLOSE + + +def _carries(value: str, source: tuple) -> bool: + return value in source[1] + + +def _tags_for(value: str, sources: tuple) -> tuple: + return tuple(name for name, _ in filter(partial(_carries, value), sources)) + + +def _ordered_values(sources: tuple) -> tuple: + return tuple(dict.fromkeys(value for _, values in sources for value in values)) + + +def _tagged(values: tuple, sources: tuple) -> tuple: + return tuple( + (value, _tag_label(value, _tags_for(value, sources))) + for value in values) + + +def tagged_pairs(sources: tuple) -> tuple: + return _tagged(_ordered_values(sources), sources) + + +def tagged_count_pairs(sources: tuple) -> tuple: + return _tagged(tuple(sorted(_ordered_values(sources), key=int)), sources) + + +def _frametime_label(fps: str) -> str: + return fps + " (" + str(round(MS_PER_S / float(fps), FRAMETIME_DIGITS)) + "ms)" + + +def frametime_pairs(values: tuple) -> tuple: + return tuple((v, _frametime_label(v)) for v in values) + + +def _low_units(low: float, step: float) -> int: + return ceil(low / step - UNIT_EPSILON) + + +def _high_units(high: float, step: float) -> int: + return floor(high / step + UNIT_EPSILON) + + +def _step_text(units: int, step: float) -> str: + match step: + case int(): + return str(units * step) + case _: + return str(round(units * step, STEP_DIGITS)) + + +def stepped_values(low: float, high: float, step: float) -> tuple: + return tuple( + _step_text(units, step) + for units in range(_low_units(low, step), _high_units(high, step) + 1)) + + +def _non_empty(entry: tuple) -> bool: + return entry[1] != () + + +def _surface_lists(data: tuple, key: str) -> tuple: + return tuple(filter(_non_empty, ( + (n, probe_list(v, key)) for n, v in probe_surfaces(data)))) + + +def present_options(data: tuple) -> tuple: + return tagged_pairs(_surface_lists(data, "present_modes")) + + +def alpha_options(data: tuple) -> tuple: + return tagged_pairs(_surface_lists(data, "composite_alphas")) + + +def gpu_options(data: tuple) -> tuple: + return tuple( + (str(at + 1), name) + for at, name in enumerate(probe_list(probe_device(data), "device_names"))) + + +def _aniso_ladder(limit: Optional[float], step: float) -> tuple: + match limit: + case None: + return () + case value: + return ((OFF_VALUE, OFF_VALUE),) + plain_pairs( + stepped_values(ANISO_FIRST, value, step)) + + +def aniso_options(data: tuple, step: float) -> tuple: + match probe_flag(probe_device(data), "sampler_anisotropy"): + case False: + return () + case True: + return _aniso_ladder(probe_number(probe_device(data), "max_anisotropy"), step) + + +def _toggle_ladder(held: bool) -> tuple: + match held: + case False: + return () + case True: + return plain_pairs(TOGGLE_VALUES) + + +def alpha_one_options(data: tuple) -> tuple: + return _toggle_ladder(probe_flag(probe_device(data), "alpha_to_one")) + + +def clamp_options(data: tuple) -> tuple: + return _toggle_ladder(probe_flag(probe_device(data), "depth_clamp")) + + +def shading_options(data: tuple, step: float) -> tuple: + match probe_flag(probe_device(data), "sample_rate_shading"): + case False: + return () + case True: + return ((OFF_VALUE, OFF_VALUE),) + plain_pairs( + stepped_values(step, SHADING_CEILING, step)) + + +def _count_ceiling(low: int, high: int) -> int: + match high: + case 0: + return low + COUNT_SPAN + case _: + return high + + +def _count_values(low: Optional[float], high: Optional[float], step: float) -> tuple: + match (low, high): + case (None, _) | (_, None): + return () + case (start, stop): + return stepped_values(int(start), _count_ceiling(int(start), int(stop)), step) + + +def _count_sources(data: tuple, step: float) -> tuple: + return tuple(filter(_non_empty, ( + (n, _count_values( + probe_number(v, "min_image_count"), + probe_number(v, "max_image_count"), + step)) + for n, v in probe_surfaces(data)))) + + +def image_count_options(data: tuple, step: float) -> tuple: + return tagged_count_pairs(_count_sources(data, step)) + + +def _mip_values(limit: Optional[float], step: float) -> tuple: + match limit: + case None: + return () + case value: + return plain_pairs(stepped_values(0, value, step)) + + +def mip_options(data: tuple, step: float) -> tuple: + return _mip_values(probe_number(probe_device(data), "max_lod_level"), step) + + +def _bias_ladder(span: float, step: float) -> tuple: + return plain_pairs(stepped_values(-span, span, step)) + + +def _bias_values(limit: Optional[float], step: float) -> tuple: + match limit: + case None: + return () + case value: + return _bias_ladder(min(value, BIAS_CEILING), step) + + +def lod_bias_options(data: tuple, step: float) -> tuple: + return _bias_values(probe_number(probe_device(data), "max_lod_bias"), step) diff --git a/volt-gui/profiles.py b/src/volt-gui/profiles.py similarity index 73% rename from volt-gui/profiles.py rename to src/volt-gui/profiles.py index 08b0863..3954033 100644 --- a/volt-gui/profiles.py +++ b/src/volt-gui/profiles.py @@ -1,19 +1,26 @@ import os +from functools import partial from functools import reduce from pathlib import Path +from typing import Any from typing import Final from typing import Optional from database import DEFAULT_PROFILE from database import DEFAULT_VALUE -from database import find_option_sources +from database import call_option_sources from database import find_profile_fields +from probe import PROBE_FILE SECTION_ORDER: Final[tuple] = ("gpu", "display", "textures", "rendering", "framerate") OPTIONS_FILE: Final[str] = "options.toml" PROFILE_SUFFIX: Final[str] = ".toml" PAIR_SEP: Final[str] = " = " +RESERVED_STEMS: Final[tuple] = ( + DEFAULT_PROFILE, + OPTIONS_FILE.removesuffix(PROFILE_SUFFIX), + PROBE_FILE.removesuffix(PROFILE_SUFFIX)) def build_config_dir() -> Path: @@ -28,22 +35,21 @@ def build_options_path() -> Path: return build_config_dir() / OPTIONS_FILE +def is_reserved_profile_name(profile_name: str) -> bool: + return profile_name.strip().lower() in RESERVED_STEMS + + def is_profile_file(file_path: Path) -> bool: - match file_path.name == OPTIONS_FILE: - case True: - return False - case False: - return file_path.stem.lower() != DEFAULT_PROFILE + return not is_reserved_profile_name(file_path.stem) -def find_all_profiles() -> tuple: +def call_all_profiles() -> tuple: match build_config_dir().exists(): case False: return (DEFAULT_PROFILE,) case True: - return (DEFAULT_PROFILE,) + tuple(sorted( - path.stem for path in build_config_dir().glob("*" + PROFILE_SUFFIX) - if is_profile_file(path))) + found = filter(is_profile_file, build_config_dir().glob("*" + PROFILE_SUFFIX)) + return (DEFAULT_PROFILE,) + tuple(sorted(path.stem for path in found)) def _quoted(value: str) -> str: @@ -55,11 +61,15 @@ def _section_lines(section: str, pairs: tuple) -> tuple: key + PAIR_SEP + _quoted(value) for key, value in pairs) + ("",) +def _in_section(section: str, field: tuple) -> bool: + return field[1] == section + + def _pairs_for_section(values: dict, section: str) -> tuple: + kept = filter(partial(_in_section, section), find_profile_fields()) return tuple( (config_key, values.get(widget_key, DEFAULT_VALUE)) - for widget_key, field_section, config_key in find_profile_fields() - if field_section == section) + for widget_key, _, config_key in kept) def serialize_profile(values: dict) -> str: @@ -95,15 +105,16 @@ def parse_profile_text(text: str) -> dict: return dict(reduce(_fold_line, text.splitlines(), ("", ()))[1]) +def _named(section_key: str, field: tuple) -> bool: + return field[1] + "." + field[2] == section_key + + def _widget_key_for(section_key: str) -> Optional[str]: - return next( - (widget_key - for widget_key, section, config_key in find_profile_fields() - if section + "." + config_key == section_key), - None) + named = filter(partial(_named, section_key), find_profile_fields()) + return next((widget_key for widget_key, _, _ in named), None) -def widget_value(widget) -> str: +def widget_value(widget: Any) -> str: match widget.currentData(): case None: return DEFAULT_VALUE @@ -111,7 +122,7 @@ def widget_value(widget) -> str: return data -def process_widget_value_update(widget, display_value: str) -> bool: +def process_widget_value_update(widget: Any, display_value: str) -> bool: match widget.findData(display_value): case -1: widget.setCurrentIndex(0) @@ -121,7 +132,7 @@ def process_widget_value_update(widget, display_value: str) -> bool: return True -def process_widget_options_rebuild(widget, options: tuple) -> None: +def process_widget_options_rebuild(widget: Any, options: tuple) -> None: keep = widget_value(widget) widget.clear() for value, label in options: @@ -151,7 +162,7 @@ def process_profile_widgets_reset(widget_collection: dict) -> None: def process_profile_options_rebuild(widget_collection: dict) -> None: - for widget_key, options in find_option_sources(): + for widget_key, options in call_option_sources(): match widget_collection.get(widget_key): case None: continue @@ -160,11 +171,15 @@ def process_profile_options_rebuild(widget_collection: dict) -> None: return None +def _widget_held(widget_collection: dict, field: tuple) -> bool: + return widget_collection.get(field[0]) is not None + + def collect_widget_values(widget_collection: dict) -> dict: return { widget_key: widget_value(widget_collection[widget_key]) - for widget_key, _, _ in find_profile_fields() - if widget_collection.get(widget_key) is not None} + for widget_key, _, _ in filter( + partial(_widget_held, widget_collection), find_profile_fields())} def call_read_profile(profile_name: str) -> dict: @@ -175,7 +190,7 @@ def call_read_profile(profile_name: str) -> dict: return parse_profile_text(build_profile_path(profile_name).read_text(encoding="utf-8")) -def _apply_to_widget(widget_collection: dict, widget_key: str, value: str) -> bool: +def _process_widget_value(widget_collection: dict, widget_key: str, value: str) -> bool: match widget_collection.get(widget_key): case None: return True @@ -183,23 +198,27 @@ def _apply_to_widget(widget_collection: dict, widget_key: str, value: str) -> bo return process_widget_value_update(widget, value) -def _apply_one(widget_collection: dict, section_key: str, value: str) -> bool: +def _process_parsed_value(widget_collection: dict, section_key: str, value: str) -> bool: match _widget_key_for(section_key): case None: return True case widget_key: - return _apply_to_widget(widget_collection, widget_key, value) + return _process_widget_value(widget_collection, widget_key, value) def _kept(value: str) -> bool: return value == DEFAULT_VALUE +def _applied(widget_collection: dict, item: tuple) -> bool: + section_key, value = item + return not _kept(value) and not _process_parsed_value(widget_collection, section_key, value) + + def _apply_parsed(widget_collection: dict, parsed: dict) -> tuple: return tuple( section_key - for section_key, value in parsed.items() - if not _kept(value) and not _apply_one(widget_collection, section_key, value)) + for section_key, _ in filter(partial(_applied, widget_collection), parsed.items())) def process_profile_widget_load(widget_collection: dict, profile_name: str) -> tuple: diff --git a/volt-gui/themes.py b/src/volt-gui/themes.py similarity index 62% rename from volt-gui/themes.py rename to src/volt-gui/themes.py index 50530de..c4aa65a 100644 --- a/volt-gui/themes.py +++ b/src/volt-gui/themes.py @@ -1,8 +1,27 @@ +from typing import Final +from typing import Optional + from PySide6.QtGui import QColor from PySide6.QtGui import QPalette +from PySide6.QtWidgets import QApplication from database import get_accent_colors +STANDARD_BUTTON_WIDTH: Final[int] = 90 +STANDARD_BUTTON_HEIGHT: Final[int] = 36 +SLIDER_HANDLE_WIDTH: Final[int] = 14 +SLIDER_GROOVE_HEIGHT: Final[int] = 6 +BASE_COLORS: Final[dict] = { + "background": "#161616", + "background_darker": "#0e0e0e", + "background_lighter": "#262626", + "surface": "#1e1e1e", + "text_primary": "#E8E8E8", + "text_secondary": "#9A9A9A", + "text_disabled": "#444444", + "card_background": "#1a1a1a", +} + def get_style_palette_roles() -> tuple: return ( @@ -32,14 +51,6 @@ def get_style_palette_disabled_roles() -> tuple: ) -def get_standard_button_width() -> int: - return 90 - - -def get_standard_button_height() -> int: - return 36 - - def get_style_stylesheet_template() -> str: return """ QWidget {{ background-color: {background}; color: {text_primary}; font-size: 10pt; font-family: "Segoe UI", "SF Pro Display", sans-serif; border: none; }} @@ -50,58 +61,62 @@ def get_style_stylesheet_template() -> str: QWidget[buttonContainer="true"] {{ background-color: {background}; }} QMainWindow {{ background-color: {background}; border: none; }} QScrollBar:vertical {{ background-color: {background}; width: 6px; margin: 0px; border: none; }} -QScrollBar::handle:vertical {{ background-color: {surface}; min-height: 40px; margin: 2px; border-radius: 3px; }} +QScrollBar::handle:vertical {{ background-color: {surface}; min-height: 40px; margin: 2px; }} QScrollBar::handle:vertical:hover {{ background-color: {accent}; }} QScrollBar::handle:vertical:pressed {{ background-color: {accent_pressed}; }} QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ background: none; height: 0px; border: none; }} QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{ background: transparent; }} -QPushButton {{ background-color: {surface}; color: {text_primary}; border: none; border-left: 3px solid transparent; outline: none; padding: 0px 14px; font-weight: 500; border-radius: 6px; }} -QPushButton:disabled {{ background-color: {background_darker}; color: {text_disabled}; border: none; border-left: 3px solid transparent; border-radius: 6px; }} -QPushButton:hover {{ background-color: {background_lighter}; color: {text_primary}; border: none; border-left: 3px solid {accent}; border-radius: 6px; }} -QPushButton:pressed {{ background-color: {accent_pressed}; color: white; border: none; border-left: 3px solid {accent}; border-radius: 6px; }} -QLineEdit {{ background-color: {surface}; color: {text_primary}; border: none; border-left: 3px solid transparent; padding: 0px 12px; selection-background-color: {accent}; border-radius: 6px; }} -QLineEdit:hover {{ background-color: {background_lighter}; border: none; border-left: 3px solid {accent}; border-radius: 6px; }} -QLineEdit:focus {{ background-color: {background_lighter}; border: none; border-left: 3px solid {accent}; border-radius: 6px; }} -QLineEdit:disabled {{ background-color: {background_darker}; color: {text_disabled}; border: none; border-left: 3px solid transparent; border-radius: 6px; }} -QComboBox {{ background-color: {surface}; color: {text_primary}; border: none; border-left: 3px solid transparent; padding: 0px 12px; selection-background-color: transparent; selection-color: {text_primary}; min-width: 60px; border-radius: 6px; }} -QComboBox:hover {{ background-color: {background_lighter}; border: none; border-left: 3px solid {accent}; border-radius: 6px; }} -QComboBox:focus {{ background-color: {background_lighter}; border: none; border-left: 3px solid {accent}; border-radius: 6px; }} -QComboBox:on {{ background-color: {background_lighter}; border: none; border-left: 3px solid {accent}; border-radius: 6px; }} -QComboBox:disabled {{ background-color: {background_darker}; color: {text_disabled}; border: none; border-left: 3px solid transparent; border-radius: 6px; }} -QComboBox::drop-down {{ subcontrol-origin: padding; subcontrol-position: top right; width: 20px; border: none; background-color: transparent; }} -QComboBox QAbstractItemView {{ background-color: {background}; color: {text_primary}; border: none; border-left: 3px solid {accent}; outline: none; padding: 4px 0px; selection-background-color: {background_lighter}; selection-color: {text_primary}; border-radius: 6px; }} -QComboBox QAbstractItemView::item {{ padding: 6px 12px; border: none; margin: 0px; background-color: {background}; }} -QComboBox QAbstractItemView::item:hover {{ background-color: {surface}; color: {text_primary}; }} -QComboBox QAbstractItemView::item:selected {{ background-color: {background_lighter}; color: {text_primary}; }} -QMenu {{ background-color: {background}; color: {text_primary}; border: none; border-left: 3px solid {accent}; padding: 6px; border-radius: 6px; }} -QMenu::item {{ padding: 6px 20px; border: none; border-left: 3px solid transparent; margin: 1px 0px; border-radius: 4px; }} +QPushButton {{ background-color: {surface}; color: {text_primary}; border: none; border-left: 3px solid transparent; outline: none; padding: 0px 14px; font-weight: 500; }} +QPushButton:disabled {{ background-color: {background_darker}; color: {text_disabled}; border: none; border-left: 3px solid transparent; }} +QPushButton:hover {{ background-color: {background_lighter}; color: {text_primary}; border: none; border-left: 3px solid {accent}; }} +QPushButton:pressed {{ background-color: {accent_pressed}; color: white; border: none; border-left: 3px solid {accent}; }} +QLineEdit {{ background-color: {surface}; color: {text_primary}; border: none; border-left: 3px solid transparent; padding: 0px 12px; selection-background-color: {accent}; }} +QLineEdit:hover {{ background-color: {background_lighter}; border: none; border-left: 3px solid {accent}; }} +QLineEdit:focus {{ background-color: {background_lighter}; border: none; border-left: 3px solid {accent}; }} +QLineEdit:disabled {{ background-color: {background_darker}; color: {text_disabled}; border: none; border-left: 3px solid transparent; }} +QMenu {{ background-color: {background}; color: {text_primary}; border: none; border-left: 3px solid {accent}; padding: 6px; }} +QMenu::item {{ padding: 6px 20px; border: none; border-left: 3px solid transparent; margin: 1px 0px; }} QMenu::item:selected {{ background-color: {surface}; color: {text_primary}; border: none; border-left: 3px solid {accent}; }} QMenu::separator {{ height: 1px; background-color: {surface}; margin: 6px 0px; }} -QToolTip {{ background-color: {background}; color: {text_primary}; border: none; border-left: 3px solid {accent}; padding: 8px 12px; font-size: 10pt; border-radius: 6px; }} +QToolTip {{ background-color: {background}; color: {text_primary}; border: none; border-left: 3px solid {accent}; padding: 8px 12px; font-size: 10pt; }} QListWidget {{ background-color: {background}; border: none; outline: none; padding: 0px; }} -QListWidget::item {{ background-color: transparent; color: {text_secondary}; border: none; border-left: 3px solid transparent; padding: 8px 14px; margin: 1px 4px; border-radius: 6px; }} +QListWidget::item {{ background-color: transparent; color: {text_secondary}; border: none; border-left: 3px solid transparent; padding: 8px 14px; margin: 1px 4px; }} QListWidget::item:selected {{ background-color: {surface}; color: {text_primary}; border: none; border-left: 3px solid {accent}; }} QListWidget::item:hover:!selected {{ background-color: {surface}; color: {text_primary}; border: none; border-left: 3px solid {accent_hover}; }} -QFrame[settingCard="true"] {{ background-color: {card_background}; border: none; border-left: 3px solid transparent; border-radius: 6px; }} -QFrame[settingCard="true"]:hover {{ border: none; border-left: 3px solid {accent}; border-radius: 6px; }} +QFrame[settingCard="true"] {{ background-color: {card_background}; border: none; border-left: 3px solid transparent; }} +QFrame[settingCard="true"]:hover {{ border: none; border-left: 3px solid {accent}; }} QInputDialog {{ background-color: {background}; }} -QInputDialog QLineEdit {{ background-color: {surface}; color: {text_primary}; border: none; border-left: 3px solid transparent; padding: 0px 12px; min-height: 36px; max-height: 36px; selection-background-color: {accent}; border-radius: 6px; }} -QInputDialog QPushButton {{ min-width: 90px; min-height: 36px; max-height: 36px; padding: 0px 12px; border-radius: 6px; }} +QInputDialog QLineEdit {{ background-color: {surface}; color: {text_primary}; border: none; border-left: 3px solid transparent; padding: 0px 12px; min-height: 36px; max-height: 36px; selection-background-color: {accent}; }} +QInputDialog QPushButton {{ min-width: 90px; min-height: 36px; max-height: 36px; padding: 0px 12px; }} +QDialogButtonBox {{ dialogbuttonbox-buttons-have-icons: 0; }} QMessageBox {{ background-color: {background}; }} -QMessageBox QPushButton {{ min-width: 90px; min-height: 36px; max-height: 36px; padding: 0px 12px; border-radius: 6px; }} +QMessageBox QPushButton {{ min-width: 90px; min-height: 36px; max-height: 36px; padding: 0px 12px; }} +""" + + +def get_style_slider_template() -> str: + return """ +QSlider {{ background-color: transparent; border: none; min-height: {handle_width}px; }} +QSlider::groove:horizontal {{ background-color: {surface}; height: {groove_height}px; border: none; }} +QSlider::sub-page:horizontal {{ background-color: {accent}; border: none; }} +QSlider::add-page:horizontal {{ background-color: {surface}; border: none; }} +QSlider::handle:horizontal {{ background-color: {text_primary}; width: {handle_width}px; margin: -{handle_margin}px 0px; border: none; }} +QSlider::handle:horizontal:hover {{ background-color: {accent_hover}; }} +QSlider::handle:horizontal:pressed {{ background-color: {accent_pressed}; }} """ +def build_slider_metrics() -> dict: + return { + "handle_width": SLIDER_HANDLE_WIDTH, + "handle_margin": (SLIDER_HANDLE_WIDTH - SLIDER_GROOVE_HEIGHT) // 2, + "groove_height": SLIDER_GROOVE_HEIGHT, + } + + def build_theme_colors(theme_name: str) -> dict: return { - "background": "#161616", - "background_darker": "#0e0e0e", - "background_lighter": "#262626", - "surface": "#1e1e1e", - "text_primary": "#E8E8E8", - "text_secondary": "#9A9A9A", - "text_disabled": "#444444", - "card_background": "#1a1a1a", + **BASE_COLORS, "accent": get_accent_colors(theme_name)[0], "accent_hover": get_accent_colors(theme_name)[1], "accent_pressed": get_accent_colors(theme_name)[2], @@ -111,22 +126,24 @@ def build_theme_colors(theme_name: str) -> dict: def build_palette(color_map: dict) -> QPalette: palette_instance = QPalette() for palette_role, color_key in get_style_palette_roles(): - palette_instance.setColor(palette_role, QColor(color_map.get(color_key, "#E8E8E8"))) + palette_instance.setColor(palette_role, QColor(color_map.get(color_key, BASE_COLORS["text_primary"]))) return palette_instance -def apply_disabled_roles(palette_instance: QPalette, color_map: dict) -> QPalette: +def process_disabled_roles(palette_instance: QPalette, color_map: dict) -> QPalette: for palette_role, color_key in get_style_palette_disabled_roles(): palette_instance.setColor(QPalette.Disabled, palette_role, QColor(color_map[color_key])) return palette_instance -def process_theme_application(application_instance, theme_name: str) -> None: +def process_theme_application(application_instance: Optional[QApplication], theme_name: str) -> None: match application_instance: case None: return None case app: color_map = build_theme_colors(theme_name) - app.setStyleSheet(get_style_stylesheet_template().format(**color_map)) - app.setPalette(apply_disabled_roles(build_palette(color_map), color_map)) + app.setStyleSheet( + (get_style_stylesheet_template() + get_style_slider_template()).format( + **color_map, **build_slider_metrics())) + app.setPalette(process_disabled_roles(build_palette(color_map), color_map)) return None diff --git a/volt-gui/ui.py b/src/volt-gui/ui.py similarity index 50% rename from volt-gui/ui.py rename to src/volt-gui/ui.py index 56cd985..98ae72e 100644 --- a/volt-gui/ui.py +++ b/src/volt-gui/ui.py @@ -1,56 +1,204 @@ +from typing import Callable +from typing import Final +from typing import Optional + from PySide6.QtCore import QEasingCurve from PySide6.QtCore import QPropertyAnimation from PySide6.QtCore import Qt from PySide6.QtCore import QTimer +from PySide6.QtCore import Signal +from PySide6.QtGui import QColor from PySide6.QtGui import QCursor from PySide6.QtGui import QFont +from PySide6.QtGui import QPainter +from PySide6.QtGui import QPaintEvent +from PySide6.QtGui import QResizeEvent +from PySide6.QtGui import QWheelEvent from PySide6.QtWidgets import QApplication -from PySide6.QtWidgets import QComboBox from PySide6.QtWidgets import QFrame from PySide6.QtWidgets import QGraphicsOpacityEffect from PySide6.QtWidgets import QHBoxLayout from PySide6.QtWidgets import QLabel -from PySide6.QtWidgets import QListView from PySide6.QtWidgets import QListWidget from PySide6.QtWidgets import QListWidgetItem from PySide6.QtWidgets import QPushButton from PySide6.QtWidgets import QScrollArea from PySide6.QtWidgets import QSizePolicy +from PySide6.QtWidgets import QSlider +from PySide6.QtWidgets import QStackedWidget from PySide6.QtWidgets import QTextEdit from PySide6.QtWidgets import QVBoxLayout from PySide6.QtWidgets import QWidget from database import APP_VERSION -from database import find_cards_for_tab -from themes import get_standard_button_height - - -def get_sidebar_width() -> int: - return 200 - - -def get_header_vertical_margin() -> int: - return 14 - - -def get_copy_button_width() -> int: - return 70 +from database import call_cards_for_tab +from themes import BASE_COLORS +from themes import SLIDER_HANDLE_WIDTH +from themes import STANDARD_BUTTON_HEIGHT + + +SIDEBAR_WIDTH: Final[int] = 200 +HEADER_VERTICAL_MARGIN: Final[int] = 14 +COPY_BUTTON_WIDTH: Final[int] = 70 +WINDOW_MIN_WIDTH: Final[int] = 620 +WINDOW_MIN_HEIGHT: Final[int] = 380 +COPY_RESET_MS: Final[int] = 1000 +COPY_FADE_MS: Final[int] = 200 +SLIDER_SPACING: Final[int] = 2 +SLIDER_PAGE_STEP: Final[int] = 1 +SLIDER_TICK_HEIGHT: Final[int] = 5 +SLIDER_TICK_GAP: Final[int] = 6 +SLIDER_TICK_STRIDES: Final[tuple] = (1, 2, 5, 10, 25, 50, 100, 250, 500, 1000) +SLIDER_TICK_COLOR: Final[str] = "text_disabled" +STYLE_SLIDER_VALUE: Final[str] = "font-weight: 500; font-size: 10pt;" +STYLE_DIVIDER: Final[str] = "QFrame { background-color: #262626; border: none; }" +STYLE_DESCRIPTION: Final[str] = "color: #9A9A9A; font-size: 9pt;" +STYLE_CODE_LABEL: Final[str] = "color: #9A9A9A; font-size: 9pt; margin-top: 4px;" +STYLE_VERSION_LABEL: Final[str] = "font-size: 8pt; color: #9A9A9A; background: transparent;" +STYLE_CODE_EDIT: Final[str] = "QTextEdit { background-color: #1e1e1e; color: #C0C0C0; border: none; border-left: 3px solid transparent; padding: 8px 12px; selection-background-color: #505050; } QTextEdit:hover { border: none; border-left: 3px solid palette(highlight); }" + + +def process_wheel_ignore(wheel_event: QWheelEvent) -> None: + wheel_event.ignore() + return None -def get_combo_minimum_width() -> int: - return 104 +def build_tick_stride(count: int, width: int) -> int: + return next( + (stride for stride in SLIDER_TICK_STRIDES + if stride * (width - SLIDER_HANDLE_WIDTH) >= SLIDER_TICK_GAP * (count - 1)), + SLIDER_TICK_STRIDES[-1]) -def create_combo_widget(options: tuple) -> QComboBox: - combo = QComboBox() - combo.setView(QListView()) - combo.setFixedHeight(get_standard_button_height()) - combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - combo.setFocusPolicy(Qt.ClickFocus) - for value, label in options: - combo.addItem(label, value) - combo.setCurrentIndex(0) - return combo +def build_tick_positions(count: int, width: int) -> tuple: + match count > 1 and width > SLIDER_HANDLE_WIDTH: + case False: + return () + case True: + return tuple( + round(SLIDER_HANDLE_WIDTH // 2 + at * (width - SLIDER_HANDLE_WIDTH) / (count - 1)) + for at in range(0, count, build_tick_stride(count, width))) + + +class StopSlider(QWidget): + currentTextChanged = Signal(str) + + def __init__(self, options: tuple) -> None: + super().__init__() + self.stops = () + self.pressed_index = 0 + self.setProperty("cardRow", True) + self.slider = QSlider(Qt.Horizontal) + self.slider.setPageStep(SLIDER_PAGE_STEP) + self.slider.setFocusPolicy(Qt.NoFocus) + self.slider.wheelEvent = process_wheel_ignore + self.slider.valueChanged.connect(self._process_value_change) + self.slider.sliderPressed.connect(self._process_press) + self.slider.sliderReleased.connect(self._process_release) + self.ticks = QWidget() + self.ticks.setProperty("cardRow", True) + self.ticks.setFixedHeight(SLIDER_TICK_HEIGHT) + self.ticks.paintEvent = self._process_ticks_paint + self.value_label = QLabel() + self.value_label.setStyleSheet(STYLE_SLIDER_VALUE) + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(SLIDER_SPACING) + layout.addWidget(self.slider) + layout.addWidget(self.ticks) + layout.addWidget(self.value_label) + for value, label in options: + self.addItem(label, value) + self.setCurrentIndex(0) + + def _stop_at(self, index: int) -> Optional[tuple]: + match 0 <= index < len(self.stops): + case True: + return self.stops[index] + case False: + return None + + def currentData(self) -> Optional[str]: + match self._stop_at(self.slider.value()): + case None: + return None + case (value, _): + return value + + def currentText(self) -> str: + match self._stop_at(self.slider.value()): + case None: + return "" + case (_, label): + return label + + def findData(self, value: str) -> int: + return next((at for at, stop in enumerate(self.stops) if stop[0] == value), -1) + + def findText(self, text: str) -> int: + return next((at for at, stop in enumerate(self.stops) if stop[1] == text), -1) + + def setCurrentIndex(self, index: int) -> None: + self.slider.setValue(index) + return None + + def setCurrentText(self, text: str) -> None: + match self.findText(text): + case -1: + return None + case index: + self.setCurrentIndex(index) + return None + + def clear(self) -> None: + self.stops = () + self.slider.setRange(0, 0) + self._process_stops_change() + return None + + def addItem(self, label: str, value: str) -> None: + self.stops = self.stops + ((value, label),) + self.slider.setRange(0, len(self.stops) - 1) + self._process_stops_change() + return None + + def _process_stops_change(self) -> None: + self.value_label.setText(self.currentText()) + self.ticks.update() + return None + + def _process_value_change(self, index: int) -> None: + self.value_label.setText(self.currentText()) + match self.slider.isSliderDown(): + case True: + return None + case False: + self.currentTextChanged.emit(self.currentText()) + return None + + def _process_press(self) -> None: + self.pressed_index = self.slider.value() + return None + + def _process_release(self) -> None: + match self.slider.value() == self.pressed_index: + case True: + return None + case False: + self.currentTextChanged.emit(self.currentText()) + return None + + def _process_ticks_paint(self, paint_event: QPaintEvent) -> None: + painter = QPainter(self.ticks) + painter.setPen(QColor(BASE_COLORS[SLIDER_TICK_COLOR])) + for position in build_tick_positions(len(self.stops), self.ticks.width()): + painter.drawLine(position, 0, position, SLIDER_TICK_HEIGHT) + painter.end() + return None + + +def create_slider_widget(options: tuple) -> StopSlider: + return StopSlider(options) def create_divider_widget() -> QFrame: @@ -59,7 +207,7 @@ def create_divider_widget() -> QFrame: divider.setFrameShadow(QFrame.Plain) divider.setFixedHeight(1) divider.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - divider.setStyleSheet("QFrame { background-color: #262626; border: none; }") + divider.setStyleSheet(STYLE_DIVIDER) return divider @@ -76,14 +224,13 @@ def create_setting_card_widget(label_text: str, description_text: str, options: title_label.setStyleSheet("font-weight: 500; font-size: 11pt;") title_label.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) card_layout.addWidget(title_label) - input_widget = create_combo_widget(options) + input_widget = create_slider_widget(options) input_widget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) - input_widget.setMinimumWidth(get_combo_minimum_width()) card_layout.addWidget(input_widget) description_label = QLabel(description_text) description_label.setWordWrap(True) description_label.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum) - description_label.setStyleSheet("color: #585858; font-size: 9pt;") + description_label.setStyleSheet(STYLE_DESCRIPTION) card_layout.addWidget(description_label) return {"card": card, "widget": input_widget} @@ -94,23 +241,23 @@ def build_monospace_font() -> QFont: return monospace_font -def process_copy_button_action(copy_button, clipboard_text: str) -> None: +def process_copy_button_action(copy_button: QPushButton, clipboard_text: str) -> None: QApplication.clipboard().setText(clipboard_text) copy_button.setText("Copied!") effect = QGraphicsOpacityEffect(copy_button) copy_button.setGraphicsEffect(effect) animation = QPropertyAnimation(effect, b"opacity") - animation.setDuration(200) + animation.setDuration(COPY_FADE_MS) animation.setStartValue(0.7) animation.setEndValue(1.0) animation.setEasingCurve(QEasingCurve.OutCubic) animation.start() - QTimer.singleShot(1000, lambda: copy_button.setText("Copy")) + QTimer.singleShot(COPY_RESET_MS, lambda: copy_button.setText("Copy")) return None def build_copy_button_stylesheet(button_width: int, button_height: int) -> str: - return "QPushButton { min-width: " + str(button_width) + "px; max-width: " + str(button_width) + "px; min-height: " + str(button_height) + "px; max-height: " + str(button_height) + "px; padding: 0px; font-size: 10pt; font-weight: bold; border: none; border-left: 3px solid transparent; border-radius: 6px; } QPushButton:hover { border: none; border-left: 3px solid palette(highlight); border-radius: 6px; }" + return "QPushButton { min-width: " + str(button_width) + "px; max-width: " + str(button_width) + "px; min-height: " + str(button_height) + "px; max-height: " + str(button_height) + "px; padding: 0px; font-size: 10pt; font-weight: bold; border: none; border-left: 3px solid transparent; } QPushButton:hover { border: none; border-left: 3px solid palette(highlight); }" def create_code_block_widget(code_text: str) -> QFrame: @@ -130,12 +277,12 @@ def create_code_block_widget(code_text: str) -> QFrame: text_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) text_edit.document().setDocumentMargin(0) text_edit.setFont(build_monospace_font()) - text_edit.setFixedHeight(get_standard_button_height()) - text_edit.setStyleSheet("QTextEdit { background-color: #1e1e1e; color: #C0C0C0; border: none; border-left: 3px solid transparent; padding: 8px 12px; selection-background-color: #505050; border-radius: 6px; } QTextEdit:hover { border: none; border-left: 3px solid palette(highlight); border-radius: 6px; }") + text_edit.setFixedHeight(STANDARD_BUTTON_HEIGHT) + text_edit.setStyleSheet(STYLE_CODE_EDIT) copy_button = QPushButton("Copy") copy_button.setCursor(QCursor(Qt.PointingHandCursor)) - copy_button.setFixedSize(get_copy_button_width(), get_standard_button_height()) - copy_button.setStyleSheet(build_copy_button_stylesheet(get_copy_button_width(), get_standard_button_height())) + copy_button.setFixedSize(COPY_BUTTON_WIDTH, STANDARD_BUTTON_HEIGHT) + copy_button.setStyleSheet(build_copy_button_stylesheet(COPY_BUTTON_WIDTH, STANDARD_BUTTON_HEIGHT)) copy_button.clicked.connect(lambda: process_copy_button_action(copy_button, text_edit.toPlainText())) layout.addWidget(text_edit, 1) layout.addWidget(copy_button, 0) @@ -143,21 +290,21 @@ def create_code_block_widget(code_text: str) -> QFrame: return frame -def _add_info_text(layout, text: str) -> None: +def _process_info_text(layout: QVBoxLayout, text: str) -> None: text_label = QLabel(text) text_label.setWordWrap(True) text_label.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum) - text_label.setStyleSheet("color: #585858; font-size: 9pt;") + text_label.setStyleSheet(STYLE_DESCRIPTION) layout.addWidget(text_label) return None -def _add_info_code(layout, item_entry: tuple) -> None: +def _process_info_code(layout: QVBoxLayout, item_entry: tuple) -> None: match len(item_entry) > 2 and item_entry[2] != "": case True: code_label = QLabel(item_entry[2]) code_label.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum) - code_label.setStyleSheet("color: #585858; font-size: 9pt; margin-top: 4px;") + code_label.setStyleSheet(STYLE_CODE_LABEL) layout.addWidget(code_label) case False: pass @@ -165,16 +312,16 @@ def _add_info_code(layout, item_entry: tuple) -> None: return None -def _add_info_entry(layout, item_entry) -> None: +def _process_info_entry(layout: QVBoxLayout, item_entry: tuple) -> None: match item_entry[0]: case "text": - _add_info_text(layout, item_entry[1]) + _process_info_text(layout, item_entry[1]) case "code": - _add_info_code(layout, item_entry) + _process_info_code(layout, item_entry) return None -def create_info_card_widget(label_text: str, card_data) -> QFrame: +def create_info_card_widget(label_text: str, card_data: str | tuple) -> QFrame: card = QFrame() card.setProperty("settingCard", True) card.setFrameStyle(QFrame.Box) @@ -188,14 +335,14 @@ def create_info_card_widget(label_text: str, card_data) -> QFrame: layout.addWidget(title_label) match isinstance(card_data, str): case True: - _add_info_text(layout, card_data) + _process_info_text(layout, card_data) case False: for item_entry in card_data: - _add_info_entry(layout, item_entry) + _process_info_entry(layout, item_entry) return card -def process_container_relayout(container_widget) -> None: +def process_container_relayout(container_widget: QWidget) -> None: match (container_widget.layout() is None, container_widget.width() <= 0): case (False, False): match container_widget.layout().heightForWidth(container_widget.width()) < 0: @@ -208,14 +355,14 @@ def process_container_relayout(container_widget) -> None: return None -def process_scroll_area_resize_sync(event, original_resize_handler, scroll_area_widget, content_container_widget) -> None: +def process_scroll_area_resize_sync(event: QResizeEvent, original_resize_handler: Callable[[QResizeEvent], None], scroll_area_widget: QScrollArea, content_container_widget: QWidget) -> None: original_resize_handler(event) content_container_widget.setFixedWidth(scroll_area_widget.viewport().width()) process_container_relayout(content_container_widget) return None -def create_scrollable_content_area(container_widget) -> QScrollArea: +def create_scrollable_content_area(container_widget: QWidget) -> QScrollArea: scroll_area = QScrollArea() scroll_area.setWidgetResizable(False) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) @@ -225,7 +372,7 @@ def create_scrollable_content_area(container_widget) -> QScrollArea: return scroll_area -def _build_content_container(info_items) -> QWidget: +def _create_content_container(info_items: Optional[dict]) -> QWidget: container_widget = QWidget() container_widget.setProperty("scrollContainer", True) content_layout = QVBoxLayout(container_widget) @@ -241,17 +388,17 @@ def _build_content_container(info_items) -> QWidget: return container_widget -def create_tab_content_widget(tab_name: str, info_items) -> dict: +def create_tab_content_widget(tab_name: str, info_items: Optional[dict]) -> dict: widget = QWidget() all_widgets = {} all_cards = {} main_layout = QVBoxLayout(widget) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) - container_widget = _build_content_container(info_items) + container_widget = _create_content_container(info_items) match info_items is None: case True: - for widget_key, label_text, description_text, options in find_cards_for_tab(tab_name): + for widget_key, label_text, description_text, options in call_cards_for_tab(tab_name): card_result = create_setting_card_widget(label_text, description_text, options) container_widget.layout().addWidget(card_result["card"]) container_widget.layout().addWidget(create_divider_widget()) @@ -263,7 +410,7 @@ def create_tab_content_widget(tab_name: str, info_items) -> dict: return {"tab": widget, "widgets": all_widgets, "cards": all_cards} -def create_sidebar_tab_list(tab_names: tuple, stacked_widget) -> QListWidget: +def create_sidebar_tab_list(tab_names: tuple, stacked_widget: QStackedWidget) -> QListWidget: tab_list = QListWidget() tab_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) tab_list.setFocusPolicy(Qt.NoFocus) @@ -276,18 +423,18 @@ def create_sidebar_tab_list(tab_names: tuple, stacked_widget) -> QListWidget: return tab_list -def build_sidebar_header_widget() -> QWidget: +def create_sidebar_header_widget() -> QWidget: header_widget = QWidget() header_widget.setStyleSheet("background-color: transparent;") header_layout = QHBoxLayout(header_widget) - header_layout.setContentsMargins(14, get_header_vertical_margin(), 14, get_header_vertical_margin()) + header_layout.setContentsMargins(14, HEADER_VERTICAL_MARGIN, 14, HEADER_VERTICAL_MARGIN) header_layout.setSpacing(0) volt_label = QLabel("volt") volt_label.setStyleSheet("font-weight: bold; font-size: 13pt; color: palette(highlight); background: transparent;") gui_label = QLabel("-gui") gui_label.setStyleSheet("font-weight: bold; font-size: 13pt; background: transparent;") version_label = QLabel("v" + APP_VERSION) - version_label.setStyleSheet("font-size: 8pt; color: #9A9A9A; background: transparent;") + version_label.setStyleSheet(STYLE_VERSION_LABEL) version_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) header_layout.addWidget(volt_label, 0) header_layout.addWidget(gui_label, 0) @@ -296,17 +443,17 @@ def build_sidebar_header_widget() -> QWidget: return header_widget -def build_sidebar_container_widget(tab_names: tuple, stacked_widget) -> tuple: +def create_sidebar_container_widget(tab_names: tuple, stacked_widget: QStackedWidget) -> tuple: sidebar_container = QWidget() - sidebar_container.setFixedWidth(get_sidebar_width()) + sidebar_container.setFixedWidth(SIDEBAR_WIDTH) sidebar_layout = QVBoxLayout(sidebar_container) sidebar_layout.setContentsMargins(0, 0, 0, 0) sidebar_layout.setSpacing(0) - sidebar_layout.addWidget(build_sidebar_header_widget()) + sidebar_layout.addWidget(create_sidebar_header_widget()) tab_list = create_sidebar_tab_list(tab_names, stacked_widget) sidebar_layout.addWidget(tab_list, 1) return (sidebar_container, tab_list) -def create_simple_sidebar_widget(tab_names: tuple, stacked_widget) -> QWidget: - return build_sidebar_container_widget(tab_names, stacked_widget)[0] +def create_simple_sidebar_widget(tab_names: tuple, stacked_widget: QStackedWidget) -> QWidget: + return create_sidebar_container_widget(tab_names, stacked_widget)[0] diff --git a/volt-gui/volt-gui.py b/src/volt-gui/volt-gui.py similarity index 62% rename from volt-gui/volt-gui.py rename to src/volt-gui/volt-gui.py index 1a894c4..013aa6e 100644 --- a/volt-gui/volt-gui.py +++ b/src/volt-gui/volt-gui.py @@ -1,29 +1,30 @@ import configparser -import json import os import signal import socket import sys -import urllib.request +from functools import partial from typing import Final +from typing import Optional from PySide6.QtCore import QProcess from PySide6.QtCore import Qt -from PySide6.QtCore import QThread from PySide6.QtCore import QTimer from PySide6.QtGui import QAction +from PySide6.QtGui import QCloseEvent from PySide6.QtGui import QIcon from PySide6.QtWidgets import QApplication -from PySide6.QtWidgets import QComboBox from PySide6.QtWidgets import QDialogButtonBox +from PySide6.QtWidgets import QFrame from PySide6.QtWidgets import QHBoxLayout from PySide6.QtWidgets import QInputDialog -from PySide6.QtWidgets import QListView +from PySide6.QtWidgets import QLabel from PySide6.QtWidgets import QMainWindow from PySide6.QtWidgets import QMenu from PySide6.QtWidgets import QMessageBox from PySide6.QtWidgets import QPushButton +from PySide6.QtWidgets import QSizePolicy from PySide6.QtWidgets import QStackedWidget from PySide6.QtWidgets import QSystemTrayIcon from PySide6.QtWidgets import QVBoxLayout @@ -40,48 +41,87 @@ from database import get_option_label from database import get_option_options from database import resolve_option_value -from presets import build_preset_combo_items +from presets import process_preset_combo_items from presets import get_preset_placeholder_label from presets import is_valid_preset_name from presets import process_preset_apply from probe import call_probe_stamp from profiles import build_config_dir from profiles import build_options_path -from profiles import find_all_profiles +from profiles import call_all_profiles +from profiles import is_reserved_profile_name from profiles import process_profile_delete from profiles import process_profile_options_rebuild from profiles import process_profile_save from profiles import process_profile_widget_load -from themes import get_standard_button_height -from themes import get_standard_button_width +from themes import STANDARD_BUTTON_HEIGHT +from themes import STANDARD_BUTTON_WIDTH from themes import process_theme_application from ui import create_code_block_widget +from ui import create_slider_widget from ui import create_scrollable_content_area from ui import create_tab_content_widget -from ui import build_sidebar_container_widget -from ui import get_header_vertical_margin +from ui import create_sidebar_container_widget +from ui import HEADER_VERTICAL_MARGIN +from ui import STYLE_DESCRIPTION +from ui import WINDOW_MIN_HEIGHT +from ui import WINDOW_MIN_WIDTH from welcome import create_welcome_window_widget -UPDATE_URL: Final[str] = "https://api.github.com/repos/pythonlover02/volt-gui/releases/latest" -UPDATE_TIMEOUT_S: Final[int] = 5 SINGLETON_PORT: Final[int] = 47832 OPTIONS_SAVE_DEBOUNCE_MS: Final[int] = 500 -NEW_PROFILE_LABEL: Final[str] = "New Profile..." -DELETE_PROFILE_LABEL: Final[str] = "Delete Current" -SCALE_MIN: Final[float] = 0.5 -SCALE_MAX: Final[float] = 3.0 +NEW_PROFILE_LABEL: Final[str] = "New" +SUBMENU_TITLE: Final[str] = "Apply Profile " +DELETE_PROFILE_LABEL: Final[str] = "Delete" +SELECTOR_MIN_WIDTH: Final[int] = 120 +SELECTOR_STRETCH: Final[int] = 1 +DEFAULT_PROFILE_LABEL: Final[str] = "Default" +SCALE_MIN: Final[float] = 0.8 +SCALE_MAX: Final[float] = 2.0 DEFAULT_SCALE: Final[str] = "1.0" +GRAPHIC_FIRST: Final[int] = 33 +GRAPHIC_LAST: Final[int] = 126 +PROBE_EXIT_OK: Final[int] = 0 +PROBE_EXIT_UNSUPPORTED: Final[int] = 2 +WINDOW_OPACITY: Final[float] = 0.95 +WINDOW_OPAQUE: Final[float] = 1.0 +WELCOME_DELAY_MS: Final[int] = 100 +SHOW_DELAY_MS: Final[int] = 0 +TRAY_ICON_NAME: Final[str] = "volt-gui" +NOTIFICATION_TITLE: Final[str] = "volt-gui" PREVIEW_BIN: Final[str] = "volt" -PREVIEW_TARGET: Final[str] = "vkgears" +PREVIEW_TARGET: Final[str] = "volt-probe" PREVIEW_POLL_MS: Final[int] = 750 PREVIEW_START_MS: Final[int] = 300 PREVIEW_STOP_MS: Final[int] = 1500 +BUNDLE_ATTR: Final[str] = "_MEIPASS" +LIB_PATH_VAR: Final[str] = "LD_LIBRARY_PATH" +LIB_PATH_ORIG: Final[str] = "LD_LIBRARY_PATH_ORIG" +PRELOAD_VAR: Final[str] = "LD_PRELOAD" +PATH_VAR: Final[str] = "PATH" +PROBE_FAILED_ERROR: Final[str] = "volt-probe failed to run.\n\nWithout it volt-gui cannot read your hardware, so every setting fed by the device holds nothing but default.\n\nvolt-probe installs next to volt and volt-gui. Check that their directory is on your PATH, then restart volt-gui." def build_preview_args(profile_name: str) -> list: return ["--probe", profile_name, "--", PREVIEW_TARGET] +def build_profile_label(profile_name: str) -> str: + match profile_name == DEFAULT_PROFILE: + case True: + return DEFAULT_PROFILE_LABEL + case False: + return profile_name + + +def resolve_profile_label(label: str) -> str: + match label == DEFAULT_PROFILE_LABEL: + case True: + return DEFAULT_PROFILE + case False: + return label + + def build_launch_command(profile_name: str) -> str: match profile_name == DEFAULT_PROFILE: case True: @@ -90,7 +130,7 @@ def build_launch_command(profile_name: str) -> str: return "volt " + profile_name + " -- %command%" -def get_persisted_option_value(option_key: str) -> str: +def call_persisted_option_value(option_key: str) -> str: match build_options_path().exists(): case False: return get_option_default_value(option_key) @@ -117,17 +157,84 @@ def resolve_scale_factor(raw: str) -> str: return DEFAULT_SCALE -def get_persisted_option_resolved(option_key: str) -> str: - return resolve_option_value(option_key, get_persisted_option_value(option_key)) +def call_persisted_option_resolved(option_key: str) -> str: + return resolve_option_value(option_key, call_persisted_option_value(option_key)) + + +def get_bundle_dir() -> str: + return getattr(sys, BUNDLE_ATTR, "") + + +def _outside_bundle(bundle: str, entry: str) -> bool: + return bundle not in entry -def calculate_initial_scale() -> None: +def _cleaned_path(value: str, bundle: str) -> str: + return os.pathsep.join( + filter(partial(_outside_bundle, bundle), value.split(os.pathsep))) + + +def call_restore_lib_path() -> None: + match os.environ.pop(LIB_PATH_ORIG, ""): + case "": + os.environ.pop(LIB_PATH_VAR, None) + case original: + os.environ[LIB_PATH_VAR] = original + return None + + +def call_drop_bundle_vars() -> None: + call_restore_lib_path() + os.environ.pop(PRELOAD_VAR, None) + return None + + +def call_clean_path(bundle: str) -> None: + os.environ[PATH_VAR] = _cleaned_path(os.environ.get(PATH_VAR, ""), bundle) + return None + + +def call_unbundle_environment(bundle: str) -> None: + call_drop_bundle_vars() + call_clean_path(bundle) + return None + + +def call_clean_environment() -> None: + match get_bundle_dir(): + case "": + return None + case bundle: + call_unbundle_environment(bundle) + return None + + +def process_initial_scale() -> None: os.environ["QT_SCALE_FACTOR"] = resolve_scale_factor( - get_persisted_option_resolved("interface_scale_factor")) + call_persisted_option_resolved("interface_scale_factor")) return None -def get_widget_option_text(main_window, option_key: str) -> str: +def build_platform_chain(platform: str) -> str: + match platform: + case "xcb": + return "xcb;wayland" + case "wayland": + return "wayland;xcb" + case other: + return other + + +def process_initial_platform() -> None: + match call_persisted_option_resolved("qt_platform"): + case "": + return None + case platform: + os.environ.setdefault("QT_QPA_PLATFORM", build_platform_chain(platform)) + return None + + +def get_widget_option_text(main_window: QMainWindow, option_key: str) -> str: match main_window.options_widgets.get(option_key): case None: return DEFAULT_VALUE @@ -135,19 +242,15 @@ def get_widget_option_text(main_window, option_key: str) -> str: return widget.currentText().strip() -def get_resolved_option_value(main_window, option_key: str) -> str: +def get_resolved_option_value(main_window: QMainWindow, option_key: str) -> str: return resolve_option_value(option_key, get_widget_option_text(main_window, option_key)) -def is_option_enabled(main_window, option_key: str) -> bool: +def is_option_enabled(main_window: QMainWindow, option_key: str) -> bool: return get_resolved_option_value(main_window, option_key) == "on" def create_options_tab_widget() -> dict: - from PySide6.QtWidgets import QFrame - from PySide6.QtWidgets import QLabel - from PySide6.QtWidgets import QSizePolicy - from ui import create_combo_widget widget = QWidget() options_widgets = {} main_layout = QVBoxLayout(widget) @@ -169,43 +272,40 @@ def create_options_tab_widget() -> dict: title_label = QLabel(get_option_label(option_key)) title_label.setStyleSheet("font-weight: 500; font-size: 11pt;") card_layout.addWidget(title_label) - combo = create_combo_widget(get_option_options(option_key)) - card_layout.addWidget(combo) + slider = create_slider_widget(get_option_options(option_key)) + card_layout.addWidget(slider) description_label = QLabel(get_option_description(option_key)) description_label.setWordWrap(True) - description_label.setStyleSheet("color: #585858; font-size: 9pt;") + description_label.setStyleSheet(STYLE_DESCRIPTION) card_layout.addWidget(description_label) content_layout.addWidget(card) - options_widgets[option_key] = combo + options_widgets[option_key] = slider main_layout.addWidget(create_scrollable_content_area(container_widget), 1) return {"tab": widget, "widgets": options_widgets} -def process_profile_list_update(main_window) -> None: +def process_profile_list_update(main_window: QMainWindow) -> None: main_window.profile_selector.blockSignals(True) main_window.profile_selector.clear() - for profile_name in find_all_profiles(): - main_window.profile_selector.addItem(profile_name) - main_window.profile_selector.insertSeparator(main_window.profile_selector.count()) - main_window.profile_selector.addItem(NEW_PROFILE_LABEL) - main_window.profile_selector.addItem(DELETE_PROFILE_LABEL) + for profile_name in call_all_profiles(): + main_window.profile_selector.addItem(build_profile_label(profile_name), profile_name) main_window.profile_selector.blockSignals(False) return None -def process_profile_selector_restore(main_window) -> None: +def process_profile_selector_restore(main_window: QMainWindow) -> None: main_window.profile_selector.blockSignals(True) - main_window.profile_selector.setCurrentText(main_window.current_profile) + main_window.profile_selector.setCurrentText(build_profile_label(main_window.current_profile)) main_window.profile_selector.blockSignals(False) return None -def process_launch_line_update(main_window) -> None: +def process_launch_line_update(main_window: QMainWindow) -> None: main_window.launch_block.code_editor.setPlainText(build_launch_command(main_window.current_profile)) return None -def process_profile_change(main_window, profile_name: str) -> None: +def process_profile_change(main_window: QMainWindow, profile_name: str) -> None: match getattr(main_window, "initial_setup_complete", False): case False: return None @@ -221,7 +321,7 @@ def process_profile_change(main_window, profile_name: str) -> None: return None -def process_yes_no_dialog(parent_widget, title: str, message: str) -> bool: +def process_yes_no_dialog(parent_widget: QMainWindow, title: str, message: str) -> bool: dialog = QMessageBox(parent_widget) dialog.setWindowTitle(title) dialog.setText(message) @@ -235,17 +335,28 @@ def process_yes_no_dialog(parent_widget, title: str, message: str) -> bool: return dialog.exec() == QMessageBox.Yes -def is_new_profile_name_valid(profile_name: str) -> bool: - match (profile_name.strip() == "", profile_name.strip().lower() == DEFAULT_PROFILE, profile_name.strip() in find_all_profiles(), "/" in profile_name or "\\" in profile_name or ".." in profile_name): - case (False, False, False, False): +def is_graphic_ascii(profile_name: str) -> bool: + return all(GRAPHIC_FIRST <= ord(character) <= GRAPHIC_LAST for character in profile_name) + + +def call_new_profile_name_valid(profile_name: str) -> bool: + match ( + profile_name.strip() == "", + is_reserved_profile_name(profile_name), + profile_name.strip() in call_all_profiles(), + "/" in profile_name or "\\" in profile_name or ".." in profile_name, + "\0" in profile_name, + is_graphic_ascii(profile_name), + ): + case (False, False, False, False, False, True): return True case _: return False -def process_new_profile_save(main_window) -> None: +def process_new_profile_save(main_window: QMainWindow) -> None: profile_name, accepted = QInputDialog.getText(main_window, "New Profile", "Profile name:") - match (accepted, profile_name is not None and is_new_profile_name_valid(profile_name)): + match (accepted, profile_name is not None and call_new_profile_name_valid(profile_name)): case (True, True): process_profile_save(main_window.all_widgets, main_window.current_profile) main_window.current_profile = profile_name.strip() @@ -254,19 +365,19 @@ def process_new_profile_save(main_window) -> None: process_profile_selector_restore(main_window) process_launch_line_update(main_window) process_tray_menu_update(main_window) - process_notification_display(main_window, "Profile '" + profile_name.strip() + "' created.", False) + process_notification_display(main_window, "Profile '" + profile_name.strip() + "' created.") return None case (True, False): - process_notification_display(main_window, "Profile name invalid or already exists.", True) + process_notification_display(main_window, "Profile name invalid or already exists.") return None case _: return None -def process_current_profile_delete(main_window) -> None: +def process_current_profile_delete(main_window: QMainWindow) -> None: match main_window.current_profile == DEFAULT_PROFILE: case True: - process_notification_display(main_window, "Cannot delete default profile.", True) + process_notification_display(main_window, "Cannot delete default profile.") return None case False: match process_yes_no_dialog(main_window, "Delete Profile", "Delete profile '" + main_window.current_profile + "'?"): @@ -277,56 +388,45 @@ def process_current_profile_delete(main_window) -> None: main_window.current_profile = DEFAULT_PROFILE process_profile_list_update(main_window) process_profile_selector_restore(main_window) - process_profile_widget_load(main_window.all_widgets, DEFAULT_PROFILE) + process_dropped_notice( + main_window, + process_profile_widget_load(main_window.all_widgets, DEFAULT_PROFILE)) process_launch_line_update(main_window) process_tray_menu_update(main_window) - process_notification_display(main_window, "Profile deleted.", False) + process_notification_display(main_window, "Profile deleted.") return None -def process_profile_combo_change(main_window, selected_text: str) -> None: - match selected_text: - case s if s == NEW_PROFILE_LABEL: - process_profile_selector_restore(main_window) - process_new_profile_save(main_window) - case s if s == DELETE_PROFILE_LABEL: - process_profile_selector_restore(main_window) - process_current_profile_delete(main_window) - case s: - process_profile_change(main_window, s) - return None - - -def process_preset_combo_change(main_window, selected_text: str) -> None: +def process_preset_combo_change(main_window: QMainWindow, selected_text: str) -> None: match (selected_text == get_preset_placeholder_label(), is_valid_preset_name(selected_text)): case (True, _): return None case (False, False): - build_preset_combo_items(main_window.preset_selector) + process_preset_combo_items(main_window.preset_selector) return None case (False, True): match process_yes_no_dialog(main_window, "Apply Preset", "Apply '" + selected_text + "' to '" + main_window.current_profile + "'? All values will be replaced."): case True: dropped = process_preset_apply(main_window.all_widgets, selected_text) process_profile_save(main_window.all_widgets, main_window.current_profile) - process_notification_display(main_window, "Preset '" + selected_text + "' applied to profile '" + main_window.current_profile + "'.", False) + process_notification_display(main_window, "Preset '" + selected_text + "' applied to profile '" + main_window.current_profile + "'.") process_dropped_notice(main_window, dropped) case False: pass - build_preset_combo_items(main_window.preset_selector) + process_preset_combo_items(main_window.preset_selector) return None -def create_system_tray_widget(main_window) -> None: +def create_system_tray_widget(main_window: QMainWindow) -> None: match QSystemTrayIcon.isSystemTrayAvailable(): case False: return None case True: main_window.tray_icon = QSystemTrayIcon(main_window) - main_window.tray_icon.setIcon(QIcon.fromTheme("preferences-system")) + main_window.tray_icon.setIcon(QIcon.fromTheme(TRAY_ICON_NAME)) menu = QMenu() menu.addAction(QAction("Show", main_window, triggered=lambda: process_window_show(main_window))) - main_window.profile_submenu = QMenu("Apply Profile", menu) + main_window.profile_submenu = QMenu(SUBMENU_TITLE, menu) process_tray_menu_update(main_window) menu.addMenu(main_window.profile_submenu) menu.addSeparator() @@ -337,20 +437,20 @@ def create_system_tray_widget(main_window) -> None: return None -def process_tray_menu_update(main_window) -> None: +def process_tray_menu_update(main_window: QMainWindow) -> None: match hasattr(main_window, "profile_submenu"): case False: return None case True: main_window.profile_submenu.clear() - for profile_name in find_all_profiles(): - action = QAction("Apply " + profile_name, main_window) + for profile_name in call_all_profiles(): + action = QAction("Apply " + build_profile_label(profile_name), main_window) action.triggered.connect(lambda checked, bound_profile_name=profile_name: process_profile_apply_from_tray(main_window, bound_profile_name)) main_window.profile_submenu.addAction(action) return None -def process_tray_activation(main_window, activation_reason) -> None: +def process_tray_activation(main_window: QMainWindow, activation_reason: QSystemTrayIcon.ActivationReason) -> None: match activation_reason in (QSystemTrayIcon.ActivationReason.Trigger, QSystemTrayIcon.ActivationReason.DoubleClick): case False: return None @@ -363,7 +463,7 @@ def process_tray_activation(main_window, activation_reason) -> None: return None -def process_window_show(main_window) -> None: +def process_window_show(main_window: QMainWindow) -> None: match main_window.start_maximized: case True: main_window.showMaximized() @@ -371,16 +471,24 @@ def process_window_show(main_window) -> None: main_window.show() main_window.activateWindow() main_window.raise_() + match main_window.welcome_pending: + case True: + main_window.welcome_pending = False + QTimer.singleShot(WELCOME_DELAY_MS, lambda: process_welcome_show(main_window)) + case False: + pass return None -def process_profile_apply_from_tray(main_window, profile_name: str) -> None: +def process_profile_apply_from_tray(main_window: QMainWindow, profile_name: str) -> None: match profile_name != main_window.current_profile: case True: process_profile_save(main_window.all_widgets, main_window.current_profile) main_window.current_profile = profile_name process_profile_selector_restore(main_window) - process_profile_widget_load(main_window.all_widgets, profile_name) + process_dropped_notice( + main_window, + process_profile_widget_load(main_window.all_widgets, profile_name)) process_launch_line_update(main_window) case False: pass @@ -388,20 +496,17 @@ def process_profile_apply_from_tray(main_window, profile_name: str) -> None: return None -def process_notification_display(main_window, notification_message: str, is_error: bool) -> None: - match (hasattr(main_window, "tray_icon"), is_error): - case (True, True): - main_window.tray_icon.showMessage("volt-gui", notification_message, QSystemTrayIcon.MessageIcon.Critical, 2000) - case (True, False): - main_window.tray_icon.showMessage("volt-gui", notification_message, QSystemTrayIcon.MessageIcon.Information, 2000) - case (False, True): - QMessageBox.warning(main_window, "volt-gui", notification_message) - case (False, False): - QMessageBox.information(main_window, "volt-gui", notification_message) +def process_notification_display(main_window: QMainWindow, notification_message: str) -> None: + dialog = QMessageBox(main_window) + dialog.setWindowTitle(NOTIFICATION_TITLE) + dialog.setText(notification_message) + dialog.setIcon(QMessageBox.NoIcon) + dialog.setStandardButtons(QMessageBox.Ok) + dialog.exec() return None -def process_tray_option_update(main_window, tray_enabled: bool) -> None: +def process_tray_option_update(main_window: QMainWindow, tray_enabled: bool) -> None: match (main_window.use_system_tray == tray_enabled, tray_enabled, hasattr(main_window, "tray_icon")): case (True, _, _): main_window.use_system_tray = tray_enabled @@ -428,22 +533,22 @@ def process_tray_option_update(main_window, tray_enabled: bool) -> None: return None -def process_options_application(main_window) -> None: +def process_options_application(main_window: QMainWindow) -> None: process_theme_application(QApplication.instance(), get_resolved_option_value(main_window, "application_theme")) - match is_option_enabled(main_window, "window_transparency"): - case True: - main_window.setWindowOpacity(0.95) - case False: - main_window.setWindowOpacity(1.0) + match (is_option_enabled(main_window, "window_transparency"), + QApplication.instance().platformName()): + case (True, "xcb"): + main_window.setWindowOpacity(WINDOW_OPACITY) + case _: + main_window.setWindowOpacity(WINDOW_OPAQUE) process_tray_option_update(main_window, is_option_enabled(main_window, "system_tray_behavior")) main_window.start_minimized = is_option_enabled(main_window, "start_window_minimized") main_window.start_maximized = is_option_enabled(main_window, "start_window_maximized") main_window.show_welcome = is_option_enabled(main_window, "welcome_message_display") - main_window.check_updates = is_option_enabled(main_window, "automatic_update_check") return None -def process_options_save_timer_trigger(main_window) -> None: +def process_options_save_timer_trigger(main_window: QMainWindow) -> None: match getattr(main_window, "options_save_timer", None): case None: main_window.options_save_timer = QTimer(main_window) @@ -455,7 +560,7 @@ def process_options_save_timer_trigger(main_window) -> None: return None -def process_option_change(main_window) -> None: +def process_option_change(main_window: QMainWindow) -> None: match getattr(main_window, "initial_setup_complete", False): case True: process_options_save_timer_trigger(main_window) @@ -464,12 +569,15 @@ def process_option_change(main_window) -> None: return None -def process_application_options_save(main_window) -> None: +def _option_present(main_window: QMainWindow, option_key: str) -> bool: + return option_key in main_window.options_widgets + + +def process_application_options_save(main_window: QMainWindow) -> None: parser_instance = configparser.ConfigParser(interpolation=None) parser_instance["Options"] = { option_key: main_window.options_widgets[option_key].currentText().strip() - for option_key in OPTIONS_DB - if option_key in main_window.options_widgets} + for option_key in filter(partial(_option_present, main_window), OPTIONS_DB)} parser_instance["Profile"] = {"last_active_profile": main_window.current_profile} os.makedirs(build_config_dir(), exist_ok=True) with open(build_options_path(), "w") as file_handle: @@ -477,7 +585,7 @@ def process_application_options_save(main_window) -> None: return None -def process_application_options_load(main_window) -> None: +def process_application_options_load(main_window: QMainWindow) -> None: parser_instance = configparser.ConfigParser(interpolation=None) parser_instance.read(build_options_path()) for option_key in OPTIONS_DB: @@ -488,10 +596,10 @@ def process_application_options_load(main_window) -> None: saved = parser_instance.get("Options", option_key, fallback=get_option_default_value(option_key)) main_window.options_widgets[option_key].setCurrentText(saved) last_profile = parser_instance.get("Profile", "last_active_profile", fallback=DEFAULT_PROFILE) - match main_window.profile_selector.findText(last_profile) >= 0: + match main_window.profile_selector.findText(build_profile_label(last_profile)) >= 0: case True: main_window.profile_selector.blockSignals(True) - main_window.profile_selector.setCurrentText(last_profile) + main_window.profile_selector.setCurrentText(build_profile_label(last_profile)) main_window.profile_selector.blockSignals(False) main_window.current_profile = last_profile case False: @@ -500,7 +608,7 @@ def process_application_options_load(main_window) -> None: return None -def process_preview_stop(main_window) -> None: +def process_preview_stop(main_window: QMainWindow) -> None: match getattr(main_window, "preview_process", None): case None: return None @@ -511,15 +619,49 @@ def process_preview_stop(main_window) -> None: return None -def process_preview_start(main_window) -> None: +def process_probe_failure(main_window: QMainWindow) -> None: + match main_window.probe_error_shown: + case True: + return None + case False: + main_window.probe_error_shown = True + process_notification_display(main_window, PROBE_FAILED_ERROR) + return None + + +def process_preview_error(main_window: QMainWindow, process_error: QProcess.ProcessError) -> None: + match process_error == QProcess.ProcessError.FailedToStart: + case True: + process_probe_failure(main_window) + return None + case False: + return None + + +def process_preview_exit(main_window: QMainWindow, exit_code: int, exit_status: QProcess.ExitStatus) -> None: + match (exit_status == QProcess.ExitStatus.NormalExit, exit_code): + case (True, code) if code in (PROBE_EXIT_OK, PROBE_EXIT_UNSUPPORTED): + return None + case (True, _): + process_probe_failure(main_window) + return None + case _: + return None + + +def process_preview_start(main_window: QMainWindow) -> None: process_preview_stop(main_window) worker = QProcess(main_window) + worker.errorOccurred.connect( + lambda process_error: process_preview_error(main_window, process_error)) + worker.finished.connect( + lambda exit_code, exit_status: process_preview_exit(main_window, exit_code, exit_status)) worker.start(PREVIEW_BIN, build_preview_args(main_window.current_profile)) main_window.preview_process = worker return None -def process_dropped_notice(main_window, dropped: tuple) -> None: +def process_dropped_notice(main_window: QMainWindow, dropped: tuple) -> None: match len(dropped): case 0: return None @@ -528,12 +670,11 @@ def process_dropped_notice(main_window, dropped: tuple) -> None: main_window, "This device cannot provide " + ", ".join(key.split(":")[-1].split(".")[-1] for key in dropped) - + ", reset to default.", - True) + + ", reset to default.") return None -def process_probe_rebuild(main_window) -> None: +def process_probe_rebuild(main_window: QMainWindow) -> None: process_profile_options_rebuild(main_window.all_widgets) process_dropped_notice( main_window, @@ -541,25 +682,29 @@ def process_probe_rebuild(main_window) -> None: return None -def process_probe_poll(main_window) -> None: - match call_probe_stamp(): - case stamp if stamp == main_window.probe_stamp: - return None - case stamp: +def process_probe_poll(main_window: QMainWindow) -> None: + match (call_probe_stamp(), main_window.probe_stamp, main_window.probe_settled): + case (stamp, seen, _) if stamp != seen: main_window.probe_stamp = stamp + main_window.probe_settled = False + return None + case (_, _, False): + main_window.probe_settled = True process_probe_rebuild(main_window) return None + case _: + return None -def process_all_settings_apply(main_window) -> None: +def process_all_settings_apply(main_window: QMainWindow) -> None: process_application_options_save(main_window) process_profile_save(main_window.all_widgets, main_window.current_profile) process_preview_start(main_window) - process_notification_display(main_window, "Profile '" + main_window.current_profile + "' saved. Start a game again to pick it up.", False) + process_notification_display(main_window, "Profile '" + main_window.current_profile + "' saved. Start a game again to pick it up.") return None -def process_window_close(main_window, singleton_socket, close_event) -> None: +def process_window_close(main_window: QMainWindow, singleton_socket: Optional[socket.socket], close_event: QCloseEvent) -> None: match (main_window.use_system_tray, hasattr(main_window, "tray_icon")): case (True, True): main_window.hide() @@ -572,7 +717,7 @@ def process_window_close(main_window, singleton_socket, close_event) -> None: return None -def process_cleanup(main_window, singleton_socket) -> None: +def process_cleanup(main_window: QMainWindow, singleton_socket: Optional[socket.socket]) -> None: match getattr(main_window, "options_save_timer", None): case None: pass @@ -595,53 +740,31 @@ def process_cleanup(main_window, singleton_socket) -> None: return None -def process_application_quit(main_window) -> None: +def process_application_quit(main_window: QMainWindow) -> None: process_cleanup(main_window, main_window.singleton_socket) QApplication.quit() return None -def process_welcome_show(main_window) -> None: +def process_welcome_show(main_window: QMainWindow) -> None: match main_window.welcome_window is None: case True: main_window.welcome_window = create_welcome_window_widget() case False: pass - main_window.welcome_window.show() - main_window.welcome_window.activateWindow() - main_window.welcome_window.raise_() - return None - - -def call_fetch_latest_tag() -> str: - try: - with urllib.request.urlopen(UPDATE_URL, timeout=UPDATE_TIMEOUT_S) as response: - payload = json.loads(response.read().decode()) - return str(payload.get("tag_name", "")).lstrip("v") - except (urllib.error.URLError, ValueError, OSError): - return "" - - -def process_updates_check_worker(main_window, worker_thread) -> None: - latest_tag = call_fetch_latest_tag() - match latest_tag not in ("", APP_VERSION): + main_window.welcome_window.setWindowOpacity(main_window.windowOpacity()) + match main_window.start_maximized: case True: - QTimer.singleShot(0, main_window, lambda bound_tag=latest_tag: process_notification_display(main_window, "New version available: " + bound_tag, False)) + main_window.welcome_window.showMaximized() case False: - pass - worker_thread.quit() + main_window.welcome_window.show() + main_window.welcome_window.activateWindow() + main_window.welcome_window.raise_() return None -def process_updates_check_async(main_window) -> None: - worker_thread = QThread(main_window) - worker_thread.started.connect(lambda: process_updates_check_worker(main_window, worker_thread)) - worker_thread.finished.connect(worker_thread.deleteLater) - worker_thread.start() - return None - -def validate_singleton_instance(singleton_port: int) -> dict: +def call_claim_singleton(singleton_port: int) -> dict: lock_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) lock_name = "\0volt-gui-singleton-" + str(singleton_port) match lock_socket.connect_ex(lock_name) != 0: @@ -655,20 +778,20 @@ def validate_singleton_instance(singleton_port: int) -> dict: return {"socket": None, "running": True} -def process_signal_handler(main_window, signal_number: int) -> None: +def process_signal_handler(main_window: QMainWindow, signal_number: int) -> None: print("\nReceived signal " + str(signal_number) + ", closing...") process_cleanup(main_window, main_window.singleton_socket) QApplication.quit() sys.exit(0) -def process_signal_handlers_setup(main_window) -> None: +def process_signal_handlers_setup(main_window: QMainWindow) -> None: signal.signal(signal.SIGINT, lambda signal_number, frame: process_signal_handler(main_window, signal_number)) signal.signal(signal.SIGTERM, lambda signal_number, frame: process_signal_handler(main_window, signal_number)) return None -def process_create_tab(stacked_widget, all_widgets: dict, options_widgets: dict, tab_name: str) -> None: +def process_create_tab(stacked_widget: QStackedWidget, all_widgets: dict, options_widgets: dict, tab_name: str) -> None: match tab_name: case "Options": tab_result = create_options_tab_widget() @@ -683,22 +806,24 @@ def process_create_tab(stacked_widget, all_widgets: dict, options_widgets: dict, return None -def create_main_window_widget(singleton_socket): +def create_main_window_widget(singleton_socket: Optional[socket.socket]) -> QMainWindow: window = QMainWindow() window.singleton_socket = singleton_socket - window.check_updates = False window.start_maximized = False window.start_minimized = False window.show_welcome = True window.use_system_tray = False window.current_profile = DEFAULT_PROFILE window.welcome_window = None + window.welcome_pending = False window.preview_process = None + window.probe_error_shown = False window.probe_stamp = call_probe_stamp() + window.probe_settled = True window.setWindowTitle("volt-gui") - window.setMinimumSize(620, 380) + window.setMinimumSize(WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT) window.setAttribute(Qt.WA_DontShowOnScreen, True) - process_theme_application(QApplication.instance(), get_persisted_option_resolved("application_theme")) + process_theme_application(QApplication.instance(), call_persisted_option_resolved("application_theme")) central_widget = QWidget() main_layout = QVBoxLayout(central_widget) main_layout.setContentsMargins(8, 8, 8, 8) @@ -711,7 +836,7 @@ def create_main_window_widget(singleton_socket): options_widgets = {} for tab_name in ALL_TABS: process_create_tab(stacked_widget, all_widgets, options_widgets, tab_name) - sidebar_container, tab_list = build_sidebar_container_widget(ALL_TABS, stacked_widget) + sidebar_container, tab_list = create_sidebar_container_widget(ALL_TABS, stacked_widget) window.sidebar_tab_list = tab_list content_layout.addWidget(sidebar_container) right_content_widget = QWidget() @@ -721,7 +846,7 @@ def create_main_window_widget(singleton_socket): window.launch_block = create_code_block_widget(build_launch_command(DEFAULT_PROFILE)) launch_wrapper = QWidget() launch_wrapper_layout = QVBoxLayout(launch_wrapper) - launch_wrapper_layout.setContentsMargins(12, get_header_vertical_margin(), 8, 8) + launch_wrapper_layout.setContentsMargins(12, HEADER_VERTICAL_MARGIN, 8, 8) launch_wrapper_layout.setSpacing(0) launch_wrapper_layout.addWidget(window.launch_block) right_content_layout.addWidget(launch_wrapper) @@ -734,32 +859,36 @@ def create_main_window_widget(singleton_socket): bottom_bar_layout.setContentsMargins(8, 8, 8, 8) bottom_bar_layout.setSpacing(8) bottom_bar_layout.setAlignment(Qt.AlignBottom) - preset_combo = QComboBox() - preset_combo.setView(QListView()) - preset_combo.setFixedSize(get_standard_button_width(), get_standard_button_height()) - preset_combo.setFocusPolicy(Qt.ClickFocus) - window.preset_selector = preset_combo - build_preset_combo_items(preset_combo) - profile_combo = QComboBox() - profile_combo.setView(QListView()) - profile_combo.setFixedSize(get_standard_button_width(), get_standard_button_height()) - profile_combo.setFocusPolicy(Qt.ClickFocus) - window.profile_selector = profile_combo + preset_slider = create_slider_widget(()) + preset_slider.setMinimumWidth(SELECTOR_MIN_WIDTH) + preset_slider.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) + window.preset_selector = preset_slider + process_preset_combo_items(preset_slider) + profile_slider = create_slider_widget(()) + profile_slider.setMinimumWidth(SELECTOR_MIN_WIDTH) + profile_slider.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) + window.profile_selector = profile_slider apply_button = QPushButton("Apply") - apply_button.setFixedSize(get_standard_button_width(), get_standard_button_height()) + apply_button.setFixedSize(STANDARD_BUTTON_WIDTH, STANDARD_BUTTON_HEIGHT) apply_button.clicked.connect(lambda: process_all_settings_apply(window)) - bottom_bar_layout.addStretch(1) - bottom_bar_layout.addWidget(preset_combo, 0, Qt.AlignBottom) - bottom_bar_layout.addWidget(apply_button, 0, Qt.AlignBottom) - bottom_bar_layout.addWidget(profile_combo, 0, Qt.AlignBottom) - bottom_bar_layout.addStretch(1) + new_button = QPushButton(NEW_PROFILE_LABEL) + new_button.setFixedSize(STANDARD_BUTTON_WIDTH, STANDARD_BUTTON_HEIGHT) + new_button.clicked.connect(lambda: process_new_profile_save(window)) + delete_button = QPushButton(DELETE_PROFILE_LABEL) + delete_button.setFixedSize(STANDARD_BUTTON_WIDTH, STANDARD_BUTTON_HEIGHT) + delete_button.clicked.connect(lambda: process_current_profile_delete(window)) + bottom_bar_layout.addWidget(preset_slider, SELECTOR_STRETCH, Qt.AlignVCenter) + bottom_bar_layout.addWidget(profile_slider, SELECTOR_STRETCH, Qt.AlignVCenter) + bottom_bar_layout.addWidget(delete_button, 0, Qt.AlignVCenter) + bottom_bar_layout.addWidget(new_button, 0, Qt.AlignVCenter) + bottom_bar_layout.addWidget(apply_button, 0, Qt.AlignVCenter) main_layout.addWidget(bottom_bar_widget) window.setCentralWidget(central_widget) window.all_widgets = all_widgets window.options_widgets = options_widgets process_profile_list_update(window) process_profile_selector_restore(window) - window.profile_selector.currentTextChanged.connect(lambda text: process_profile_combo_change(window, text)) + window.profile_selector.currentTextChanged.connect(lambda text: process_profile_change(window, resolve_profile_label(text))) window.preset_selector.currentTextChanged.connect(lambda text: process_preset_combo_change(window, text)) for option_key in options_widgets: options_widgets[option_key].currentTextChanged.connect(lambda text, bound_window=window: process_option_change(bound_window)) @@ -775,19 +904,16 @@ def create_main_window_widget(singleton_socket): QApplication.instance().setQuitOnLastWindowClosed(not window.use_system_tray) case True: pass - match window.show_welcome: - case True: - QTimer.singleShot(100, lambda: process_welcome_show(window)) - case False: - pass - match window.check_updates: - case True: - QTimer.singleShot(200, lambda: process_updates_check_async(window)) - case False: + match (window.show_welcome, window.start_minimized and window.use_system_tray): + case (True, False): + QTimer.singleShot(WELCOME_DELAY_MS, lambda: process_welcome_show(window)) + case (True, True): + window.welcome_pending = True + case (False, _): pass match window.start_minimized and window.use_system_tray: case False: - QTimer.singleShot(0, lambda: process_window_show(window)) + QTimer.singleShot(SHOW_DELAY_MS, lambda: process_window_show(window)) case True: pass window.probe_timer = QTimer(window) @@ -805,7 +931,7 @@ def main() -> None: sys.exit(1) case False: pass - singleton_result = validate_singleton_instance(SINGLETON_PORT) + singleton_result = call_claim_singleton(SINGLETON_PORT) match singleton_result["running"]: case True: print("volt-gui is already running.") @@ -813,7 +939,9 @@ def main() -> None: case False: pass os.environ.setdefault("QT_LOGGING_RULES", "qt.qpa.theme.gnome=false") - calculate_initial_scale() + call_clean_environment() + process_initial_platform() + process_initial_scale() application = QApplication(sys.argv) application.setStyle("Fusion") application.setQuitOnLastWindowClosed(False) diff --git a/src/volt-gui/welcome.py b/src/volt-gui/welcome.py new file mode 100644 index 0000000..ab5e5db --- /dev/null +++ b/src/volt-gui/welcome.py @@ -0,0 +1,148 @@ +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QHBoxLayout +from PySide6.QtWidgets import QMainWindow +from PySide6.QtWidgets import QPushButton +from PySide6.QtWidgets import QStackedWidget +from PySide6.QtWidgets import QVBoxLayout +from PySide6.QtWidgets import QWidget + +from themes import STANDARD_BUTTON_HEIGHT +from themes import STANDARD_BUTTON_WIDTH +from ui import create_simple_sidebar_widget +from ui import create_tab_content_widget +from ui import WINDOW_MIN_HEIGHT +from ui import WINDOW_MIN_WIDTH + + +def get_welcome_settings() -> dict: + return { + "Welcome": { + "Welcome to volt gui": ( + ("text", "volt-gui is my AMD Adrenaline / NVIDIA Settings Linux Alternative.\n\nSettings are applied by volt, a Vulkan implicit layer, so they work on every Vulkan driver: RADV, ANV, NVK, AMDVLK, the NVIDIA proprietary driver, and anything else that supports Vulkan 1.0."), + ("text", "The layer sticks to core Vulkan 1.0 and the swapchain extension, so nothing here behaves differently from one driver to the next."), + ) + }, + "How it Works": { + "The volt Layer": ( + ("text", "Every setting in this application is written to a profile file at ~/.config/volt-gui/. The volt Vulkan layer reads that profile when a game starts and rewrites the Vulkan calls the game makes: samplers for texture filtering and mip selection, the swapchain for vsync, image count and compositing, device enumeration for GPU selection, presents for the frame limiter, and pipelines for the rendering toggles."), + ("text", "Settings are read once when a game starts and never change while it runs. Press Apply, then start the game again."), + ), + "What it Will Not Do": ( + ("text", "volt only changes what the game asks Vulkan for. It never draws anything itself, so sharpening, upscaling, frame generation, forced MSAA and overlays are all out of scope. Use MangoHud for an overlay and LACT for clocks and fan curves, or CoreCtrl if you also want CPU controls."), + ("text", "It also never turns anything on that the game left off. volt enables no device feature and no extension. Where a setting needs a feature, volt reads what the game asked for and applies the setting only if the game enabled it: that is how Anisotropic Filtering, Sample Shading, Alpha To One and Depth Clamp work, and where the game left the feature clear the setting is ignored and a line is logged. A setting that cannot be reached that way at all stays out, which keeps cubic filtering off the table, line width stays out because forcing it breaks games either way, and forced wireframe stays out because it is a wallhack. Where a game moves state onto an extension path, volt follows it there: a hook for an extension the game never enabled is simply unreachable."), + ) + }, + "Settings": { + "One Value Per Setting": ( + ("text", "Every setting is a single choice: the value volt forces, or default, which means volt does not touch what the game asked for. There is no range, no ordering between values, and nothing to get backwards."), + ("text", "A card offers values core Vulkan 1.0 and VK_KHR_swapchain define and nothing else. A value above that floor is shown in a log line, never offered and never read back from a profile."), + ("text", "Where the specification admits only what a query returned, a value your device did not report is not forced. volt keeps the game's own value and logs a warning, so a profile written on another machine never makes a call invalid."), + ("text", "Where the specification bounds a value, volt clamps the value it forces rather than the one the game supplied. A forced LOD bias is clamped to your device limit. A forced image count is clamped to what the surface allows, except on a swapchain the game created in a shared present mode, which keeps the single image the specification requires of it. That clamp is correctness rather than a choice, so it is not shown here."), + ), + "Where the Lists Come From": ( + ("text", "Many of the cards are filled in from your own hardware rather than from a list built into volt-gui. Present modes, image counts and alpha modes come from what the surface reports, the GPU list comes from what the driver enumerates, and anisotropy, mip levels and LOD bias run up to the limits your device gives. A card without the feature behind it holds nothing but default, and so does every device backed card until the probe has run: volt-gui offers no option it has not read."), + ("text", "It also means a profile written on another machine can name something this one cannot do, in which case that setting resets to default and volt-gui tells you which ones."), + ("text", "The rest carry fixed lists, because there is nothing to read. Nearest and linear are core Vulkan with no feature and no query behind them, so every driver has both and none of them says so. The Framerate settings have nothing to read at all: a game never tells Vulkan what frame rate it wants, so there is nothing on the device to ask."), + ), + "The Three Filter Cards": ( + ("text", "Three sampler fields, three cards. Nothing overrides anything. Linear is forced only onto a sampler whose own magnification filter, minification filter or mipmap mode is already linear, so every draw time rule the forced linear triggers is one the sampler's own linear already triggers on the same image."), + ("text", "In the order magnification, minification, mipmap:\n\n- retro: nearest, nearest, nearest.\n- bilinear: linear, linear, nearest.\n- trilinear: linear, linear, linear.\n- sharp pixel art without distant shimmer: nearest, linear, linear."), + ("text", "Magnification is what you see up close. Minification is most of the screen, and where mipmaps and anisotropic filtering do their work. Mipmap Mode is the blend between levels."), + ("text", "An unnormalized, subsampled, image processing or converted sampler keeps every field its own shape restricts, so a setting naming one of those fields leaves it alone and logs a line."), + ), + "The Frame Limiter": ( + ("text", "Frame Limit caps the rate at present time. Cadence sets which rate the limiter aims at, Method sets when it waits, Pacing sets how, and none of the three does anything until Limit is set."), + ("text", "Cadence is the rate the limiter aims at. fixed is your cap and nothing else. smooth paces at the slowest of the last few frames, so the fast frames wait for the slow ones and the cadence comes out even at whatever the machine is holding. dynamic reads exactly what smooth reads and then rounds it down to a quarter step of your cap, so it sits on a set rate rather than following the load. The steps are quarter steps of your cap's frame time, so they sit close together low down and far apart up top: a 60 cap steps 60, 48, 40, 34, 30, while a 240 cap steps 240, 192, 160, 137, 120. Both come from how consoles handle a machine that cannot hold its target, which is picking a rate it can hold and staying there. A console drops resolution to get there and volt cannot touch resolution, so frame handling is the one place the idea fits. A limiter can only make frames later, which is why neither reads the average: a frame slower than the average could never be paced up to it. Both climb back on their own, and neither goes faster than your cap. The trade is frames for evenness: fixed does nothing at all once the machine falls under the cap, so what you get is whatever the machine produced, one frame long and the next short. smooth and dynamic hold the short frames back to match the long ones, which costs you the frames you would have seen and buys you even spacing. dynamic changing step is visible, but it is one change rather than a different frame time every frame. Set fixed if the machine holds the cap, or if you want every frame you can get for the input latency."), + ("text", "Cadence and Method are separate cards because they answer different questions, and any pair of them works together. dynamic with late holds a set rate and still reads input as close to display time as it can."), + ("text", "sleep hands the whole wait to the kernel and costs nothing. sliced sleeps in short steps and rechecks the clock, which corrects for the kernel waking late. precise sleeps most of the interval then busy waits half a millisecond. spin busy waits the whole interval, the steadiest of the four and the only one that keeps a core awake."), + ), + "Every Setting Forces, Bar One": ( + ("text", "volt writes the value you picked into its own copy of the structure that carries it, so a setting lands whether or not the game consulted a query first. That holds for every card here except one."), + ("text", "Physical Device is the exception, and it is a fact about Vulkan rather than a choice. Nothing names the device a swapchain runs on: the game already holds a physical device by the time volt sees anything it could patch. Hiding the others from enumeration is the only lever there is, so a game that ignores enumeration order keeps the device it picked."), + ("text", "Where a query governs what is legal, volt filters that too. A present mode must be one the surface reported, so filtering the query means a game picking the first entry it is offered gets the right value without volt overriding anything, and a game that hardcodes gets it at the create call instead. Both halves, one setting."), + ("text", "A forced value the device did not report is not forced. Where the surface turns down the present mode or the composite alpha you named, volt keeps the game's own value and logs a warning. It never passes down a value that would make the call invalid."), + ) + }, + "Usage": { + "Launching Games": ( + ("text", "Prepend the volt launcher to your game command. It activates the layer for that process only and selects the profile:"), + ("code", "volt -- %command%", "Steam (Launch Options, default profile):"), + ("code", "volt myprofile -- %command%", "Steam (named profile):"), + ("code", "volt -- ./game", "Terminal:"), + ("code", "volt -- flatpak run com.example.Game", "Flatpak:"), + ), + "Default Behavior": ( + ("text", "Every setting defaults to \"default\", which means the layer does not touch that value and the application keeps its own choice. A profile with everything on default is a true passthrough."), + ), + "Seeing What Applied": ( + ("text", "Run the game from a terminal with VOLT_LOG=info and the layer prints what it applied and what the surface or the device turned down."), + ("code", "VOLT_LOG=info volt -- ./game", ""), + ("text", "Every setting gets a line: either the value the game asked for and the value volt wrote in its place, or the reason the setting did not land. The applied value is the one volt wrote, so a setting the device clamped shows what landed rather than what you picked."), + ("text", "A setting that needs a device feature the game left clear names that feature instead. The Framerate settings have no asked value, since a game never tells Vulkan what frame rate it wants, so they report what volt applied or say the profile did not set them."), + ("text", "The GPU line reads `asked N, applied M` when you set a gpu: N is the device the game used, M the one your profile picked. With no gpu in the profile it reads `asked N` alone."), + ("text", "Each distinct asked and applied pair prints once per device however many samplers, pipelines or swapchains the game creates, and a reason line prints each time a value is kept."), + ), + "The Probe": ( + ("text", "volt-gui runs volt-probe under the profile you are editing. It is what fills the setting lists with your hardware. Pressing Apply runs it again so those lists match the values you just saved, and switching profiles runs it again too."), + ("text", "For each backend it can reach it opens a one pixel window that is never mapped and creates a surface and a swapchain, so the layer sees every path it needs. It creates one sampler, records what the device reported, and exits. Nothing appears on screen and nothing is drawn."), + ("text", "It opens X11 and Wayland, each through a client library loaded at runtime. A machine missing one reports the other, a machine missing both still fills every device backed card, and those three cards hold nothing but default. The file carries one section per backend that opened, so a third backend later is a section and nothing else."), + ("text", "Present modes, image counts and alpha modes are answered against a surface rather than against the card, so your display path bounds them as much as your hardware does, and a short list there is the answer rather than a failure. Those three cards are also the only ones this touches. The card offers the union of what the backends reported, with every value naming the backends that reported it: mailbox (xcb, wayland), immediate (xcb). The profile stores the value, never the tag. Where a backend's answer does not match the game's surface, the layer already handles it: image count is clamped against the surface the game actually opened, and a present mode or alpha mode that surface turns down leaves the game's own value alone with a line in the log."), + ("text", "volt-probe is built by make and installed next to volt and volt-gui, so there is nothing extra to fetch. It loads libxcb and libwayland-client at runtime and needs neither."), + ("code", "volt --probe myprofile -- volt-probe", "Run it yourself:"), + ) + }, + "Profiles": { + "Profiles": ( + ("text", "Create profiles to switch between configurations per game.\n\n1. Press New in the bottom bar.\n2. Configure and Apply settings.\n3. Launch the game with that profile name, or switch profiles from the System Tray."), + ("text", "The launch command shown at the top of the window always matches the selected profile and can be copied directly into Steam."), + ) + }, + "Presets": { + "Presets": ( + ("text", "Presets fill the profile you have open with a starting point, arranged as a ladder from best looking to fastest:\n\n- Quality: trilinear filtering, a slight sharpening bias, every mip level allowed, 16x anisotropy, classic vsync, a 4 image swapchain, and precise pacing on an early wait.\n- Balanced: trilinear still, mailbox present for vsync without the latency, 8x anisotropy, sliced pacing.\n- Performance FPS: bilinear, a blurring bias, mailbox present, the swapchain held to 4 images and the cheaper sleep pacing.\n- Performance Low Latency: the same, aimed at input lag instead, with immediate present, a 2 image swapchain, a late wait and spin pacing, the steadiest of the four.\n- Potato FPS: bilinear, anisotropy off, a full step of blurring bias, the top two mips off the table, cutout smoothing off.\n- Potato Low Latency: the same again with immediate present, a 2 image swapchain and a late wait.\n\nNo preset sets Composite Alpha or Clipped Presentation: those depend on your compositor, so they go back to default and the choice stays yours."), + ("text", "Applying a preset replaces every value in the profile after a confirmation, so anything the preset does not set goes back to default. That includes the frame limit: the right cap depends on your display, so that choice stays yours."), + ("text", "A preset can name something your hardware does not offer, mailbox on a surface without it for instance. That setting resets to default and volt-gui says which ones, so the rest of the preset still lands."), + ("text", "The filter presets are also the answer to what the three filter cards should be set to. Quality and Balanced are trilinear, the rest are bilinear with hard mip cuts, and every one of them is spelled out card by card in The Three Filter Cards above."), + ) + }, + "Options": { + "Options": ( + ("text", "Changes to Options are saved automatically but only take effect after restarting volt-gui. This includes the theme, scaling, tray behavior, and all other preferences."), + ) + }, + } + + +def create_welcome_window_widget() -> QMainWindow: + window = QMainWindow() + window.setWindowTitle("volt-gui Welcome") + window.setMinimumSize(WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT) + central_widget = QWidget() + main_layout = QVBoxLayout(central_widget) + main_layout.setContentsMargins(8, 8, 8, 8) + main_layout.setSpacing(8) + content_layout = QHBoxLayout() + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(0) + welcome_settings = get_welcome_settings() + stacked_widget = QStackedWidget() + for section_data in welcome_settings.values(): + stacked_widget.addWidget(create_tab_content_widget("", section_data)["tab"]) + content_layout.addWidget(create_simple_sidebar_widget(tuple(welcome_settings.keys()), stacked_widget)) + content_layout.addWidget(stacked_widget, 1) + main_layout.addLayout(content_layout, 1) + button_container = QWidget() + button_container.setProperty("buttonContainer", True) + button_layout = QHBoxLayout(button_container) + button_layout.setContentsMargins(8, 8, 8, 8) + button_layout.setSpacing(8) + button_layout.setAlignment(Qt.AlignVCenter) + close_button = QPushButton("Close") + close_button.setFixedSize(STANDARD_BUTTON_WIDTH, STANDARD_BUTTON_HEIGHT) + close_button.clicked.connect(window.close) + button_layout.addStretch(1) + button_layout.addWidget(close_button, 0, Qt.AlignVCenter) + button_layout.addStretch(1) + main_layout.addWidget(button_container) + window.setCentralWidget(central_widget) + return window diff --git a/src/volt-probe/dl.rs b/src/volt-probe/dl.rs new file mode 100644 index 0000000..410b98b --- /dev/null +++ b/src/volt-probe/dl.rs @@ -0,0 +1,38 @@ +use std::ffi::c_void; +use std::ffi::CString; +use std::mem; + +pub(crate) struct Library { + handle: *mut c_void, +} + +fn found(address: *mut c_void) -> Option<*mut c_void> { + match address.is_null() { + true => None, + false => Some(address), + } +} + +fn call_open_one(name: &str) -> Option<*mut c_void> { + let text = CString::new(name).ok()?; + found(unsafe { libc::dlopen(text.as_ptr(), libc::RTLD_NOW | libc::RTLD_LOCAL) }) +} + +pub(crate) fn call_open_library(names: &[&str]) -> Option { + names + .iter() + .find_map(|name| call_open_one(name)) + .map(|handle| Library { handle }) +} + +impl Library { + pub(crate) fn address(&self, name: &str) -> Option<*mut c_void> { + let text = CString::new(name).ok()?; + found(unsafe { libc::dlsym(self.handle, text.as_ptr()) }) + } + + pub(crate) fn symbol(&self, name: &str) -> Option { + self.address(name) + .map(|address| unsafe { mem::transmute_copy(&address) }) + } +} diff --git a/src/volt-probe/volt-probe.rs b/src/volt-probe/volt-probe.rs new file mode 100644 index 0000000..652bc93 --- /dev/null +++ b/src/volt-probe/volt-probe.rs @@ -0,0 +1,571 @@ +mod dl; +mod wayland; +mod xcb; + +use std::ffi::c_char; +use std::ffi::c_void; +use std::ffi::CStr; +use std::ffi::CString; +use std::process::exit; + +use ash::vk; + +const API_MAJOR: u32 = 1; +const API_MINOR: u32 = 0; +const API_PATCH: u32 = 0; +const API_VARIANT: u32 = 0; +const EXIT_OK: i32 = 0; +const EXIT_FAIL: i32 = 1; +const EXIT_UNSUPPORTED: i32 = 2; +const EXT_PORTABILITY_SUBSET: &str = "VK_KHR_portability_subset"; +const EXT_PROPERTIES_2: &str = "VK_KHR_get_physical_device_properties2"; +const NO_ARRAY_LAYERS: u32 = 0; +const ZERO_EXTENT: u32 = 0; +const IMAGE_LAYERS: u32 = 1; +const QUEUE_COUNT: u32 = 1; +const QUEUE_PRIORITY: f32 = 1.0; + +const EXT_SURFACE: &str = "VK_KHR_surface"; +const EXT_SWAPCHAIN: &str = "VK_KHR_swapchain"; +const FN_CREATE_SWAPCHAIN: &str = "vkCreateSwapchainKHR"; +const FN_DESTROY_SWAPCHAIN: &str = "vkDestroySwapchainKHR"; + +pub(crate) const WINDOW_EDGE: u16 = 1; + +pub(crate) struct Handles { + pub(crate) display: *mut c_void, + pub(crate) window: u64, +} + +pub(crate) struct Backend { + pub(crate) extension: &'static str, + pub(crate) open: fn() -> Option, + pub(crate) create_surface: + fn(&ash::Entry, &ash::Instance, &Handles) -> Option, + pub(crate) close: fn(&Handles), +} + +const BACKENDS: [Backend; 2] = [xcb::BACKEND, wayland::BACKEND]; + +type PfnCreateSwapchain = unsafe extern "system" fn( + vk::Device, + *const vk::SwapchainCreateInfoKHR<'_>, + *const vk::AllocationCallbacks<'_>, + *mut vk::SwapchainKHR, +) -> vk::Result; + +type PfnDestroySwapchain = unsafe extern "system" fn( + vk::Device, + vk::SwapchainKHR, + *const vk::AllocationCallbacks<'_>, +); + +fn wanted_extensions(has_surface: bool) -> Vec<&'static str> { + let surface: Vec<&'static str> = match has_surface { + true => vec![EXT_SURFACE], + false => Vec::new(), + }; + let backends: Vec<&'static str> = match has_surface { + true => BACKENDS.iter().map(|backend| backend.extension).collect(), + false => Vec::new(), + }; + surface + .into_iter() + .chain(std::iter::once(EXT_PROPERTIES_2)) + .chain(backends) + .collect() +} + +fn available_name(one: &vk::ExtensionProperties) -> Option { + unsafe { CStr::from_ptr(one.extension_name.as_ptr()) } + .to_str() + .ok() + .map(str::to_owned) +} + +fn call_available_names(entry: &ash::Entry) -> Vec { + unsafe { entry.enumerate_instance_extension_properties(None) } + .unwrap_or_default() + .iter() + .filter_map(available_name) + .collect() +} + +fn call_enabled_name_list(entry: &ash::Entry) -> Vec { + let available = call_available_names(entry); + let has_surface = available.iter().any(|one| one.as_str() == EXT_SURFACE); + wanted_extensions(has_surface) + .into_iter() + .filter(|name| available.iter().any(|one| one.as_str() == *name)) + .map(str::to_owned) + .collect() +} + +fn enabled_names(enabled: &[String]) -> Vec { + enabled + .iter() + .filter_map(|name| CString::new(name.as_str()).ok()) + .collect() +} + +fn name_pointers(names: &[CString]) -> Vec<*const c_char> { + names.iter().map(|name| name.as_ptr()).collect() +} + +fn family_draws(one: &vk::QueueFamilyProperties) -> bool { + one.queue_flags.contains(vk::QueueFlags::GRAPHICS) + || one.queue_flags.contains(vk::QueueFlags::COMPUTE) +} + +fn drawing_family(props: &[vk::QueueFamilyProperties]) -> Option { + props.iter().position(family_draws).map(|at| at as u32) +} + +fn asked_extent(caps: &vk::SurfaceCapabilitiesKHR) -> (u32, u32) { + match caps.current_extent.width { + u32::MAX => (WINDOW_EDGE as u32, WINDOW_EDGE as u32), + _ => (caps.current_extent.width, caps.current_extent.height), + } +} + +fn swapchain_extent(caps: &vk::SurfaceCapabilitiesKHR) -> vk::Extent2D { + let (width, height) = asked_extent(caps); + vk::Extent2D { + width: width.clamp(caps.min_image_extent.width, caps.max_image_extent.width), + height: height.clamp(caps.min_image_extent.height, caps.max_image_extent.height), + } +} + +fn surface_usable(caps: &vk::SurfaceCapabilitiesKHR) -> bool { + caps.current_extent.width != ZERO_EXTENT + && caps.current_extent.height != ZERO_EXTENT + && caps.max_image_array_layers > NO_ARRAY_LAYERS + && caps + .supported_usage_flags + .contains(vk::ImageUsageFlags::COLOR_ATTACHMENT) + && caps.supported_composite_alpha.as_raw() != 0 +} + +fn supported_alpha(mask: vk::CompositeAlphaFlagsKHR) -> vk::CompositeAlphaFlagsKHR { + vk::CompositeAlphaFlagsKHR::from_raw(mask.as_raw() & mask.as_raw().wrapping_neg()) +} + +fn supported_transform(caps: &vk::SurfaceCapabilitiesKHR) -> vk::SurfaceTransformFlagsKHR { + match caps.supported_transforms.contains(caps.current_transform) { + true => caps.current_transform, + false => vk::SurfaceTransformFlagsKHR::from_raw( + caps.supported_transforms.as_raw() & caps.supported_transforms.as_raw().wrapping_neg(), + ), + } +} + +fn swapchain_info( + surface: vk::SurfaceKHR, + format: vk::SurfaceFormatKHR, + caps: &vk::SurfaceCapabilitiesKHR, +) -> vk::SwapchainCreateInfoKHR<'static> { + vk::SwapchainCreateInfoKHR { + surface, + min_image_count: caps.min_image_count, + image_format: format.format, + image_color_space: format.color_space, + image_extent: swapchain_extent(caps), + image_array_layers: IMAGE_LAYERS, + image_usage: vk::ImageUsageFlags::COLOR_ATTACHMENT, + image_sharing_mode: vk::SharingMode::EXCLUSIVE, + pre_transform: supported_transform(caps), + composite_alpha: supported_alpha(caps.supported_composite_alpha), + present_mode: vk::PresentModeKHR::FIFO, + clipped: vk::TRUE, + ..Default::default() + } +} + +fn sampler_info() -> vk::SamplerCreateInfo<'static> { + vk::SamplerCreateInfo { + mag_filter: vk::Filter::LINEAR, + min_filter: vk::Filter::LINEAR, + mipmap_mode: vk::SamplerMipmapMode::LINEAR, + ..Default::default() + } +} + +fn call_entry() -> Option { + unsafe { ash::Entry::load() }.ok() +} + +fn call_create_instance(entry: &ash::Entry, enabled: &[String]) -> Option { + let names = enabled_names(enabled); + let pointers = name_pointers(&names); + let application = vk::ApplicationInfo { + api_version: vk::make_api_version(API_VARIANT, API_MAJOR, API_MINOR, API_PATCH), + ..Default::default() + }; + let info = vk::InstanceCreateInfo { + p_application_info: &application, + enabled_extension_count: pointers.len() as u32, + pp_enabled_extension_names: pointers.as_ptr(), + ..Default::default() + }; + unsafe { entry.create_instance(&info, None) }.ok() +} + +enum DeviceOutcome { + Ready(vk::PhysicalDevice), + Unsupported, + Missing, +} + +fn call_device_extension_names(instance: &ash::Instance, phys: vk::PhysicalDevice) -> Vec { + unsafe { instance.enumerate_device_extension_properties(phys) } + .unwrap_or_default() + .iter() + .filter_map(available_name) + .collect() +} + +fn portability_listed(names: &[String]) -> bool { + names.iter().any(|one| one == EXT_PORTABILITY_SUBSET) +} + +fn outcome_for( + phys: vk::PhysicalDevice, + listed: bool, + support: bool, +) -> DeviceOutcome { + match (listed, support) { + (true, false) => DeviceOutcome::Unsupported, + (_, _) => DeviceOutcome::Ready(phys), + } +} + +fn call_first_physical( + instance: &ash::Instance, + support: bool, +) -> DeviceOutcome { + match unsafe { instance.enumerate_physical_devices() } + .ok() + .and_then(|all| all.first().copied()) + { + None => DeviceOutcome::Missing, + Some(phys) => outcome_for( + phys, + portability_listed(&call_device_extension_names(instance, phys)), + support, + ), + } +} + +fn portability_support(enabled: &[String]) -> bool { + enabled.iter().any(|one| one.as_str() == EXT_PROPERTIES_2) +} + +fn call_surface_supported( + loader: &ash::khr::surface::Instance, + phys: vk::PhysicalDevice, + family: u32, + surface: vk::SurfaceKHR, +) -> bool { + unsafe { loader.get_physical_device_surface_support(phys, family, surface) }.unwrap_or(false) +} + +fn call_format_supported( + instance: &ash::Instance, + phys: vk::PhysicalDevice, + format: vk::Format, +) -> bool { + unsafe { + instance.get_physical_device_image_format_properties( + phys, + format, + vk::ImageType::TYPE_2D, + vk::ImageTiling::OPTIMAL, + vk::ImageUsageFlags::COLOR_ATTACHMENT, + vk::ImageCreateFlags::empty(), + ) + } + .is_ok() +} + +fn call_drawing_family(instance: &ash::Instance, phys: vk::PhysicalDevice) -> Option { + drawing_family(&unsafe { instance.get_physical_device_queue_family_properties(phys) }) +} + +fn call_family_count(instance: &ash::Instance, phys: vk::PhysicalDevice) -> u32 { + unsafe { instance.get_physical_device_queue_family_properties(phys) }.len() as u32 +} + +fn call_surface_family( + instance: &ash::Instance, + phys: vk::PhysicalDevice, + loader: &ash::khr::surface::Instance, + surface: vk::SurfaceKHR, +) -> bool { + (0..call_family_count(instance, phys)) + .any(|family| call_surface_supported(loader, phys, family, surface)) +} + +fn device_extensions_to_enable( + instance_enabled: &[String], + listed: &[String], + support: bool, +) -> Vec<&'static str> { + let has_surface = instance_enabled.iter().any(|one| one.as_str() == EXT_SURFACE); + let mut out: Vec<&'static str> = Vec::new(); + match has_surface && listed.iter().any(|one| one.as_str() == EXT_SWAPCHAIN) { + true => out.push(EXT_SWAPCHAIN), + false => (), + } + match support && listed.iter().any(|one| one.as_str() == EXT_PORTABILITY_SUBSET) { + true => out.push(EXT_PORTABILITY_SUBSET), + false => (), + } + out +} + +fn call_create_device( + instance: &ash::Instance, + phys: vk::PhysicalDevice, + family: u32, + instance_enabled: &[String], + support: bool, +) -> Option<(ash::Device, bool)> { + let listed = call_device_extension_names(instance, phys); + let wanted = device_extensions_to_enable(instance_enabled, &listed, support); + let names: Vec = wanted + .iter() + .filter_map(|name| CString::new(*name).ok()) + .collect(); + let pointers: Vec<*const c_char> = names.iter().map(|name| name.as_ptr()).collect(); + let priorities = [QUEUE_PRIORITY]; + let queue = vk::DeviceQueueCreateInfo { + queue_family_index: family, + queue_count: QUEUE_COUNT, + p_queue_priorities: priorities.as_ptr(), + ..Default::default() + }; + let info = vk::DeviceCreateInfo { + queue_create_info_count: QUEUE_COUNT, + p_queue_create_infos: &queue, + enabled_extension_count: pointers.len() as u32, + pp_enabled_extension_names: pointers.as_ptr(), + ..Default::default() + }; + unsafe { instance.create_device(phys, &info, None) } + .ok() + .map(|device| (device, wanted.contains(&EXT_SWAPCHAIN))) +} + +fn call_first_format( + loader: &ash::khr::surface::Instance, + phys: vk::PhysicalDevice, + surface: vk::SurfaceKHR, +) -> Option { + unsafe { loader.get_physical_device_surface_formats(phys, surface) } + .ok() + .and_then(|all| all.first().copied()) +} + +fn call_surface_caps( + loader: &ash::khr::surface::Instance, + phys: vk::PhysicalDevice, + surface: vk::SurfaceKHR, +) -> Option { + unsafe { loader.get_physical_device_surface_capabilities(phys, surface) }.ok() +} + +fn call_device_fn(instance: &ash::Instance, device: &ash::Device, name: &str) -> Option { + let cname = CString::new(name).ok()?; + unsafe { (instance.fp_v1_0().get_device_proc_addr)(device.handle(), cname.as_ptr()) } + .map(|found| unsafe { std::mem::transmute_copy(&found) }) +} + +fn call_swapchain_result( + result: vk::Result, + swapchain: vk::SwapchainKHR, +) -> Option { + match result { + vk::Result::SUCCESS => Some(swapchain), + _ => None, + } +} + +fn call_create_swapchain( + fp: PfnCreateSwapchain, + device: &ash::Device, + surface: vk::SurfaceKHR, + format: vk::SurfaceFormatKHR, + caps: &vk::SurfaceCapabilitiesKHR, +) -> Option { + let info = swapchain_info(surface, format, caps); + let mut swapchain = vk::SwapchainKHR::null(); + call_swapchain_result( + unsafe { fp(device.handle(), &info, std::ptr::null(), &mut swapchain) }, + swapchain, + ) +} + +fn call_create_sampler(device: &ash::Device) -> Option { + unsafe { device.create_sampler(&sampler_info(), None) }.ok() +} + +fn call_exercise_sampler(device: &ash::Device) -> Option<()> { + let sampler = call_create_sampler(device)?; + unsafe { device.destroy_sampler(sampler, None) }; + Some(()) +} + +fn call_on_swapchain( + instance: &ash::Instance, + phys: vk::PhysicalDevice, + device: &ash::Device, + surfaces: &ash::khr::surface::Instance, + surface: vk::SurfaceKHR, +) -> Option<()> { + match call_surface_family(instance, phys, surfaces, surface) { + true => (), + false => return None, + } + let caps = call_surface_caps(surfaces, phys, surface)?; + match surface_usable(&caps) { + true => (), + false => return None, + } + let format = call_first_format(surfaces, phys, surface)?; + match call_format_supported(instance, phys, format.format) { + true => (), + false => return None, + } + let create: PfnCreateSwapchain = call_device_fn(instance, device, FN_CREATE_SWAPCHAIN)?; + let destroy: PfnDestroySwapchain = call_device_fn(instance, device, FN_DESTROY_SWAPCHAIN)?; + let swapchain = call_create_swapchain(create, device, surface, format, &caps)?; + unsafe { destroy(device.handle(), swapchain, std::ptr::null()) }; + Some(()) +} + +fn call_on_surface( + entry: &ash::Entry, + instance: &ash::Instance, + phys: vk::PhysicalDevice, + device: &ash::Device, + backend: &Backend, + handles: &Handles, +) -> Option<()> { + let surfaces = ash::khr::surface::Instance::new(entry, instance); + let surface = (backend.create_surface)(entry, instance, handles)?; + let done = call_on_swapchain(instance, phys, device, &surfaces, surface); + unsafe { surfaces.destroy_surface(surface, None) }; + done +} + +fn call_on_backend( + entry: &ash::Entry, + instance: &ash::Instance, + phys: vk::PhysicalDevice, + device: &ash::Device, + instance_enabled: &[String], + backend: &Backend, +) -> Option<()> { + match instance_enabled.iter().any(|name| name.as_str() == backend.extension) { + false => None, + true => { + let handles = (backend.open)()?; + let done = call_on_surface(entry, instance, phys, device, backend, &handles); + (backend.close)(&handles); + done + } + } +} + +fn call_every_backend( + entry: &ash::Entry, + instance: &ash::Instance, + phys: vk::PhysicalDevice, + device: &ash::Device, + instance_enabled: &[String], +) { + BACKENDS.iter().for_each(|backend| { + let _ = call_on_backend(entry, instance, phys, device, instance_enabled, backend); + }); +} + +fn call_on_device( + entry: &ash::Entry, + instance: &ash::Instance, + phys: vk::PhysicalDevice, + device: &ash::Device, + instance_enabled: &[String], + swapchain: bool, +) -> Option<()> { + call_exercise_sampler(device)?; + match swapchain { + true => call_every_backend(entry, instance, phys, device, instance_enabled), + false => (), + } + Some(()) +} + +fn call_with_device( + entry: &ash::Entry, + instance: &ash::Instance, + phys: vk::PhysicalDevice, + instance_enabled: &[String], + support: bool, +) -> Option<()> { + let family = call_drawing_family(instance, phys)?; + let (device, swapchain) = + call_create_device(instance, phys, family, instance_enabled, support)?; + let done = call_on_device(entry, instance, phys, &device, instance_enabled, swapchain); + unsafe { device.destroy_device(None) }; + done +} + +fn exit_of(done: Option<()>) -> i32 { + match done { + Some(()) => EXIT_OK, + None => EXIT_FAIL, + } +} + +fn call_on_instance( + entry: &ash::Entry, + instance: &ash::Instance, + instance_enabled: &[String], + support: bool, +) -> i32 { + match call_first_physical(instance, support) { + DeviceOutcome::Missing => EXIT_FAIL, + DeviceOutcome::Unsupported => EXIT_UNSUPPORTED, + DeviceOutcome::Ready(phys) => exit_of(call_with_device( + entry, + instance, + phys, + instance_enabled, + support, + )), + } +} + +fn call_with_instance(entry: &ash::Entry) -> i32 { + let enabled = call_enabled_name_list(entry); + let support = portability_support(&enabled); + match call_create_instance(entry, &enabled) { + None => EXIT_FAIL, + Some(instance) => { + let code = call_on_instance(entry, &instance, &enabled, support); + unsafe { instance.destroy_instance(None) }; + code + } + } +} + +fn call_status() -> i32 { + match call_entry() { + None => EXIT_FAIL, + Some(entry) => call_with_instance(&entry), + } +} + +fn main() { + exit(call_status()); +} diff --git a/src/volt-probe/wayland.rs b/src/volt-probe/wayland.rs new file mode 100644 index 0000000..6a15235 --- /dev/null +++ b/src/volt-probe/wayland.rs @@ -0,0 +1,300 @@ +use std::ffi::c_char; +use std::ffi::c_int; +use std::ffi::c_void; +use std::ffi::CStr; +use std::ffi::CString; +use std::mem; +use std::ptr; +use std::sync::OnceLock; + +use ash::vk; + +use crate::dl::call_open_library; +use crate::dl::Library; +use crate::Backend; +use crate::Handles; + +const LIB_VERSIONED: &str = "libwayland-client.so.0"; +const LIB_PLAIN: &str = "libwayland-client.so"; +const LIBRARIES: [&str; 2] = [LIB_VERSIONED, LIB_PLAIN]; +const SYM_DISPLAY_CONNECT: &str = "wl_display_connect"; +const SYM_DISPLAY_DISCONNECT: &str = "wl_display_disconnect"; +const SYM_DISPLAY_ROUNDTRIP: &str = "wl_display_roundtrip"; +const SYM_PROXY_MARSHAL: &str = "wl_proxy_marshal_flags"; +const SYM_PROXY_ADD_LISTENER: &str = "wl_proxy_add_listener"; +const SYM_PROXY_GET_VERSION: &str = "wl_proxy_get_version"; +const SYM_REGISTRY_INTERFACE: &str = "wl_registry_interface"; +const SYM_COMPOSITOR_INTERFACE: &str = "wl_compositor_interface"; +const SYM_SURFACE_INTERFACE: &str = "wl_surface_interface"; +const EXT_SURFACE: &str = "VK_KHR_wayland_surface"; +const FN_CREATE_SURFACE: &str = "vkCreateWaylandSurfaceKHR"; +const SURFACE_TYPE: i32 = 1000006000; +const COMPOSITOR_NAME: &[u8] = b"wl_compositor"; +const COMPOSITOR_VERSION: u32 = 1; +const DISPLAY_GET_REGISTRY: u32 = 1; +const REGISTRY_BIND: u32 = 0; +const COMPOSITOR_CREATE_SURFACE: u32 = 0; +const SURFACE_DESTROY: u32 = 0; +const MARSHAL_DESTROY: u32 = 1; +const NO_FLAGS: u32 = 0; + +pub(crate) const BACKEND: Backend = Backend { + extension: EXT_SURFACE, + open: call_open, + create_surface: call_create_surface, + close: call_close, +}; + +#[repr(C)] +struct WlInterfaceHead { + name: *const c_char, +} + +#[repr(C)] +struct RegistryListener { + global: unsafe extern "C" fn(*mut c_void, *mut c_void, u32, *const c_char, u32), + global_remove: unsafe extern "C" fn(*mut c_void, *mut c_void, u32), +} + +#[repr(C)] +struct VkWaylandSurfaceCreateInfo { + s_type: vk::StructureType, + p_next: *const c_void, + flags: u32, + display: *mut c_void, + surface: *mut c_void, +} + +type PfnCreateWaylandSurface = unsafe extern "system" fn( + vk::Instance, + *const VkWaylandSurfaceCreateInfo, + *const vk::AllocationCallbacks<'_>, + *mut vk::SurfaceKHR, +) -> vk::Result; + +type PfnMarshal = + unsafe extern "C" fn(*mut c_void, u32, *const c_void, u32, u32, ...) -> *mut c_void; + +struct Wl { + connect: unsafe extern "C" fn(*const c_char) -> *mut c_void, + disconnect: unsafe extern "C" fn(*mut c_void), + roundtrip: unsafe extern "C" fn(*mut c_void) -> c_int, + marshal: PfnMarshal, + add_listener: unsafe extern "C" fn(*mut c_void, *const c_void, *mut c_void) -> c_int, + get_version: unsafe extern "C" fn(*mut c_void) -> u32, + registry_interface: *const c_void, + compositor_interface: *const c_void, + surface_interface: *const c_void, +} + +unsafe impl Send for Wl {} +unsafe impl Sync for Wl {} + +struct BindState { + wl: &'static Wl, + compositor: *mut c_void, +} + +static WL: OnceLock> = OnceLock::new(); + +static LISTENER: RegistryListener = RegistryListener { + global: on_global, + global_remove: on_global_remove, +}; + +fn loaded(library: &Library) -> Option { + Some(Wl { + connect: library.symbol(SYM_DISPLAY_CONNECT)?, + disconnect: library.symbol(SYM_DISPLAY_DISCONNECT)?, + roundtrip: library.symbol(SYM_DISPLAY_ROUNDTRIP)?, + marshal: library.symbol(SYM_PROXY_MARSHAL)?, + add_listener: library.symbol(SYM_PROXY_ADD_LISTENER)?, + get_version: library.symbol(SYM_PROXY_GET_VERSION)?, + registry_interface: library.address(SYM_REGISTRY_INTERFACE)?, + compositor_interface: library.address(SYM_COMPOSITOR_INTERFACE)?, + surface_interface: library.address(SYM_SURFACE_INTERFACE)?, + }) +} + +fn wayland() -> Option<&'static Wl> { + WL.get_or_init(|| call_open_library(&LIBRARIES).and_then(|library| loaded(&library))) + .as_ref() +} + +fn interface_name(interface: *const c_void) -> *const c_char { + unsafe { (*(interface as *const WlInterfaceHead)).name } +} + +fn call_bind(wl: &Wl, registry: *mut c_void, name: u32) -> *mut c_void { + unsafe { + (wl.marshal)( + registry, + REGISTRY_BIND, + wl.compositor_interface, + COMPOSITOR_VERSION, + NO_FLAGS, + name, + interface_name(wl.compositor_interface), + COMPOSITOR_VERSION, + ptr::null_mut::(), + ) + } +} + +extern "C" fn on_global( + data: *mut c_void, + registry: *mut c_void, + name: u32, + interface: *const c_char, + _version: u32, +) { + let state = unsafe { &mut *(data as *mut BindState) }; + match unsafe { CStr::from_ptr(interface) }.to_bytes() == COMPOSITOR_NAME { + true => state.compositor = call_bind(state.wl, registry, name), + false => (), + } +} + +extern "C" fn on_global_remove(_data: *mut c_void, _registry: *mut c_void, _name: u32) {} + +fn call_registry(wl: &Wl, display: *mut c_void) -> *mut c_void { + unsafe { + (wl.marshal)( + display, + DISPLAY_GET_REGISTRY, + wl.registry_interface, + (wl.get_version)(display), + NO_FLAGS, + ptr::null_mut::(), + ) + } +} + +fn call_compositor(wl: &'static Wl, display: *mut c_void) -> *mut c_void { + let registry = call_registry(wl, display); + let mut state = BindState { + wl, + compositor: ptr::null_mut(), + }; + unsafe { + (wl.add_listener)( + registry, + &LISTENER as *const RegistryListener as *const c_void, + &mut state as *mut BindState as *mut c_void, + ) + }; + unsafe { (wl.roundtrip)(display) }; + state.compositor +} + +fn call_wl_surface(wl: &Wl, compositor: *mut c_void) -> *mut c_void { + unsafe { + (wl.marshal)( + compositor, + COMPOSITOR_CREATE_SURFACE, + wl.surface_interface, + (wl.get_version)(compositor), + NO_FLAGS, + ptr::null_mut::(), + ) + } +} + +fn call_dropped(wl: &Wl, display: *mut c_void) -> Option { + unsafe { (wl.disconnect)(display) }; + None +} + +fn call_handles(wl: &Wl, display: *mut c_void, surface: *mut c_void) -> Option { + match surface.is_null() { + true => call_dropped(wl, display), + false => Some(Handles { + display, + window: surface as u64, + }), + } +} + +fn call_bound(wl: &Wl, display: *mut c_void, compositor: *mut c_void) -> Option { + match compositor.is_null() { + true => call_dropped(wl, display), + false => call_handles(wl, display, call_wl_surface(wl, compositor)), + } +} + +fn call_connected(display: *mut c_void) -> Option<*mut c_void> { + match display.is_null() { + true => None, + false => Some(display), + } +} + +fn call_open() -> Option { + let wl = wayland()?; + let display = call_connected(unsafe { (wl.connect)(ptr::null()) })?; + call_bound(wl, display, call_compositor(wl, display)) +} + +fn call_destroy_wl_surface(wl: &Wl, surface: *mut c_void) { + unsafe { + (wl.marshal)( + surface, + SURFACE_DESTROY, + ptr::null::(), + (wl.get_version)(surface), + MARSHAL_DESTROY, + ) + }; +} + +fn call_teardown(wl: &Wl, handles: &Handles) { + call_destroy_wl_surface(wl, handles.window as *mut c_void); + unsafe { (wl.disconnect)(handles.display) }; +} + +fn call_close(handles: &Handles) { + match wayland() { + Some(wl) => call_teardown(wl, handles), + None => (), + } +} + +fn surface_info(handles: &Handles) -> VkWaylandSurfaceCreateInfo { + VkWaylandSurfaceCreateInfo { + s_type: vk::StructureType::from_raw(SURFACE_TYPE), + p_next: ptr::null(), + flags: NO_FLAGS, + display: handles.display, + surface: handles.window as *mut c_void, + } +} + +fn call_surface_fn( + entry: &ash::Entry, + instance: &ash::Instance, +) -> Option { + let name = CString::new(FN_CREATE_SURFACE).ok()?; + unsafe { entry.get_instance_proc_addr(instance.handle(), name.as_ptr()) } + .map(|found| unsafe { mem::transmute(found) }) +} + +fn call_surface_result(result: vk::Result, surface: vk::SurfaceKHR) -> Option { + match result { + vk::Result::SUCCESS => Some(surface), + _ => None, + } +} + +fn call_create_surface( + entry: &ash::Entry, + instance: &ash::Instance, + handles: &Handles, +) -> Option { + let create = call_surface_fn(entry, instance)?; + let info = surface_info(handles); + let mut surface = vk::SurfaceKHR::null(); + call_surface_result( + unsafe { create(instance.handle(), &info, ptr::null(), &mut surface) }, + surface, + ) +} diff --git a/src/volt-probe/xcb.rs b/src/volt-probe/xcb.rs new file mode 100644 index 0000000..c917954 --- /dev/null +++ b/src/volt-probe/xcb.rs @@ -0,0 +1,239 @@ +use std::ffi::c_char; +use std::ffi::c_int; +use std::ffi::c_void; +use std::ffi::CString; +use std::mem; +use std::ptr; +use std::sync::OnceLock; + +use ash::vk; + +use crate::dl::call_open_library; +use crate::dl::Library; +use crate::Backend; +use crate::Handles; +use crate::WINDOW_EDGE; + +const LIB_VERSIONED: &str = "libxcb.so.1"; +const LIB_PLAIN: &str = "libxcb.so"; +const LIBRARIES: [&str; 2] = [LIB_VERSIONED, LIB_PLAIN]; +const SYM_CONNECT: &str = "xcb_connect"; +const SYM_DISCONNECT: &str = "xcb_disconnect"; +const SYM_HAS_ERROR: &str = "xcb_connection_has_error"; +const SYM_GET_SETUP: &str = "xcb_get_setup"; +const SYM_ROOTS_ITERATOR: &str = "xcb_setup_roots_iterator"; +const SYM_GENERATE_ID: &str = "xcb_generate_id"; +const SYM_CREATE_WINDOW: &str = "xcb_create_window"; +const SYM_FLUSH: &str = "xcb_flush"; +const EXT_SURFACE: &str = "VK_KHR_xcb_surface"; +const FN_CREATE_SURFACE: &str = "vkCreateXcbSurfaceKHR"; +const SURFACE_TYPE: i32 = 1000005000; +const WINDOW_ORIGIN: i16 = 0; +const WINDOW_BORDER: u16 = 0; +const WINDOW_INPUT_OUTPUT: u16 = 1; +const COPY_FROM_PARENT: u8 = 0; +const NO_VALUES: u32 = 0; +const NO_FLAGS: u32 = 0; +const NO_ERROR: c_int = 0; + +pub(crate) const BACKEND: Backend = Backend { + extension: EXT_SURFACE, + open: call_open, + create_surface: call_create_surface, + close: call_close, +}; + +#[repr(C)] +struct XcbScreen { + root: u32, + default_colormap: u32, + white_pixel: u32, + black_pixel: u32, + current_input_masks: u32, + width_in_pixels: u16, + height_in_pixels: u16, + width_in_millimeters: u16, + height_in_millimeters: u16, + min_installed_maps: u16, + max_installed_maps: u16, + root_visual: u32, + backing_stores: u8, + save_unders: u8, + root_depth: u8, + allowed_depths_len: u8, +} + +#[repr(C)] +struct XcbScreenIterator { + data: *mut XcbScreen, + rem: c_int, + index: c_int, +} + +#[repr(C)] +struct VkXcbSurfaceCreateInfo { + s_type: vk::StructureType, + p_next: *const c_void, + flags: u32, + connection: *mut c_void, + window: u32, +} + +type PfnCreateXcbSurface = unsafe extern "system" fn( + vk::Instance, + *const VkXcbSurfaceCreateInfo, + *const vk::AllocationCallbacks<'_>, + *mut vk::SurfaceKHR, +) -> vk::Result; + +struct Xcb { + connect: unsafe extern "C" fn(*const c_char, *mut c_int) -> *mut c_void, + disconnect: unsafe extern "C" fn(*mut c_void), + has_error: unsafe extern "C" fn(*mut c_void) -> c_int, + get_setup: unsafe extern "C" fn(*mut c_void) -> *const c_void, + roots_iterator: unsafe extern "C" fn(*const c_void) -> XcbScreenIterator, + generate_id: unsafe extern "C" fn(*mut c_void) -> u32, + create_window: unsafe extern "C" fn( + *mut c_void, + u8, + u32, + u32, + i16, + i16, + u16, + u16, + u16, + u16, + u32, + u32, + *const u32, + ) -> u32, + flush: unsafe extern "C" fn(*mut c_void) -> c_int, +} + +unsafe impl Send for Xcb {} +unsafe impl Sync for Xcb {} + +static XCB: OnceLock> = OnceLock::new(); + +fn loaded(library: &Library) -> Option { + Some(Xcb { + connect: library.symbol(SYM_CONNECT)?, + disconnect: library.symbol(SYM_DISCONNECT)?, + has_error: library.symbol(SYM_HAS_ERROR)?, + get_setup: library.symbol(SYM_GET_SETUP)?, + roots_iterator: library.symbol(SYM_ROOTS_ITERATOR)?, + generate_id: library.symbol(SYM_GENERATE_ID)?, + create_window: library.symbol(SYM_CREATE_WINDOW)?, + flush: library.symbol(SYM_FLUSH)?, + }) +} + +fn xcb() -> Option<&'static Xcb> { + XCB.get_or_init(|| call_open_library(&LIBRARIES).and_then(|library| loaded(&library))) + .as_ref() +} + +fn call_dropped(xcb: &Xcb, connection: *mut c_void) -> Option { + unsafe { (xcb.disconnect)(connection) }; + None +} + +fn call_connected(xcb: &Xcb, connection: *mut c_void) -> Option<*mut c_void> { + match unsafe { (xcb.has_error)(connection) } { + NO_ERROR => Some(connection), + _ => call_dropped(xcb, connection), + } +} + +fn call_root_screen(xcb: &Xcb, connection: *mut c_void) -> Option<*mut XcbScreen> { + match unsafe { (xcb.roots_iterator)((xcb.get_setup)(connection)) }.data { + screen if screen.is_null() => None, + screen => Some(screen), + } +} + +fn call_place_window(xcb: &Xcb, connection: *mut c_void, screen: *mut XcbScreen) -> u32 { + let handle = unsafe { (xcb.generate_id)(connection) }; + unsafe { + (xcb.create_window)( + connection, + COPY_FROM_PARENT, + handle, + (*screen).root, + WINDOW_ORIGIN, + WINDOW_ORIGIN, + WINDOW_EDGE, + WINDOW_EDGE, + WINDOW_BORDER, + WINDOW_INPUT_OUTPUT, + (*screen).root_visual, + NO_VALUES, + ptr::null(), + ) + }; + unsafe { (xcb.flush)(connection) }; + handle +} + +fn call_windowed(xcb: &Xcb, connection: *mut c_void) -> Option { + match call_root_screen(xcb, connection) { + None => call_dropped(xcb, connection), + Some(screen) => Some(Handles { + display: connection, + window: call_place_window(xcb, connection, screen) as u64, + }), + } +} + +fn call_open() -> Option { + let xcb = xcb()?; + let connection = call_connected(xcb, unsafe { + (xcb.connect)(ptr::null(), ptr::null_mut()) + })?; + call_windowed(xcb, connection) +} + +fn call_close(handles: &Handles) { + match xcb() { + Some(xcb) => unsafe { (xcb.disconnect)(handles.display) }, + None => (), + } +} + +fn surface_info(handles: &Handles) -> VkXcbSurfaceCreateInfo { + VkXcbSurfaceCreateInfo { + s_type: vk::StructureType::from_raw(SURFACE_TYPE), + p_next: ptr::null(), + flags: NO_FLAGS, + connection: handles.display, + window: handles.window as u32, + } +} + +fn call_surface_fn(entry: &ash::Entry, instance: &ash::Instance) -> Option { + let name = CString::new(FN_CREATE_SURFACE).ok()?; + unsafe { entry.get_instance_proc_addr(instance.handle(), name.as_ptr()) } + .map(|found| unsafe { mem::transmute(found) }) +} + +fn call_surface_result(result: vk::Result, surface: vk::SurfaceKHR) -> Option { + match result { + vk::Result::SUCCESS => Some(surface), + _ => None, + } +} + +fn call_create_surface( + entry: &ash::Entry, + instance: &ash::Instance, + handles: &Handles, +) -> Option { + let create = call_surface_fn(entry, instance)?; + let info = surface_info(handles); + let mut surface = vk::SurfaceKHR::null(); + call_surface_result( + unsafe { create(instance.handle(), &info, ptr::null(), &mut surface) }, + surface, + ) +} diff --git a/src/volt/checks.rs b/src/volt/checks.rs new file mode 100644 index 0000000..9c3c6e9 --- /dev/null +++ b/src/volt/checks.rs @@ -0,0 +1,768 @@ +use std::ffi::c_void; +use std::mem; +use std::ptr; + +use ash::vk; + +use crate::config::parse_float; +use crate::config::parse_settings; +use crate::config::sanitized_name; +use crate::consts::CadenceChoice; +use crate::consts::DEFAULT_PROFILE; +use crate::consts::FEATURE_ANISOTROPY; +use crate::consts::MethodChoice; +use crate::consts::NOTE_NOT_SET; +use crate::consts::SETTING_ANISOTROPY; +use crate::consts::SETTING_FRAME_LIMIT; +use crate::consts::SETTING_PRESENT_MODE; +use crate::consts::TEXT_LINEAR; +use crate::consts::TEXT_NEAREST; +use crate::consts::TOGGLE_OFF; +use crate::consts::TOGGLE_ON; +use crate::consts::SOURCE_PUSH_INDEX; +use crate::consts::SOURCE_SHADER_RECORD_INDEX; +use crate::consts::SWAPCHAIN_COUNTER_TYPE; +use crate::consts::SWAPCHAIN_MODE_LIST_TYPE; +use crate::instance::copied_node; +use crate::instance::relinked_chain; +use crate::instance::VkChainNode; +use crate::instance::VkDescriptorSetAndBindingMappingEXT; +use crate::instance::VkSwapchainCounterCreateInfoEXT; +use crate::instance::provider_on; +use crate::layer::negotiated_version; +use crate::lists::filtered; +use crate::lists::forced; +use crate::lists::kept; +use crate::present::advanced; +use crate::present::target_interval_ns; +use crate::present::Timeline; +use crate::probe::render_surface; +use crate::probe::SurfaceFacts; +use crate::ranks::alpha_display; +use crate::ranks::alpha_parse; +use crate::ranks::alpha_semantic; +use crate::ranks::present_display; +use crate::ranks::present_parse; +use crate::ranks::present_semantic; +use crate::ranks::AlphaFacts; +use crate::ranks::PresentFacts; +use crate::report::feature_note; +use crate::report::filter_text; +use crate::report::applied_text; +use crate::report::number_text; +use crate::report::report_line; +use crate::sampler::embedded_sampler; +use crate::swapchain::present_narrowed; + +const UNKNOWN_MODE: i32 = 4242; +const UNKNOWN_ALPHA: u32 = 16; +const FIFO_MODE: i32 = 2; +const SHARED_MODE: i32 = 1000111000; +const OPAQUE_ALPHA: u32 = 1; +const NEAREST_FILTER: i32 = 0; +const LINEAR_FILTER: i32 = 1; +const FILTER_PROFILE: &str = "[textures]\nmag_filter = \"nearest\"\nmin_filter = \"linear\"\n"; +const ALPHA_ONE_PROFILE: &str = "[rendering]\nalpha_to_one = \"on\"\n"; +const CLAMP_PROFILE: &str = "[rendering]\ndepth_clamp = \"off\"\n"; +const INHERIT_ALPHA: u32 = 8; +const UNNAMED_MODE_PROFILE: &str = "[display]\npresent_mode = \"present mode 4242\"\n"; +const NAMED_MODE_PROFILE: &str = "[display]\npresent_mode = \"fifo\"\n"; + +const LIMIT_START_NS: u64 = 10_000; +const LIMIT_INTERVAL_NS: u64 = 1_000; +const OTHER_INTERVAL_NS: u64 = 2_000; +const LATE_NS: u64 = 100; +const LATE_FRAME_NS: u64 = 1_500; +const TWO_HUNDRED_FPS: f32 = 200.0; +const TWO_HUNDRED_FPS_NS: u64 = 5_000_000; +const FIXED_PROFILE: &str = "[framerate]\nframe_limit_cadence = \"fixed\"\n"; +const DYNAMIC_PROFILE: &str = "[framerate]\nframe_limit_cadence = \"dynamic\"\n"; +const PEAK_START_NS: u64 = 2_000; +const SLOW_FRAME_NS: u64 = 1_600; +const SMOOTH_TARGET_NS: u64 = 11_600; +const DYNAMIC_TARGET_NS: u64 = 11_750; +const FAST_FRAME_NS: u64 = 500; +const DECAY_FROM_NS: u64 = 4_000; +const DECAY_TO_NS: u64 = 3_000; +const SPIKE_PEAK_NS: u64 = 4_000; +const HITCH_NS: u64 = 100_000; +const ASKED_MODE: &str = "fifo"; +const FORCED_MODE: &str = "mailbox"; +const ASKED_ANISO: &str = "off"; +const FORCED_LIMIT: &str = "60"; +const SAME_LINE: &str = "present_mode: asked fifo, applied fifo"; +const BOTH_LINE: &str = "present_mode: asked fifo, applied mailbox"; +const APPLIED_LINE: &str = "frame_limit: applied 60"; +const UNSET_LINE: &str = "frame_limit: the profile did not set it"; +const BLOCKED_LINE: &str = "anisotropy: the application did not enable samplerAnisotropy"; +const ANISO_SIXTEEN: f32 = 16.0; +const ANISO_SIXTEEN_TEXT: &str = "16"; +const BIAS_DOWN_TEXT: &str = "-0.6"; +const SURFACE_TAG: &str = "wayland"; +const SURFACE_PRESENT: [&str; 2] = ["mailbox", "fifo"]; +const SURFACE_ALPHA: [&str; 1] = ["opaque"]; +const SURFACE_MIN_IMAGES: u32 = 4; +const SURFACE_MAX_IMAGES: u32 = 0; +const SURFACE_SECTION: &str = "[wayland]\npresent_modes = \"mailbox;fifo\"\ncomposite_alphas = \"opaque\"\nmin_image_count = \"4\"\nmax_image_count = \"0\"\n"; +const UNKNOWN_CHAIN_TYPE: u32 = 424242; +const SOURCE_PUSH_DATA: i32 = 5; +const NO_COUNTERS: u32 = 0; +const MAILBOX_VALUE: i32 = 1; +const DEFAULT_NAME_MIXED: &str = "Default"; +const RESERVED_NAME_MIXED: &str = "Probe"; +const PLAIN_NAME: &str = "myprofile"; +const CORE_ONE_ONE: u32 = 4198400; +const CORE_ONE_ZERO: u32 = 4194304; +const SURFACE_EXT_NAME: &str = "VK_KHR_surface"; +const LOADER_OFFERS_LOW: u32 = 1; +const LOADER_OFFERS_HIGH: u32 = 5; +const LAYER_WANTS: u32 = 2; +const NAN_TEXT: &str = "NaN"; +const INF_TEXT: &str = "inf"; +const NEG_INF_TEXT: &str = "-inf"; +const FINITE_TEXT: &str = "1.5"; +const FINITE_VALUE: f32 = 1.5; +const LIST_FIRST: i32 = 1; +const LIST_SECOND: i32 = 2; +const LIST_THIRD: i32 = 3; +const LIST_MISSING: i32 = 9; +const QUEUE_2_COMMAND: &str = "vkGetDeviceQueue2"; +const SURFACE_CAPS_COMMAND: &str = "vkGetPhysicalDeviceSurfaceCapabilitiesKHR"; +const BIAS_DOWN: f32 = -0.6; + +#[test] +fn reads_one_spelling_of_the_default_profile_name() { + assert_eq!(sanitized_name(DEFAULT_NAME_MIXED), Some(DEFAULT_PROFILE.into())); + assert_eq!(sanitized_name(RESERVED_NAME_MIXED), None); + assert_eq!(sanitized_name(PLAIN_NAME), Some(PLAIN_NAME.into())); +} + +#[test] +fn hands_a_hook_out_where_any_one_provider_is_on() { + let none: std::collections::HashSet = std::collections::HashSet::new(); + let surface: std::collections::HashSet = + std::iter::once(SURFACE_EXT_NAME.to_string()).collect(); + assert!(provider_on(CORE_ONE_ONE, &none, QUEUE_2_COMMAND)); + assert!(!provider_on(CORE_ONE_ZERO, &none, QUEUE_2_COMMAND)); + assert!(provider_on(CORE_ONE_ZERO, &surface, SURFACE_CAPS_COMMAND)); + assert!(!provider_on(CORE_ONE_ZERO, &none, SURFACE_CAPS_COMMAND)); +} + +#[test] +fn negotiation_fails_where_the_loader_offers_less_than_the_layer_needs() { + assert_eq!(negotiated_version(LOADER_OFFERS_LOW, LAYER_WANTS), None); +} + +#[test] +fn negotiation_takes_the_lower_of_the_two_interface_versions() { + assert_eq!(negotiated_version(LOADER_OFFERS_HIGH, LAYER_WANTS), Some(LAYER_WANTS)); + assert_eq!(negotiated_version(LAYER_WANTS, LAYER_WANTS), Some(LAYER_WANTS)); +} + +#[test] +fn a_number_that_is_not_finite_is_not_a_value() { + assert_eq!(parse_float(NAN_TEXT), None); + assert_eq!(parse_float(INF_TEXT), None); + assert_eq!(parse_float(NEG_INF_TEXT), None); + assert_eq!(parse_float(FINITE_TEXT), Some(FINITE_VALUE)); +} + +#[test] +fn keeps_the_application_value_when_nothing_is_forced() { + assert_eq!(forced(None, LIST_THIRD), LIST_THIRD); + assert_eq!(forced(Some(LIST_SECOND), LIST_THIRD), LIST_SECOND); +} + +#[test] +fn restores_a_list_the_choice_emptied() { + assert_eq!( + kept(vec![LIST_FIRST, LIST_SECOND, LIST_THIRD], |value: &i32| *value > LIST_THIRD).items, + vec![LIST_FIRST, LIST_SECOND, LIST_THIRD] + ); + assert_eq!( + kept(vec![LIST_FIRST, LIST_SECOND, LIST_THIRD], |value: &i32| *value > LIST_FIRST).items, + vec![LIST_SECOND, LIST_THIRD] + ); +} + +#[test] +fn keeps_only_what_the_choice_names() { + assert_eq!( + filtered(vec![LIST_FIRST, LIST_SECOND, LIST_THIRD], Some(LIST_SECOND), |v: &i32| Some(*v)).items, + vec![LIST_SECOND] + ); + assert_eq!( + filtered(vec![LIST_FIRST, LIST_SECOND, LIST_THIRD], None, |v: &i32| Some(*v)).items, + vec![LIST_FIRST, LIST_SECOND, LIST_THIRD] + ); + assert_eq!( + filtered(vec![LIST_FIRST, LIST_SECOND, LIST_THIRD], Some(LIST_MISSING), |v: &i32| Some(*v)).items, + vec![LIST_FIRST, LIST_SECOND, LIST_THIRD] + ); +} + +#[test] +fn round_trips_a_floor_present_mode_through_its_name() { + let mode = vk::PresentModeKHR::from_raw(FIFO_MODE); + assert_eq!(present_parse(&present_display(mode)), Some(mode)); +} + +#[test] +fn never_reads_a_present_mode_above_the_floor_back_from_a_profile() { + let mode = vk::PresentModeKHR::from_raw(UNKNOWN_MODE); + let shared = vk::PresentModeKHR::from_raw(SHARED_MODE); + assert_eq!(present_parse(&present_display(mode)), None); + assert_eq!(present_parse(&present_display(shared)), None); +} + +#[test] +fn round_trips_a_composite_alpha_through_its_name() { + let alpha = vk::CompositeAlphaFlagsKHR::from_raw(OPAQUE_ALPHA); + assert_eq!(alpha_parse(&alpha_display(alpha)), Some(alpha)); +} + +#[test] +fn never_reads_a_composite_alpha_volt_has_no_name_for() { + let alpha = vk::CompositeAlphaFlagsKHR::from_raw(UNKNOWN_ALPHA); + assert_eq!(alpha_parse(&alpha_display(alpha)), None); +} + +#[test] +fn gives_each_enum_its_own_unknown_prefix() { + assert_ne!( + present_display(vk::PresentModeKHR::from_raw(UNKNOWN_MODE)), + alpha_display(vk::CompositeAlphaFlagsKHR::from_raw(UNKNOWN_MODE as u32)) + ); +} + +#[test] +fn groups_an_unnamed_value_under_nothing() { + assert_eq!(present_semantic(vk::PresentModeKHR::from_raw(UNKNOWN_MODE)), None); + assert_eq!(alpha_semantic(vk::CompositeAlphaFlagsKHR::from_raw(UNKNOWN_ALPHA)), None); +} + +#[test] +fn reads_the_facts_a_setting_branches_on() { + let opaque = vk::CompositeAlphaFlagsKHR::from_raw(OPAQUE_ALPHA); + let inherit = vk::CompositeAlphaFlagsKHR::from_raw(INHERIT_ALPHA); + let fifo = vk::PresentModeKHR::from_raw(FIFO_MODE); + let shared = vk::PresentModeKHR::from_raw(SHARED_MODE); + assert_eq!(alpha_semantic(opaque), Some(AlphaFacts { blends: false })); + assert_eq!(alpha_semantic(inherit), Some(AlphaFacts { blends: true })); + assert_eq!( + present_semantic(fifo), + Some(PresentFacts { floor: true, shared: false }) + ); + assert_eq!( + present_semantic(shared), + Some(PresentFacts { floor: false, shared: true }) + ); +} + +#[test] +fn leaves_a_value_above_the_floor_at_default() { + assert_eq!(parse_settings(UNNAMED_MODE_PROFILE).settings.present_mode, None); + assert_eq!( + parse_settings(NAMED_MODE_PROFILE).settings.present_mode, + Some(vk::PresentModeKHR::from_raw(FIFO_MODE)) + ); +} + +#[test] +fn reads_the_two_feature_gated_toggles_from_a_profile() { + assert_eq!(parse_settings(ALPHA_ONE_PROFILE).settings.alpha_to_one, Some(TOGGLE_ON)); + assert_eq!(parse_settings(CLAMP_PROFILE).settings.depth_clamp, Some(TOGGLE_OFF)); +} + +#[test] +fn reads_each_sampler_filter_on_its_own() { + assert_eq!( + parse_settings(FILTER_PROFILE).settings.mag_filter, + Some(vk::Filter::from_raw(NEAREST_FILTER)) + ); + assert_eq!( + parse_settings(FILTER_PROFILE).settings.min_filter, + Some(vk::Filter::from_raw(LINEAR_FILTER)) + ); +} + +#[test] +fn turns_a_frame_rate_into_an_interval() { + assert_eq!(target_interval_ns(TWO_HUNDRED_FPS), TWO_HUNDRED_FPS_NS); +} + +#[test] +fn starts_the_timeline_one_interval_after_the_first_frame() { + assert_eq!( + advanced( + None, + LIMIT_START_NS, + LIMIT_INTERVAL_NS, + Some(MethodChoice::Early), + None, + ), + Timeline { + target: LIMIT_START_NS + LIMIT_INTERVAL_NS, + interval: LIMIT_INTERVAL_NS, + last: LIMIT_START_NS + LIMIT_INTERVAL_NS, + peak: LIMIT_INTERVAL_NS, + } + ); +} + +#[test] +fn holds_the_cadence_when_a_frame_runs_a_little_late() { + assert_eq!( + advanced( + Some(Timeline { + target: LIMIT_START_NS, + interval: LIMIT_INTERVAL_NS, + last: LIMIT_START_NS, + peak: LIMIT_INTERVAL_NS, + }), + LIMIT_START_NS + LATE_NS, + LIMIT_INTERVAL_NS, + Some(MethodChoice::Early), + None, + ) + .target, + LIMIT_START_NS + LIMIT_INTERVAL_NS + ); +} + +#[test] +fn never_chases_a_slow_frame_with_a_fast_one() { + assert_eq!( + advanced( + Some(Timeline { + target: LIMIT_START_NS, + interval: LIMIT_INTERVAL_NS, + last: LIMIT_START_NS, + peak: LIMIT_INTERVAL_NS, + }), + LIMIT_START_NS + LIMIT_INTERVAL_NS + LIMIT_INTERVAL_NS, + LIMIT_INTERVAL_NS, + Some(MethodChoice::Early), + None, + ) + .target, + LIMIT_START_NS + LIMIT_INTERVAL_NS + LIMIT_INTERVAL_NS + ); +} + +#[test] +fn a_frame_past_its_deadline_leaves_without_waiting() { + assert_eq!( + advanced( + Some(Timeline { + target: LIMIT_START_NS, + interval: LIMIT_INTERVAL_NS, + last: LIMIT_START_NS, + peak: LIMIT_INTERVAL_NS, + }), + LIMIT_START_NS + LATE_FRAME_NS, + LIMIT_INTERVAL_NS, + Some(MethodChoice::Early), + None, + ) + .target, + LIMIT_START_NS + LATE_FRAME_NS + ); +} + +#[test] +fn measures_a_reactive_interval_from_the_frame_just_shown() { + assert_eq!( + advanced( + Some(Timeline { + target: LIMIT_START_NS, + interval: LIMIT_INTERVAL_NS, + last: LIMIT_START_NS, + peak: LIMIT_INTERVAL_NS, + }), + LIMIT_START_NS + LATE_NS, + LIMIT_INTERVAL_NS, + Some(MethodChoice::Reactive), + None, + ) + .target, + LIMIT_START_NS + LATE_NS + LIMIT_INTERVAL_NS + ); +} + +#[test] +fn restarts_the_timeline_when_the_frame_limit_changes() { + assert_eq!( + advanced( + Some(Timeline { + target: LIMIT_START_NS, + interval: LIMIT_INTERVAL_NS, + last: LIMIT_START_NS, + peak: LIMIT_INTERVAL_NS, + }), + LIMIT_START_NS + LATE_NS, + OTHER_INTERVAL_NS, + Some(MethodChoice::Late), + None, + ), + Timeline { + target: LIMIT_START_NS + LATE_NS + OTHER_INTERVAL_NS, + interval: OTHER_INTERVAL_NS, + last: LIMIT_START_NS + LATE_NS + OTHER_INTERVAL_NS, + peak: OTHER_INTERVAL_NS, + } + ); +} + +#[test] +fn reads_a_frame_limit_cadence_from_a_profile() { + assert_eq!(parse_settings(FIXED_PROFILE).settings.cadence, Some(CadenceChoice::Fixed)); + assert_eq!(parse_settings(DYNAMIC_PROFILE).settings.cadence, Some(CadenceChoice::Dynamic)); +} + +#[test] +fn times_production_from_the_release_of_the_last_frame() { + assert_eq!( + advanced( + None, + LIMIT_START_NS, + LIMIT_INTERVAL_NS, + Some(MethodChoice::Early), + Some(CadenceChoice::Smooth), + ) + .last, + LIMIT_START_NS + LIMIT_INTERVAL_NS + ); +} + +#[test] +fn a_fixed_cadence_paces_at_the_cap() { + assert_eq!( + advanced( + Some(Timeline { + target: LIMIT_START_NS, + interval: LIMIT_INTERVAL_NS, + last: LIMIT_START_NS, + peak: LIMIT_INTERVAL_NS, + }), + LIMIT_START_NS + FAST_FRAME_NS, + LIMIT_INTERVAL_NS, + Some(MethodChoice::Early), + Some(CadenceChoice::Fixed), + ) + .target, + LIMIT_START_NS + LIMIT_INTERVAL_NS + ); +} + +#[test] +fn smooth_paces_at_the_worst_recent_frame() { + assert_eq!( + advanced( + Some(Timeline { + target: LIMIT_START_NS, + interval: LIMIT_INTERVAL_NS, + last: LIMIT_START_NS, + peak: PEAK_START_NS, + }), + LIMIT_START_NS + SLOW_FRAME_NS, + LIMIT_INTERVAL_NS, + Some(MethodChoice::Early), + Some(CadenceChoice::Smooth), + ) + .target, + SMOOTH_TARGET_NS + ); +} + +#[test] +fn dynamic_rounds_the_pace_to_a_quarter_step() { + assert_eq!( + advanced( + Some(Timeline { + target: LIMIT_START_NS, + interval: LIMIT_INTERVAL_NS, + last: LIMIT_START_NS, + peak: PEAK_START_NS, + }), + LIMIT_START_NS + SLOW_FRAME_NS, + LIMIT_INTERVAL_NS, + Some(MethodChoice::Early), + Some(CadenceChoice::Dynamic), + ) + .target, + DYNAMIC_TARGET_NS + ); +} + +#[test] +fn neither_cadence_paces_faster_than_the_cap() { + assert_eq!( + advanced( + Some(Timeline { + target: LIMIT_START_NS, + interval: LIMIT_INTERVAL_NS, + last: LIMIT_START_NS, + peak: FAST_FRAME_NS, + }), + LIMIT_START_NS + FAST_FRAME_NS, + LIMIT_INTERVAL_NS, + Some(MethodChoice::Early), + Some(CadenceChoice::Smooth), + ) + .target, + LIMIT_START_NS + LIMIT_INTERVAL_NS + ); + assert_eq!( + advanced( + Some(Timeline { + target: LIMIT_START_NS, + interval: LIMIT_INTERVAL_NS, + last: LIMIT_START_NS, + peak: FAST_FRAME_NS, + }), + LIMIT_START_NS + FAST_FRAME_NS, + LIMIT_INTERVAL_NS, + Some(MethodChoice::Early), + Some(CadenceChoice::Dynamic), + ) + .target, + LIMIT_START_NS + LIMIT_INTERVAL_NS + ); +} + +#[test] +fn the_peak_decays_so_the_pace_climbs_back() { + assert_eq!( + advanced( + Some(Timeline { + target: LIMIT_START_NS, + interval: LIMIT_INTERVAL_NS, + last: LIMIT_START_NS, + peak: DECAY_FROM_NS, + }), + LIMIT_START_NS + LIMIT_INTERVAL_NS, + LIMIT_INTERVAL_NS, + Some(MethodChoice::Early), + Some(CadenceChoice::Smooth), + ) + .peak, + DECAY_TO_NS + ); +} + +#[test] +fn a_hitch_cannot_set_the_peak_past_the_spike_limit() { + assert_eq!( + advanced( + Some(Timeline { + target: LIMIT_START_NS, + interval: LIMIT_INTERVAL_NS, + last: LIMIT_START_NS, + peak: LIMIT_INTERVAL_NS, + }), + LIMIT_START_NS + HITCH_NS, + LIMIT_INTERVAL_NS, + Some(MethodChoice::Early), + Some(CadenceChoice::Smooth), + ) + .peak, + SPIKE_PEAK_NS + ); +} + +#[test] +fn writes_a_setting_line_in_either_shape() { + assert_eq!( + report_line( + SETTING_PRESENT_MODE, + Some(ASKED_MODE.into()), + Some(ASKED_MODE.into()), + None, + ), + SAME_LINE + ); + assert_eq!( + report_line( + SETTING_PRESENT_MODE, + Some(ASKED_MODE.into()), + Some(FORCED_MODE.into()), + None, + ), + BOTH_LINE + ); + assert_eq!( + report_line(SETTING_FRAME_LIMIT, None, Some(FORCED_LIMIT.into()), None), + APPLIED_LINE + ); + assert_eq!( + report_line(SETTING_FRAME_LIMIT, None, None, Some(NOTE_NOT_SET.into())), + UNSET_LINE + ); + assert_eq!( + report_line( + SETTING_ANISOTROPY, + Some(ASKED_ANISO.into()), + Some(ASKED_ANISO.into()), + feature_note(true, false, FEATURE_ANISOTROPY), + ), + BLOCKED_LINE + ); +} + +#[test] +fn names_the_applied_value_wherever_the_profile_set_one() { + let nearest = vk::Filter::from_raw(NEAREST_FILTER); + let linear = vk::Filter::from_raw(LINEAR_FILTER); + assert_eq!(applied_text(nearest, filter_text), TEXT_NEAREST.to_string()); + assert_eq!(applied_text(linear, filter_text), TEXT_LINEAR.to_string()); +} + +#[test] +fn notes_a_feature_only_where_the_profile_set_the_setting() { + assert!(feature_note(true, false, FEATURE_ANISOTROPY).is_some()); + assert!(feature_note(false, false, FEATURE_ANISOTROPY).is_none()); + assert!(feature_note(true, true, FEATURE_ANISOTROPY).is_none()); +} + +#[test] +fn writes_a_number_the_way_a_profile_writes_it() { + assert_eq!(number_text(ANISO_SIXTEEN), ANISO_SIXTEEN_TEXT); + assert_eq!(number_text(BIAS_DOWN), BIAS_DOWN_TEXT); +} + +fn chain_node(s_type: u32, next: *mut c_void) -> VkChainNode { + VkChainNode { + s_type: vk::StructureType::from_raw(s_type as i32), + p_next: next, + } +} + +fn mapping_with( + source: i32, + sampler: *const vk::SamplerCreateInfo<'static>, +) -> VkDescriptorSetAndBindingMappingEXT { + let mut mapping: VkDescriptorSetAndBindingMappingEXT = unsafe { mem::zeroed() }; + mapping.source = source; + mapping.source_data.push_index.p_embedded_sampler = sampler; + mapping +} + +#[test] +fn copies_every_chain_node_it_declares() { + let declared = VkSwapchainCounterCreateInfoEXT { + s_type: vk::StructureType::from_raw(SWAPCHAIN_COUNTER_TYPE as i32), + p_next: ptr::null(), + surface_counters: NO_COUNTERS, + }; + let undeclared = chain_node(UNKNOWN_CHAIN_TYPE, ptr::null_mut()); + assert!(copied_node( + &declared as *const VkSwapchainCounterCreateInfoEXT as *const VkChainNode + ) + .is_some()); + assert!(copied_node(&undeclared as *const VkChainNode).is_none()); +} + +#[test] +fn chains_its_own_copy_in_front_of_the_replacement() { + let mut target = chain_node(SWAPCHAIN_MODE_LIST_TYPE, ptr::null_mut()); + let front = VkSwapchainCounterCreateInfoEXT { + s_type: vk::StructureType::from_raw(SWAPCHAIN_COUNTER_TYPE as i32), + p_next: &mut target as *mut VkChainNode as *const c_void, + surface_counters: NO_COUNTERS, + }; + let replacement = chain_node(SWAPCHAIN_MODE_LIST_TYPE, ptr::null_mut()); + let built = relinked_chain( + &front as *const VkSwapchainCounterCreateInfoEXT as *const c_void, + SWAPCHAIN_MODE_LIST_TYPE, + &replacement as *const VkChainNode as *const c_void, + ) + .unwrap(); + let copy = built.head as *const VkChainNode; + assert_ne!( + built.head, + &front as *const VkSwapchainCounterCreateInfoEXT as *const c_void + ); + assert_eq!( + unsafe { (*copy).s_type.as_raw() as u32 }, + SWAPCHAIN_COUNTER_TYPE + ); + assert_eq!( + unsafe { (*copy).p_next as *const c_void }, + &replacement as *const VkChainNode as *const c_void + ); +} + +#[test] +fn passes_the_chain_down_when_a_node_in_front_is_undeclared() { + let mut target = chain_node(SWAPCHAIN_MODE_LIST_TYPE, ptr::null_mut()); + let front = chain_node( + UNKNOWN_CHAIN_TYPE, + &mut target as *mut VkChainNode as *mut c_void, + ); + let replacement = chain_node(SWAPCHAIN_MODE_LIST_TYPE, ptr::null_mut()); + assert!(relinked_chain( + &front as *const VkChainNode as *const c_void, + SWAPCHAIN_MODE_LIST_TYPE, + &replacement as *const VkChainNode as *const c_void, + ) + .is_none()); +} + +#[test] +fn reads_the_sampler_only_where_the_selector_names_one() { + let sampler = vk::SamplerCreateInfo::default(); + let held = + &sampler as *const vk::SamplerCreateInfo<'_> as *const vk::SamplerCreateInfo<'static>; + assert!(embedded_sampler(&mapping_with(SOURCE_PUSH_INDEX, held)).is_some()); + assert!(embedded_sampler(&mapping_with(SOURCE_PUSH_DATA, held)).is_none()); + assert!(embedded_sampler(&mapping_with(SOURCE_PUSH_INDEX, ptr::null())).is_none()); + assert!(embedded_sampler(&mapping_with(SOURCE_SHADER_RECORD_INDEX, ptr::null())).is_none()); +} + +#[test] +fn narrows_a_chained_mode_list_to_the_choice() { + let mailbox = vk::PresentModeKHR::from_raw(MAILBOX_VALUE); + assert_eq!( + present_narrowed(vec![vk::PresentModeKHR::FIFO, mailbox], Some(mailbox)).items, + vec![mailbox] + ); + assert_eq!( + present_narrowed(vec![vk::PresentModeKHR::FIFO], Some(mailbox)).items, + vec![vk::PresentModeKHR::FIFO] + ); +} + +#[test] +fn leaves_a_mode_above_the_floor_where_it_was() { + let mailbox = vk::PresentModeKHR::from_raw(MAILBOX_VALUE); + let shared = vk::PresentModeKHR::from_raw(SHARED_MODE); + assert_eq!( + present_narrowed(vec![vk::PresentModeKHR::FIFO, mailbox, shared], Some(mailbox)).items, + vec![mailbox, shared] + ); +} + +#[test] +fn restores_a_list_where_no_floor_mode_survives_beside_one_above_it() { + let mailbox = vk::PresentModeKHR::from_raw(MAILBOX_VALUE); + let shared = vk::PresentModeKHR::from_raw(SHARED_MODE); + let modes = vec![vk::PresentModeKHR::FIFO, shared]; + assert_eq!(present_narrowed(modes.clone(), Some(mailbox)).items, modes); +} + +#[test] +fn heads_a_surface_section_with_the_backend_that_read_it() { + assert_eq!( + render_surface( + SURFACE_TAG, + &SurfaceFacts { + present: SURFACE_PRESENT.iter().map(|name| (*name).into()).collect(), + alphas: SURFACE_ALPHA.iter().map(|name| (*name).into()).collect(), + min_images: SURFACE_MIN_IMAGES, + max_images: SURFACE_MAX_IMAGES, + }, + ), + SURFACE_SECTION + ); +} diff --git a/src/volt/config.rs b/src/volt/config.rs new file mode 100644 index 0000000..76eb5d7 --- /dev/null +++ b/src/volt/config.rs @@ -0,0 +1,391 @@ +use std::fs; +use std::path::PathBuf; +use std::sync::OnceLock; + +use ash::vk; + +use crate::consts::ANISO_OFF; +use crate::consts::CADENCE_DYNAMIC; +use crate::consts::CADENCE_FIXED; +use crate::consts::CADENCE_SMOOTH; +use crate::consts::CadenceChoice; +use crate::consts::DEFAULT_PROFILE; +use crate::consts::FILTER_LINEAR; +use crate::consts::FILTER_NEAREST; +use crate::consts::FIRST_DEVICE; +use crate::consts::FRAME_LIMIT_MIN; +use crate::consts::HOME_FALLBACK; +use crate::consts::KEY_ALPHA_TO_COVERAGE; +use crate::consts::KEY_ALPHA_TO_ONE; +use crate::consts::KEY_ANISOTROPY; +use crate::consts::KEY_CLIPPED; +use crate::consts::KEY_COMPOSITE_ALPHA; +use crate::consts::KEY_DEPTH_CLAMP; +use crate::consts::KEY_DOT; +use crate::consts::KEY_DEVICE; +use crate::consts::KEY_FRAME_LIMIT; +use crate::consts::KEY_FRAME_LIMIT_CADENCE; +use crate::consts::KEY_FRAME_LIMIT_METHOD; +use crate::consts::KEY_FRAME_PACING; +use crate::consts::KEY_IMAGE_COUNT; +use crate::consts::KEY_LOD_BIAS; +use crate::consts::KEY_MAG_FILTER; +use crate::consts::KEY_MIN_FILTER; +use crate::consts::KEY_MIPMAP_MODE; +use crate::consts::KEY_MIP_CEILING; +use crate::consts::KEY_MIP_FLOOR; +use crate::consts::KEY_PRESENT_MODE; +use crate::consts::KEY_SAMPLE_SHADING; +use crate::consts::LOG_INVALID_PROFILE; +use crate::consts::NAME_DEFAULT; +use crate::consts::NAME_EMPTY; +use crate::consts::HOME_UNSET_WARN; +use crate::consts::METHOD_EARLY; +use crate::consts::METHOD_LATE; +use crate::consts::METHOD_REACTIVE; +use crate::consts::MethodChoice; +use crate::consts::MIPMAP_LINEAR; +use crate::consts::MIPMAP_NEAREST; +use crate::consts::PACING_PRECISE; +use crate::consts::PARSE_FAILED_CLOSE; +use crate::consts::PARSE_FAILED_OPEN; +use crate::consts::REPORT_MARK; +use crate::consts::PACING_SLEEP; +use crate::consts::PACING_SLICED; +use crate::consts::PACING_SPIN; +use crate::consts::PacingChoice; +use crate::consts::RESERVED_PROFILES; +use crate::consts::SECTION_DISPLAY; +use crate::consts::SECTION_FRAMERATE; +use crate::consts::SECTION_GPU; +use crate::consts::SECTION_RENDERING; +use crate::consts::SECTION_TEXTURES; +use crate::consts::SETTINGS_FROZEN_INFO; +use crate::consts::SHADING_MAX; +use crate::consts::SHADING_OFF; +use crate::consts::TEXT_LINEAR; +use crate::consts::TEXT_NEAREST; +use crate::consts::TEXT_OFF; +use crate::consts::TEXT_ON; +use crate::consts::TOGGLE_OFF; +use crate::consts::TOGGLE_ON; +use crate::consts::UNREAD_CLOSE; +use crate::consts::UNREAD_OPEN; +use crate::consts::UNREAD_PROFILE_CLOSE; +use crate::env::env_config_name; +use crate::env::env_home; +use crate::logging::call_init_log_level; +use crate::logging::call_log_at; +use crate::logging::LogLevel; +use crate::ranks::alpha_parse; +use crate::ranks::present_parse; + +#[derive(Default)] +pub(crate) struct Settings { + pub(crate) gpu: Option, + pub(crate) present_mode: Option, + pub(crate) image_count: Option, + pub(crate) composite_alpha: Option, + pub(crate) clipped: Option, + pub(crate) mag_filter: Option, + pub(crate) min_filter: Option, + pub(crate) mipmap: Option, + pub(crate) anisotropy: Option, + pub(crate) lod_bias: Option, + pub(crate) mip_floor: Option, + pub(crate) mip_ceiling: Option, + pub(crate) sample_shading: Option, + pub(crate) alpha_coverage: Option, + pub(crate) alpha_to_one: Option, + pub(crate) depth_clamp: Option, + pub(crate) frame_limit: Option, + pub(crate) cadence: Option, + pub(crate) limit_method: Option, + pub(crate) pacing: Option, +} + +static SETTINGS: OnceLock = OnceLock::new(); + +fn table_value<'a>(doc: &'a toml::Table, section: &str, key: &str) -> Option<&'a str> { + doc.get(section) + .and_then(|v| v.as_table()) + .and_then(|t| t.get(key)) + .and_then(|v| v.as_str()) +} + +fn non_default(text: &str) -> Option<&str> { + match text { + NAME_DEFAULT | NAME_EMPTY => None, + other => Some(other), + } +} + +pub(crate) fn parse_float(text: &str) -> Option { + text.parse::().ok().filter(|value| value.is_finite()) +} + +fn parse_uint(text: &str) -> Option { + text.parse::().ok() +} + +fn parse_filter(text: &str) -> Option { + match text { + TEXT_NEAREST => Some(vk::Filter::from_raw(FILTER_NEAREST)), + TEXT_LINEAR => Some(vk::Filter::from_raw(FILTER_LINEAR)), + _ => None, + } +} + +fn parse_mipmap(text: &str) -> Option { + match text { + TEXT_NEAREST => Some(vk::SamplerMipmapMode::from_raw(MIPMAP_NEAREST)), + TEXT_LINEAR => Some(vk::SamplerMipmapMode::from_raw(MIPMAP_LINEAR)), + _ => None, + } +} + +fn parse_toggle(text: &str) -> Option { + match text { + TEXT_OFF => Some(TOGGLE_OFF), + TEXT_ON => Some(TOGGLE_ON), + _ => None, + } +} + +fn parse_off_only(text: &str) -> Option { + match text { + TEXT_OFF => Some(TOGGLE_OFF), + _ => None, + } +} + +fn parse_aniso(text: &str) -> Option { + match text { + TEXT_OFF => Some(ANISO_OFF), + other => parse_float(other).filter(|v| *v >= ANISO_OFF), + } +} + +fn parse_shading(text: &str) -> Option { + match text { + TEXT_OFF => Some(SHADING_OFF), + other => parse_float(other).filter(|v| (SHADING_OFF..=SHADING_MAX).contains(v)), + } +} + +fn parse_limit(text: &str) -> Option { + parse_float(text).filter(|v| *v >= FRAME_LIMIT_MIN) +} + +fn parse_gpu(text: &str) -> Option { + parse_uint(text).filter(|v| *v >= FIRST_DEVICE) +} + +fn parse_cadence(text: &str) -> Option { + match text { + CADENCE_FIXED => Some(CadenceChoice::Fixed), + CADENCE_SMOOTH => Some(CadenceChoice::Smooth), + CADENCE_DYNAMIC => Some(CadenceChoice::Dynamic), + _ => None, + } +} + +fn parse_method(text: &str) -> Option { + match text { + METHOD_EARLY => Some(MethodChoice::Early), + METHOD_LATE => Some(MethodChoice::Late), + METHOD_REACTIVE => Some(MethodChoice::Reactive), + _ => None, + } +} + +fn parse_pacing(text: &str) -> Option { + match text { + PACING_SLEEP => Some(PacingChoice::Sleep), + PACING_SLICED => Some(PacingChoice::Sliced), + PACING_PRECISE => Some(PacingChoice::Precise), + PACING_SPIN => Some(PacingChoice::Spin), + _ => None, + } +} + +pub(crate) struct Parsed { + pub(crate) settings: Settings, + pub(crate) warnings: Vec, +} + +fn checked( + warnings: &mut Vec, + section: &str, + key: &str, + text: &str, + value: Option, +) -> Option { + match value { + Some(v) => Some(v), + None => { + warnings.push([section, KEY_DOT, key, UNREAD_OPEN, text, UNREAD_CLOSE].concat()); + None + } + } +} + +fn field( + doc: &toml::Table, + warnings: &mut Vec, + section: &str, + key: &str, + parse: F, +) -> Option +where + F: Fn(&str) -> Option, +{ + match table_value(doc, section, key).and_then(non_default) { + None => None, + Some(text) => checked(warnings, section, key, text, parse(text)), + } +} + +fn parse_doc(text: &str, warnings: &mut Vec) -> toml::Table { + match text.parse::() { + Ok(d) => d, + Err(e) => { + warnings.push([PARSE_FAILED_OPEN, e.to_string().as_str(), PARSE_FAILED_CLOSE].concat()); + toml::Table::new() + } + } +} + +pub(crate) fn parse_settings(text: &str) -> Parsed { + let mut warnings = Vec::new(); + let doc = parse_doc(text, &mut warnings); + let settings = Settings { + gpu: field(&doc, &mut warnings, SECTION_GPU, KEY_DEVICE, parse_gpu), + present_mode: field(&doc, &mut warnings, SECTION_DISPLAY, KEY_PRESENT_MODE, present_parse), + image_count: field(&doc, &mut warnings, SECTION_DISPLAY, KEY_IMAGE_COUNT, parse_uint), + composite_alpha: field(&doc, &mut warnings, SECTION_DISPLAY, KEY_COMPOSITE_ALPHA, alpha_parse), + clipped: field(&doc, &mut warnings, SECTION_DISPLAY, KEY_CLIPPED, parse_toggle), + mag_filter: field(&doc, &mut warnings, SECTION_TEXTURES, KEY_MAG_FILTER, parse_filter), + min_filter: field(&doc, &mut warnings, SECTION_TEXTURES, KEY_MIN_FILTER, parse_filter), + mipmap: field(&doc, &mut warnings, SECTION_TEXTURES, KEY_MIPMAP_MODE, parse_mipmap), + anisotropy: field(&doc, &mut warnings, SECTION_TEXTURES, KEY_ANISOTROPY, parse_aniso), + lod_bias: field(&doc, &mut warnings, SECTION_TEXTURES, KEY_LOD_BIAS, parse_float), + mip_floor: field(&doc, &mut warnings, SECTION_TEXTURES, KEY_MIP_FLOOR, parse_float), + mip_ceiling: field(&doc, &mut warnings, SECTION_TEXTURES, KEY_MIP_CEILING, parse_float), + sample_shading: field(&doc, &mut warnings, SECTION_RENDERING, KEY_SAMPLE_SHADING, parse_shading), + alpha_coverage: field(&doc, &mut warnings, SECTION_RENDERING, KEY_ALPHA_TO_COVERAGE, parse_off_only), + alpha_to_one: field(&doc, &mut warnings, SECTION_RENDERING, KEY_ALPHA_TO_ONE, parse_toggle), + depth_clamp: field(&doc, &mut warnings, SECTION_RENDERING, KEY_DEPTH_CLAMP, parse_toggle), + frame_limit: field(&doc, &mut warnings, SECTION_FRAMERATE, KEY_FRAME_LIMIT, parse_limit), + cadence: field(&doc, &mut warnings, SECTION_FRAMERATE, KEY_FRAME_LIMIT_CADENCE, parse_cadence), + limit_method: field(&doc, &mut warnings, SECTION_FRAMERATE, KEY_FRAME_LIMIT_METHOD, parse_method), + pacing: field(&doc, &mut warnings, SECTION_FRAMERATE, KEY_FRAME_PACING, parse_pacing), + }; + Parsed { settings, warnings } +} + +fn reserved_name(raw: &str) -> bool { + RESERVED_PROFILES + .iter() + .any(|name| raw.eq_ignore_ascii_case(name)) +} + +fn name_is_valid(raw: &str) -> bool { + !raw.is_empty() + && !raw.contains('/') + && !raw.contains('\\') + && !raw.contains("..") + && !raw.contains('\0') + && !reserved_name(raw) + && raw.chars().all(|ch| ch.is_ascii_graphic()) +} + +fn folded_name(raw: &str) -> String { + match raw.eq_ignore_ascii_case(DEFAULT_PROFILE) { + true => DEFAULT_PROFILE.into(), + false => raw.into(), + } +} + +pub(crate) fn sanitized_name(raw: &str) -> Option { + match name_is_valid(raw) { + true => Some(folded_name(raw)), + false => None, + } +} + +pub(crate) fn call_sanitize_name(raw: &str) -> String { + match sanitized_name(raw) { + Some(name) => name, + None => { + call_log_at(LogLevel::Warn, LOG_INVALID_PROFILE); + DEFAULT_PROFILE.into() + } + } +} + +fn call_fallback_home() -> String { + call_log_at(LogLevel::Warn, HOME_UNSET_WARN); + HOME_FALLBACK.into() +} + +fn home_text() -> String { + match env_home() { + Some(path) => path, + None => call_fallback_home(), + } +} + +pub(crate) fn home_dir() -> PathBuf { + PathBuf::from(home_text()) +} + +pub(crate) fn config_dir() -> PathBuf { + home_dir().join(".config").join("volt-gui") +} + +pub(crate) fn profile_name() -> String { + call_sanitize_name(&env_config_name()) +} + +pub(crate) fn config_path(name: &str) -> PathBuf { + config_dir().join(format!("{}.toml", name)) +} + +fn call_logged_settings(parsed: Parsed) -> Settings { + parsed + .warnings + .iter() + .for_each(|warning| call_log_at(LogLevel::Warn, warning)); + parsed.settings +} + +fn call_read_config(path: &PathBuf) -> Settings { + match fs::read_to_string(path) { + Ok(text) => call_logged_settings(parse_settings(&text)), + Err(e) => { + call_log_at( + LogLevel::Warn, + &[ + path.display().to_string().as_str(), + REPORT_MARK, + e.to_string().as_str(), + UNREAD_PROFILE_CLOSE, + ] + .concat(), + ); + Settings::default() + } + } +} + +fn call_load_settings() -> Settings { + call_init_log_level(); + let loaded = call_read_config(&config_path(&profile_name())); + call_log_at(LogLevel::Info, SETTINGS_FROZEN_INFO); + loaded +} + +pub(crate) fn ensure_settings() -> &'static Settings { + SETTINGS.get_or_init(call_load_settings) +} diff --git a/src/volt/consts.rs b/src/volt/consts.rs new file mode 100644 index 0000000..99ceac5 --- /dev/null +++ b/src/volt/consts.rs @@ -0,0 +1,507 @@ +use ash::vk; + +pub(crate) const ENABLE_VALUE: &str = "1"; +pub(crate) const DEFAULT_PROFILE: &str = "default"; +pub(crate) const RESERVED_PROFILES: [&str; 2] = ["probe", "options"]; +pub(crate) const EXIT_EXEC_FAILED: i32 = 127; +pub(crate) const EXIT_USAGE: i32 = 1; +pub(crate) const EXIT_OK: i32 = 0; + +pub(crate) const ENV_CONFIG_NAME: &str = "VOLT_CONFIG_NAME"; +pub(crate) const ENV_LOG: &str = "VOLT_LOG"; +pub(crate) const ENV_ENABLE: &str = "VOLT_ENABLE"; +pub(crate) const ENV_PROBE: &str = "VOLT_PROBE"; +pub(crate) const ENV_HOME: &str = "HOME"; +pub(crate) const ENV_LIB_PATH: &str = "LD_LIBRARY_PATH"; + +pub(crate) const HOME_FALLBACK: &str = "/tmp"; +pub(crate) const HOME_UNSET_WARN: &str = "HOME is unset, reading profiles from /tmp instead"; +pub(crate) const USER_LIB_REL: &str = ".local/lib/volt"; +pub(crate) const LIB_DIR_64: &str = "x86_64-linux-gnu"; +pub(crate) const LIB_DIR_32: &str = "i386-linux-gnu"; +pub(crate) const PATH_SEP: &str = ":"; + +pub(crate) const PROBE_FLAG: &str = "--probe"; +pub(crate) const PROBE_FILE: &str = "probe.toml"; +pub(crate) const PROBE_SECTION: &str = "[probe]"; +pub(crate) const PROBE_TEMP: &str = "probe.toml.new"; +pub(crate) const PROBE_SECTION_OPEN: &str = "["; +pub(crate) const PROBE_SECTION_CLOSE: &str = "]"; +pub(crate) const PROBE_SEP: &str = ";"; +pub(crate) const PROBE_ON: &str = "on"; +pub(crate) const PROBE_OFF: &str = "off"; +pub(crate) const PROBE_UNSET: &str = ""; +pub(crate) const PROBE_WRITE_INFO: &str = "probe written to the config directory"; +pub(crate) const PROBE_FAIL_WARN: &str = "probe write failed, the interface keeps the lists it last read"; +pub(crate) const PROBE_PAIR_OPEN: &str = " = \""; +pub(crate) const PROBE_PAIR_CLOSE: &str = "\"\n"; +pub(crate) const PROBE_KEY_DEVICE_INDEX: &str = "device_index"; +pub(crate) const PROBE_KEY_DEVICE_NAMES: &str = "device_names"; +pub(crate) const PROBE_KEY_MAX_ANISOTROPY: &str = "max_anisotropy"; +pub(crate) const PROBE_KEY_MAX_LOD_BIAS: &str = "max_lod_bias"; +pub(crate) const PROBE_KEY_MAX_LOD_LEVEL: &str = "max_lod_level"; +pub(crate) const PROBE_KEY_SAMPLER_ANISOTROPY: &str = "sampler_anisotropy"; +pub(crate) const PROBE_KEY_SAMPLE_RATE_SHADING: &str = "sample_rate_shading"; +pub(crate) const PROBE_KEY_ALPHA_TO_ONE: &str = "alpha_to_one"; +pub(crate) const PROBE_KEY_DEPTH_CLAMP: &str = "depth_clamp"; +pub(crate) const PROBE_KEY_PRESENT_MODES: &str = "present_modes"; +pub(crate) const PROBE_KEY_COMPOSITE_ALPHAS: &str = "composite_alphas"; +pub(crate) const PROBE_KEY_MIN_IMAGE_COUNT: &str = "min_image_count"; +pub(crate) const PROBE_KEY_MAX_IMAGE_COUNT: &str = "max_image_count"; + +pub(crate) const FLATPAK_CMD: &str = "flatpak"; +pub(crate) const FLATPAK_RUN: &str = "run"; +pub(crate) const FLATPAK_INJECT: &str = "/usr/lib/extensions/vulkan/volt/bin/volt-flatpak"; +pub(crate) const FLATPAK_CONFIG_RO: &str = "--filesystem=xdg-config/volt-gui:ro"; +pub(crate) const FLATPAK_CONFIG_RW: &str = "--filesystem=xdg-config/volt-gui"; + +pub(crate) const LOG_FD: i32 = 2; +pub(crate) const LOG_LEVEL_OFF: i32 = 0; +pub(crate) const LOG_LEVEL_ERROR: i32 = 1; +pub(crate) const LOG_LEVEL_WARN: i32 = 2; +pub(crate) const LOG_LEVEL_INFO: i32 = 3; +pub(crate) const DEFAULT_LOG_LEVEL: i32 = 2; + +pub(crate) const NS_PER_S: f64 = 1_000_000_000.0; +pub(crate) const SPIN_MARGIN_NS: u64 = 500_000; +pub(crate) const SLICE_MARGIN_NS: u64 = 150_000; +pub(crate) const SLICE_STEP_NS: u64 = 1_000_000; +pub(crate) const PACE_WINDOW: u64 = 4; +pub(crate) const PACE_SPIKE_LIMIT: u64 = 4; +pub(crate) const PACE_STEPS: u64 = 4; +pub(crate) const FRAME_LIMIT_MIN: f32 = 1.0; +pub(crate) const ANISO_OFF: f32 = 1.0; +pub(crate) const SHADING_OFF: f32 = 0.0; +pub(crate) const SHADING_MAX: f32 = 1.0; +pub(crate) const LAYER_IFACE_VERSION: u32 = 2; +pub(crate) const LAYER_LINK_INFO: i32 = 0; +pub(crate) const LAYER_DATA_CALLBACK: i32 = 1; +pub(crate) const LAYER_NEGOTIATE_INTERFACE_STRUCT: i32 = 1; +pub(crate) const LOADER_INSTANCE_CREATE_INFO_TYPE: u32 = 47; +pub(crate) const LOADER_DEVICE_CREATE_INFO_TYPE: u32 = 48; + +pub(crate) const DEVICE_GROUP_SIZE: usize = 32; +pub(crate) const DEVICE_GROUP_PROPERTIES_TYPE: u32 = 1000070000; +pub(crate) const DEVICE_GROUP_DEVICE_CREATE_INFO_TYPE: u32 = 1000070001; +pub(crate) const DEVICE_FEATURES_2_TYPE: u32 = 1000059000; +pub(crate) const MODE_COMPATIBILITY_TYPE: u32 = 1000274002; +pub(crate) const LATENCY_CAPABILITIES_TYPE: u32 = 1000505008; +pub(crate) const MODE_LIST_TYPES: [u32; 2] = [ + MODE_COMPATIBILITY_TYPE, + LATENCY_CAPABILITIES_TYPE, +]; + +pub(crate) const SHADER_MAPPING_INFO_TYPE: u32 = 1000135006; +pub(crate) const SAMPLER_YCBCR_CONVERSION_INFO_TYPE: u32 = 1000156001; +pub(crate) const SAMPLER_SUBSAMPLED_BIT: u32 = 1; +pub(crate) const SAMPLER_IMAGE_PROCESSING_BIT: u32 = 16; +pub(crate) const SAMPLER_SHAPE_REASON: &str = "the sampler's own shape restricts it, keeping the application value"; +pub(crate) const MIP_CROSS_REASON: &str = "the forced value would cross the other bound, keeping the application value"; +pub(crate) const LINEAR_REASON: &str = "linear is forced only where the sampler already uses linear, keeping the application value"; +pub(crate) const CUBIC_REASON: &str = "anisotropy is never enabled beside a cubic filter, keeping the application value"; +pub(crate) const PORTABILITY_REASON: &str = "the application enabled the portability subset, keeping the application value"; +pub(crate) const EXT_PORTABILITY_SUBSET: &str = "VK_KHR_portability_subset"; +pub(crate) const EXT_MIXED_SAMPLES: &str = "VK_NV_framebuffer_mixed_samples"; + +pub(crate) const LOG_DEVICE_REGISTERED: &str = "vk device registered"; +pub(crate) const LOG_INSTANCE_REGISTERED: &str = "vk instance registered"; +pub(crate) const LOG_SWAPCHAIN_CREATED: &str = "swapchain created"; +pub(crate) const LOG_QUEUE_UNREGISTERED: &str = "GetDeviceQueue on unregistered device"; +pub(crate) const LOG_QUEUE_2_UNREGISTERED: &str = "GetDeviceQueue2 on unregistered device"; +pub(crate) const LOG_NO_APP_ID: &str = "flatpak run: no app id found"; +pub(crate) const LOG_INVALID_PROFILE: &str = "invalid profile name, using default profile"; +pub(crate) const CHAIN_SAMPLER_LABEL: &str = "sampler"; +pub(crate) const KEY_DOT: &str = "."; +pub(crate) const UNREAD_OPEN: &str = " names \""; +pub(crate) const UNREAD_CLOSE: &str = "\", which is not a value this build can read: that setting was left alone"; +pub(crate) const PARSE_FAILED_OPEN: &str = "config parse failed: "; +pub(crate) const PARSE_FAILED_CLOSE: &str = ", using defaults"; +pub(crate) const UNREAD_PROFILE_CLOSE: &str = ", every setting left alone"; +pub(crate) const EXEC_FAILED: &str = "exec failed: "; +pub(crate) const LOG_PREFIX: &str = "[volt] "; +pub(crate) const LINE_END: &str = "\n"; +pub(crate) const NO_FLAGS: u32 = 0; +pub(crate) const NO_IMAGE_LIMIT: u32 = 0; +pub(crate) const UNSET_API_VERSION: u32 = 0; +pub(crate) const SINGLE_DEVICE: usize = 1; +pub(crate) const FIRST_DEVICE: u32 = 1; + +pub(crate) const KEY_DEVICE: &str = "device"; +pub(crate) const KEY_PRESENT_MODE: &str = "present_mode"; +pub(crate) const KEY_IMAGE_COUNT: &str = "image_count"; +pub(crate) const KEY_COMPOSITE_ALPHA: &str = "composite_alpha"; +pub(crate) const KEY_CLIPPED: &str = "clipped"; +pub(crate) const KEY_MAG_FILTER: &str = "mag_filter"; +pub(crate) const KEY_MIN_FILTER: &str = "min_filter"; +pub(crate) const KEY_MIPMAP_MODE: &str = "mipmap_mode"; +pub(crate) const KEY_ANISOTROPY: &str = "anisotropy"; +pub(crate) const KEY_LOD_BIAS: &str = "lod_bias"; +pub(crate) const KEY_MIP_FLOOR: &str = "mip_floor"; +pub(crate) const KEY_MIP_CEILING: &str = "mip_ceiling"; +pub(crate) const KEY_SAMPLE_SHADING: &str = "sample_shading"; +pub(crate) const KEY_ALPHA_TO_COVERAGE: &str = "alpha_to_coverage"; +pub(crate) const KEY_ALPHA_TO_ONE: &str = "alpha_to_one"; +pub(crate) const KEY_DEPTH_CLAMP: &str = "depth_clamp"; +pub(crate) const KEY_FRAME_LIMIT: &str = "frame_limit"; +pub(crate) const KEY_FRAME_LIMIT_CADENCE: &str = "frame_limit_cadence"; +pub(crate) const KEY_FRAME_LIMIT_METHOD: &str = "frame_limit_method"; +pub(crate) const KEY_FRAME_PACING: &str = "frame_pacing"; + +pub(crate) const NAME_DEFAULT: &str = "default"; +pub(crate) const NAME_EMPTY: &str = ""; +pub(crate) const FLAG_HELP_LONG: &str = "--help"; +pub(crate) const FLAG_HELP_SHORT: &str = "-h"; +pub(crate) const FLAG_SEPARATOR: &str = "--"; +pub(crate) const FLATPAK_SUFFIX: &str = "/flatpak"; +pub(crate) const LEVEL_WORD_OFF: &str = "off"; +pub(crate) const LEVEL_WORD_ERROR: &str = "error"; +pub(crate) const LEVEL_WORD_INFO: &str = "info"; +pub(crate) const LEVEL_WORD_WARN: &str = "warn"; +pub(crate) const SHADER_GROUPS_TYPE: u32 = 1000277002; + +pub(crate) const SOURCE_CONSTANT_OFFSET: i32 = 0; +pub(crate) const SOURCE_PUSH_INDEX: i32 = 1; +pub(crate) const SOURCE_INDIRECT_INDEX: i32 = 2; +pub(crate) const SOURCE_INDIRECT_INDEX_ARRAY: i32 = 3; +pub(crate) const SOURCE_SHADER_RECORD_INDEX: i32 = 8; + +pub(crate) const DEVICE_GROUP_SWAPCHAIN_TYPE: u32 = 1000060012; +pub(crate) const IMAGE_COMPRESSION_CONTROL_TYPE: u32 = 1000338001; +pub(crate) const IMAGE_FORMAT_LIST_TYPE: u32 = 1000147000; +pub(crate) const IMAGE_USAGE_FLAGS_2_TYPE: u32 = 1000668002; +pub(crate) const SWAPCHAIN_COUNTER_TYPE: u32 = 1000091003; +pub(crate) const SWAPCHAIN_NATIVE_HDR_TYPE: u32 = 1000213001; +pub(crate) const SWAPCHAIN_LATENCY_TYPE: u32 = 1000505007; +pub(crate) const SWAPCHAIN_PRESENT_BARRIER_TYPE: u32 = 1000292002; +pub(crate) const SWAPCHAIN_PRESENT_SCALING_TYPE: u32 = 1000275004; +pub(crate) const CUSTOM_RESOLVE_TYPE: u32 = 1000628002; +pub(crate) const DEBUG_UTILS_OBJECT_NAME_TYPE: u32 = 1000128000; +pub(crate) const PIPELINE_ROBUSTNESS_TYPE: u32 = 1000068000; +pub(crate) const STAGE_MODULE_IDENTIFIER_TYPE: u32 = 1000462002; +pub(crate) const STAGE_SUBGROUP_SIZE_TYPE: u32 = 1000225001; +pub(crate) const SHADER_MODULE_TYPE: u32 = 16; +pub(crate) const SHADER_MODULE_CACHE_TYPE: u32 = 1000160001; +pub(crate) const VALIDATION_FEATURES_TYPE: u32 = 1000247000; +pub(crate) const ATTACHMENT_SAMPLE_COUNT_TYPE: u32 = 1000044008; +pub(crate) const GRAPHICS_PIPELINE_LIBRARY_TYPE: u32 = 1000320002; +pub(crate) const MULTIVIEW_PER_VIEW_TYPE: u32 = 1000044009; +pub(crate) const PIPELINE_BINARY_INFO_TYPE: u32 = 1000483002; +pub(crate) const PIPELINE_COMPILER_CONTROL_TYPE: u32 = 1000183000; +pub(crate) const PIPELINE_CREATE_FLAGS_2_TYPE: u32 = 1000470005; +pub(crate) const PIPELINE_CREATION_FEEDBACK_TYPE: u32 = 1000192000; +pub(crate) const PIPELINE_DISCARD_RECTANGLE_TYPE: u32 = 1000099001; +pub(crate) const PIPELINE_DENSITY_LAYERED_TYPE: u32 = 1000611002; +pub(crate) const PIPELINE_SHADING_RATE_ENUM_TYPE: u32 = 1000326002; +pub(crate) const PIPELINE_SHADING_RATE_STATE_TYPE: u32 = 1000226001; +pub(crate) const PIPELINE_LIBRARY_TYPE: u32 = 1000290000; +pub(crate) const PIPELINE_RENDERING_TYPE: u32 = 1000044002; +pub(crate) const PIPELINE_REPRESENTATIVE_TYPE: u32 = 1000166001; +pub(crate) const RENDERING_ATTACHMENT_LOCATION_TYPE: u32 = 1000232001; +pub(crate) const RENDERING_INPUT_ATTACHMENT_TYPE: u32 = 1000232002; +pub(crate) const PIPELINE_CREATE_INFO_KHR_TYPE: u32 = 1000483007; +pub(crate) const GRAPHICS_PIPELINE_CREATE_INFO_TYPE: u32 = 28; +pub(crate) const COMPUTE_PIPELINE_CREATE_INFO_TYPE: u32 = 29; +pub(crate) const RAY_TRACING_PIPELINE_CREATE_INFO_KHR_TYPE: u32 = 1000150015; +pub(crate) const FN_GET_PIPELINE_KEY: &str = "vkGetPipelineKeyKHR"; +pub(crate) const FN_CREATE_PIPELINE_BINARIES: &str = "vkCreatePipelineBinariesKHR"; +pub(crate) const EXT_PIPELINE_BINARY: &str = "VK_KHR_pipeline_binary"; +pub(crate) const BINARY_INFO_LOG: &str = "pipeline: a create info naming pipeline binaries was rebuilt like any other"; +pub(crate) const WRAPPED_UNDECLARED_LOG: &str = "pipeline: the wrapped create info is one this build does not declare, leaving the setting alone"; + +pub(crate) const CHAIN_NODE_REASON: &str = "a node in the chain is one this build does not declare, leaving the setting alone"; +pub(crate) const SWAPCHAIN_MODE_LIST_TYPE: u32 = 1000275002; +pub(crate) const SURFACE_PRESENT_MODE_TYPE: u32 = 1000274000; +pub(crate) const SURFACE_INFO_2_TYPE: u32 = 1000119000; +pub(crate) const SURFACE_CAPABILITIES_2_TYPE: u32 = 1000119001; +pub(crate) const SWAPCHAIN_PRESENT_MODE_INFO_TYPE: u32 = 1000275003; +pub(crate) const DEVICE_GROUP_PRESENT_INFO_TYPE: u32 = 1000060011; +pub(crate) const DISPLAY_PRESENT_INFO_TYPE: u32 = 1000003000; +pub(crate) const FRAME_BOUNDARY_TYPE: u32 = 1000375001; +pub(crate) const FRAME_BOUNDARY_TENSORS_TYPE: u32 = 1000460023; +pub(crate) const PRESENT_ID_2_TYPE: u32 = 1000479001; +pub(crate) const PRESENT_ID_TYPE: u32 = 1000294000; +pub(crate) const PRESENT_REGIONS_TYPE: u32 = 1000084000; +pub(crate) const PRESENT_TIMES_GOOGLE_TYPE: u32 = 1000092000; +pub(crate) const PRESENT_TIMINGS_TYPE: u32 = 1000208003; +pub(crate) const SET_PRESENT_CONFIG_TYPE: u32 = 1000613000; +pub(crate) const SWAPCHAIN_PRESENT_FENCE_TYPE: u32 = 1000275001; +pub(crate) const EXT_SURFACE_MAINTENANCE_1: &str = "VK_KHR_surface_maintenance1"; +pub(crate) const EXT_SURFACE_MAINTENANCE_1_EXT: &str = "VK_EXT_surface_maintenance1"; + +pub(crate) const PRESENT_UNKNOWN_PREFIX: &str = "present mode "; +pub(crate) const ALPHA_UNKNOWN_PREFIX: &str = "composite alpha "; + +pub(crate) const FILTER_NEAREST: i32 = 0; +pub(crate) const FILTER_LINEAR: i32 = 1; +pub(crate) const FILTER_CUBIC: i32 = 1000015000; + +pub(crate) const MIPMAP_NEAREST: i32 = 0; +pub(crate) const MIPMAP_LINEAR: i32 = 1; + +pub(crate) const TOGGLE_OFF: vk::Bool32 = vk::FALSE; +pub(crate) const TOGGLE_ON: vk::Bool32 = vk::TRUE; + +pub(crate) const TEXT_NEAREST: &str = "nearest"; +pub(crate) const TEXT_LINEAR: &str = "linear"; +pub(crate) const TEXT_OFF: &str = "off"; +pub(crate) const TEXT_ON: &str = "on"; +pub(crate) const FILTER_UNKNOWN_PREFIX: &str = "filter "; +pub(crate) const MIPMAP_UNKNOWN_PREFIX: &str = "mipmap mode "; + +pub(crate) const CADENCE_FIXED: &str = "fixed"; +pub(crate) const CADENCE_SMOOTH: &str = "smooth"; +pub(crate) const CADENCE_DYNAMIC: &str = "dynamic"; +pub(crate) const METHOD_EARLY: &str = "early"; +pub(crate) const METHOD_LATE: &str = "late"; +pub(crate) const METHOD_REACTIVE: &str = "reactive"; +pub(crate) const PACING_SLEEP: &str = "sleep"; +pub(crate) const PACING_SLICED: &str = "sliced"; +pub(crate) const PACING_PRECISE: &str = "precise"; +pub(crate) const PACING_SPIN: &str = "spin"; + +pub(crate) const SETTINGS_FROZEN_INFO: &str = "settings loaded and frozen for the life of the process"; +pub(crate) const PRESENT_MISS_WARN: &str = "present_mode: the surface does not support it, keeping application choice"; +pub(crate) const PRESENT_LIST_REASON: &str = "the surface could not confirm the forced mode for the chained mode list, keeping the application value"; +pub(crate) const PRESENT_TIE_REASON: &str = "the application set swapchain flags or a scaling node, keeping the application value"; +pub(crate) const PRESENT_ABOVE_FLOOR_REASON: &str = "the application picked a mode an extension defines, keeping the application value"; +pub(crate) const COUNT_SHARED_REASON: &str = "the application picked a shared present mode, which keeps a single image"; +pub(crate) const PRESENT_EMPTY_WARN: &str = "present_mode: the choice matched no supported mode, keeping every mode"; + +pub(crate) const ALPHA_MISS_WARN: &str = "composite_alpha: the surface does not support it, keeping application choice"; +pub(crate) const UNOWNED_QUEUE_ERROR: &str = "present on a queue with no registered device"; +pub(crate) const UNOWNED_BUFFER_ERROR: &str = "dynamic state on a command buffer with no registered device"; + +pub(crate) const GPU_EMPTY_WARN: &str = "gpu device: the choice matched no device, keeping every device"; +pub(crate) const GROUP_EMPTY_WARN: &str = "gpu device: the choice matched no device group, keeping every group"; +pub(crate) const GPU_MISS_WARN: &str = "gpu device: the choice did not take, the application kept the device it picked"; +pub(crate) const ALPHA_OPAQUE_INFO: &str = "composite_alpha: opaque skips compositor blending"; + +pub(crate) const REPORT_MARK: &str = ": "; +pub(crate) const REPORT_SEP: &str = ", "; +pub(crate) const REPORT_ASKED: &str = "asked "; +pub(crate) const REPORT_APPLIED: &str = "applied "; +pub(crate) const NOTE_NOT_ENABLED: &str = "the application did not enable "; +pub(crate) const NOTE_NOT_SET: &str = "the profile did not set it"; +pub(crate) const NOTE_MIXED_SAMPLES: &str = "the application enabled mixed framebuffer samples, so only off is forced"; + +pub(crate) const FEATURE_ANISOTROPY: &str = "samplerAnisotropy"; +pub(crate) const FEATURE_SHADING: &str = "sampleRateShading"; +pub(crate) const FEATURE_ALPHA_ONE: &str = "alphaToOne"; +pub(crate) const FEATURE_DEPTH_CLAMP: &str = "depthClamp"; + +pub(crate) const SETTING_GPU: &str = "gpu device"; +pub(crate) const SETTING_PRESENT_MODE: &str = "present_mode"; +pub(crate) const SETTING_IMAGE_COUNT: &str = "image_count"; +pub(crate) const SETTING_COMPOSITE_ALPHA: &str = "composite_alpha"; +pub(crate) const SETTING_CLIPPED: &str = "clipped"; +pub(crate) const SETTING_MAG_FILTER: &str = "mag_filter"; +pub(crate) const SETTING_MIN_FILTER: &str = "min_filter"; +pub(crate) const SETTING_MIPMAP_MODE: &str = "mipmap_mode"; +pub(crate) const SETTING_ANISOTROPY: &str = "anisotropy"; +pub(crate) const SETTING_LOD_BIAS: &str = "lod_bias"; +pub(crate) const SETTING_MIP_FLOOR: &str = "mip_floor"; +pub(crate) const SETTING_MIP_CEILING: &str = "mip_ceiling"; +pub(crate) const SETTING_SAMPLE_SHADING: &str = "sample_shading"; +pub(crate) const SETTING_ALPHA_COVERAGE: &str = "alpha_to_coverage"; +pub(crate) const SETTING_ALPHA_ONE: &str = "alpha_to_one"; +pub(crate) const SETTING_DEPTH_CLAMP: &str = "depth_clamp"; +pub(crate) const SETTING_FRAME_LIMIT: &str = "frame_limit"; +pub(crate) const SETTING_FRAME_LIMIT_CADENCE: &str = "frame_limit_cadence"; +pub(crate) const SETTING_FRAME_LIMIT_METHOD: &str = "frame_limit_method"; +pub(crate) const SETTING_FRAME_PACING: &str = "frame_pacing"; + +pub(crate) const NULL_OK: [&str; 2] = ["vkGetInstanceProcAddr", "vkCreateInstance"]; + +pub(crate) const USAGE: &str = "usage: volt [--probe] [PROFILE] -- COMMAND [ARGS...]\n volt -- CMD run CMD with the default profile (~/.config/volt-gui/default.toml)\n volt NAME -- CMD run CMD with profile ~/.config/volt-gui/NAME.toml\n volt --probe NAME -- CMD the same, and record what this device supports\n\nsettings are read once when the application starts and never change while it\nruns: edit the profile, then start the application again\n"; + +pub(crate) const FN_CREATE_SWAPCHAIN: &str = "vkCreateSwapchainKHR"; +pub(crate) const FN_DESTROY_SWAPCHAIN: &str = "vkDestroySwapchainKHR"; +pub(crate) const FN_QUEUE_PRESENT: &str = "vkQueuePresentKHR"; +pub(crate) const FN_DEVICE_QUEUE_2: &str = "vkGetDeviceQueue2"; + +pub(crate) const FN_SURFACE_CAPS_2: &str = "vkGetPhysicalDeviceSurfaceCapabilities2KHR"; +pub(crate) const FN_DEVICE_GROUPS: &str = "vkEnumeratePhysicalDeviceGroups"; +pub(crate) const FN_DEVICE_GROUPS_KHR: &str = "vkEnumeratePhysicalDeviceGroupsKHR"; +pub(crate) const FN_SHARED_SWAPCHAINS: &str = "vkCreateSharedSwapchainsKHR"; +pub(crate) const FN_WRITE_SAMPLERS: &str = "vkWriteSamplerDescriptorsEXT"; +pub(crate) const FN_SET_ALPHA_COVERAGE: &str = "vkCmdSetAlphaToCoverageEnableEXT"; +pub(crate) const FN_SET_ALPHA_ONE: &str = "vkCmdSetAlphaToOneEnableEXT"; +pub(crate) const FN_SET_DEPTH_CLAMP: &str = "vkCmdSetDepthClampEnableEXT"; +pub(crate) const FN_CREATE_XCB_SURFACE: &str = "vkCreateXcbSurfaceKHR"; +pub(crate) const FN_CREATE_XLIB_SURFACE: &str = "vkCreateXlibSurfaceKHR"; +pub(crate) const FN_CREATE_WAYLAND_SURFACE: &str = "vkCreateWaylandSurfaceKHR"; +pub(crate) const FN_DESTROY_SURFACE: &str = "vkDestroySurfaceKHR"; +pub(crate) const FN_CREATE_COMPUTE_PIPELINES: &str = "vkCreateComputePipelines"; +pub(crate) const FN_CREATE_SHADERS: &str = "vkCreateShadersEXT"; +pub(crate) const FN_CREATE_RAY_TRACING_KHR: &str = "vkCreateRayTracingPipelinesKHR"; +pub(crate) const FN_CREATE_RAY_TRACING_NV: &str = "vkCreateRayTracingPipelinesNV"; +pub(crate) const FN_PIPELINE_INDIRECT_MEMORY: &str = "vkGetPipelineIndirectMemoryRequirementsNV"; + +pub(crate) const TAG_XCB: &str = "xcb"; +pub(crate) const TAG_WAYLAND: &str = "wayland"; + +pub(crate) const FN_GET_INSTANCE_PROC_ADDR: &str = "vkGetInstanceProcAddr"; +pub(crate) const FN_GET_DEVICE_PROC_ADDR: &str = "vkGetDeviceProcAddr"; +pub(crate) const FN_CREATE_INSTANCE: &str = "vkCreateInstance"; +pub(crate) const FN_DESTROY_INSTANCE: &str = "vkDestroyInstance"; +pub(crate) const FN_CREATE_DEVICE: &str = "vkCreateDevice"; +pub(crate) const FN_DESTROY_DEVICE: &str = "vkDestroyDevice"; +pub(crate) const FN_ENUMERATE_DEVICES: &str = "vkEnumeratePhysicalDevices"; +pub(crate) const FN_CREATE_GRAPHICS_PIPELINES: &str = "vkCreateGraphicsPipelines"; +pub(crate) const FN_CREATE_SAMPLER: &str = "vkCreateSampler"; +pub(crate) const FN_ALLOCATE_COMMAND_BUFFERS: &str = "vkAllocateCommandBuffers"; +pub(crate) const FN_FREE_COMMAND_BUFFERS: &str = "vkFreeCommandBuffers"; +pub(crate) const FN_DESTROY_COMMAND_POOL: &str = "vkDestroyCommandPool"; +pub(crate) const FN_DEVICE_QUEUE: &str = "vkGetDeviceQueue"; +pub(crate) const FN_SURFACE_PRESENT_MODES: &str = "vkGetPhysicalDeviceSurfacePresentModesKHR"; +pub(crate) const FN_SURFACE_CAPS: &str = "vkGetPhysicalDeviceSurfaceCapabilitiesKHR"; + +pub(crate) const EXT_SURFACE: &str = "VK_KHR_surface"; +pub(crate) const EXT_SWAPCHAIN: &str = "VK_KHR_swapchain"; +pub(crate) const EXT_DISPLAY_SWAPCHAIN: &str = "VK_KHR_display_swapchain"; +pub(crate) const EXT_DESCRIPTOR_HEAP: &str = "VK_EXT_descriptor_heap"; +pub(crate) const EXT_SHADER_OBJECT: &str = "VK_EXT_shader_object"; +pub(crate) const EXT_RAY_TRACING_PIPELINE: &str = "VK_KHR_ray_tracing_pipeline"; +pub(crate) const EXT_NV_RAY_TRACING: &str = "VK_NV_ray_tracing"; +pub(crate) const EXT_NV_GENERATED_COMPUTE: &str = "VK_NV_device_generated_commands_compute"; +pub(crate) const EXT_DYNAMIC_STATE_3: &str = "VK_EXT_extended_dynamic_state3"; +pub(crate) const EXT_DEVICE_GROUP_CREATION: &str = "VK_KHR_device_group_creation"; +pub(crate) const EXT_GET_SURFACE_CAPS_2: &str = "VK_KHR_get_surface_capabilities2"; +pub(crate) const EXT_XCB_SURFACE: &str = "VK_KHR_xcb_surface"; +pub(crate) const EXT_XLIB_SURFACE: &str = "VK_KHR_xlib_surface"; +pub(crate) const EXT_WAYLAND_SURFACE: &str = "VK_KHR_wayland_surface"; + +pub(crate) enum Provider { + Version(u32), + Ext(&'static str), +} + +pub(crate) const HOOK_PROVIDERS: &[(&str, Provider)] = &[ + (FN_SURFACE_PRESENT_MODES, Provider::Ext(EXT_SURFACE)), + (FN_SURFACE_CAPS, Provider::Ext(EXT_SURFACE)), + (FN_DESTROY_SURFACE, Provider::Ext(EXT_SURFACE)), + (FN_SURFACE_CAPS_2, Provider::Ext(EXT_GET_SURFACE_CAPS_2)), + (FN_DEVICE_GROUPS, Provider::Version(vk::API_VERSION_1_1)), + (FN_DEVICE_GROUPS_KHR, Provider::Ext(EXT_DEVICE_GROUP_CREATION)), + (FN_CREATE_XCB_SURFACE, Provider::Ext(EXT_XCB_SURFACE)), + (FN_CREATE_XLIB_SURFACE, Provider::Ext(EXT_XLIB_SURFACE)), + (FN_CREATE_WAYLAND_SURFACE, Provider::Ext(EXT_WAYLAND_SURFACE)), + (FN_CREATE_SWAPCHAIN, Provider::Ext(EXT_SWAPCHAIN)), + (FN_DESTROY_SWAPCHAIN, Provider::Ext(EXT_SWAPCHAIN)), + (FN_QUEUE_PRESENT, Provider::Ext(EXT_SWAPCHAIN)), + (FN_DEVICE_QUEUE_2, Provider::Version(vk::API_VERSION_1_1)), + (FN_SHARED_SWAPCHAINS, Provider::Ext(EXT_DISPLAY_SWAPCHAIN)), + (FN_WRITE_SAMPLERS, Provider::Ext(EXT_DESCRIPTOR_HEAP)), + (FN_CREATE_SHADERS, Provider::Ext(EXT_SHADER_OBJECT)), + (FN_CREATE_RAY_TRACING_KHR, Provider::Ext(EXT_RAY_TRACING_PIPELINE)), + (FN_CREATE_RAY_TRACING_NV, Provider::Ext(EXT_NV_RAY_TRACING)), + (FN_PIPELINE_INDIRECT_MEMORY, Provider::Ext(EXT_NV_GENERATED_COMPUTE)), + (FN_SET_ALPHA_COVERAGE, Provider::Ext(EXT_DYNAMIC_STATE_3)), + (FN_SET_ALPHA_COVERAGE, Provider::Ext(EXT_SHADER_OBJECT)), + (FN_SET_ALPHA_ONE, Provider::Ext(EXT_DYNAMIC_STATE_3)), + (FN_SET_ALPHA_ONE, Provider::Ext(EXT_SHADER_OBJECT)), + (FN_SET_DEPTH_CLAMP, Provider::Ext(EXT_DYNAMIC_STATE_3)), + (FN_SET_DEPTH_CLAMP, Provider::Ext(EXT_SHADER_OBJECT)), + (FN_GET_PIPELINE_KEY, Provider::Ext(EXT_PIPELINE_BINARY)), + (FN_CREATE_PIPELINE_BINARIES, Provider::Ext(EXT_PIPELINE_BINARY)), +]; + +pub(crate) const CORE_10_DEVICE_HOOKS: [&str; 9] = [ + FN_GET_DEVICE_PROC_ADDR, + FN_DESTROY_DEVICE, + FN_CREATE_GRAPHICS_PIPELINES, + FN_CREATE_COMPUTE_PIPELINES, + FN_CREATE_SAMPLER, + FN_ALLOCATE_COMMAND_BUFFERS, + FN_FREE_COMMAND_BUFFERS, + FN_DESTROY_COMMAND_POOL, + FN_DEVICE_QUEUE, +]; + +pub(crate) const SURFACE_CREATORS: [(&str, &str); 3] = [ + (FN_CREATE_XCB_SURFACE, TAG_XCB), + (FN_CREATE_XLIB_SURFACE, TAG_XCB), + (FN_CREATE_WAYLAND_SURFACE, TAG_WAYLAND), +]; + +pub(crate) const SECTION_GPU: &str = "gpu"; +pub(crate) const SECTION_DISPLAY: &str = "display"; +pub(crate) const SECTION_TEXTURES: &str = "textures"; +pub(crate) const SECTION_RENDERING: &str = "rendering"; +pub(crate) const SECTION_FRAMERATE: &str = "framerate"; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum PacingChoice { + Sleep, + Sliced, + Precise, + Spin, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum MethodChoice { + Early, + Late, + Reactive, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum CadenceChoice { + Fixed, + Smooth, + Dynamic, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum LimitStage { + Before, + After, +} + +pub(crate) const DEFAULT_CONFIG: &str = r#"# volt profile +# every setting is one value: the value volt forces, or "default", which +# keeps whatever the application asked for. there is no range and no order +# +# a value volt has no name for is never read from a profile +# +# anisotropy, sample_shading, alpha_to_one and depth_clamp need a device +# feature the application itself enabled. volt never enables one: where the +# application left the feature clear the setting is ignored and a line is +# logged +# +# a forced value the device did not report is not forced: volt keeps the +# application's own value and logs a warning +# +# settings are read once when the application starts. changing this file has +# no effect on an application that is already running: start it again + +[gpu] +device = "default" + +[display] +present_mode = "default" +image_count = "default" +composite_alpha = "default" +clipped = "default" + +[textures] +mag_filter = "default" +min_filter = "default" +mipmap_mode = "default" +anisotropy = "default" +lod_bias = "default" +mip_floor = "default" +mip_ceiling = "default" + +[rendering] +sample_shading = "default" +alpha_to_coverage = "default" +alpha_to_one = "default" +depth_clamp = "default" + +[framerate] +frame_limit = "default" +frame_limit_cadence = "default" +frame_limit_method = "default" +frame_pacing = "default" +"#; diff --git a/src/volt/device.rs b/src/volt/device.rs new file mode 100644 index 0000000..42ef7b6 --- /dev/null +++ b/src/volt/device.rs @@ -0,0 +1,551 @@ +use std::collections::HashMap; +use std::collections::HashSet; +use std::ffi::c_void; +use std::mem; +use std::sync::Arc; +use std::sync::RwLock; + +use ash::vk; +use ash::vk::Handle; + +use crate::config::ensure_settings; +use crate::consts::FN_CREATE_RAY_TRACING_KHR; +use crate::consts::FN_CREATE_RAY_TRACING_NV; +use crate::consts::FN_CREATE_PIPELINE_BINARIES; +use crate::consts::FN_CREATE_SHADERS; +use crate::consts::FN_GET_PIPELINE_KEY; +use crate::consts::FN_PIPELINE_INDIRECT_MEMORY; +use crate::consts::FN_SET_ALPHA_COVERAGE; +use crate::consts::FN_SET_ALPHA_ONE; +use crate::consts::FN_SET_DEPTH_CLAMP; +use crate::consts::FN_SHARED_SWAPCHAINS; +use crate::consts::FN_WRITE_SAMPLERS; +use crate::consts::DEVICE_FEATURES_2_TYPE; +use crate::consts::EXT_MIXED_SAMPLES; +use crate::consts::FN_CREATE_DEVICE; +use crate::consts::FN_DEVICE_QUEUE_2; +use crate::consts::LOG_DEVICE_REGISTERED; +use crate::consts::EXT_PORTABILITY_SUBSET; +use crate::consts::DEVICE_GROUP_DEVICE_CREATE_INFO_TYPE; +use crate::consts::GPU_MISS_WARN; +use crate::consts::SETTING_GPU; +use crate::env::env_probe_active; +use crate::instance::call_all_devices; +use crate::instance::call_next_gdpa; +use crate::instance::call_next_gipa; +use crate::instance::cstr_names; +use crate::instance::device_index; +use crate::instance::owning_instance; +use crate::instance::provider_on; +use crate::instance::PfnCmdSetAlphaToCoverage; +use crate::instance::PfnCmdSetAlphaToOne; +use crate::instance::PfnCmdSetDepthClamp; +use crate::instance::PfnCreateRayTracingKHR; +use crate::instance::PfnCreateRayTracingNV; +use crate::instance::PfnCreateShaders; +use crate::instance::PfnCreatePipelineBinaries; +use crate::instance::PfnCreateSharedSwapchains; +use crate::instance::PfnGetDeviceQueue2; +use crate::instance::PfnGetPipelineKey; +use crate::instance::PfnPipelineIndirectMemory; +use crate::instance::PfnSetDeviceLoaderData; +use crate::instance::PfnWriteSamplers; +use crate::instance::walked_nodes; +use crate::instance::VkDeviceGroupDeviceCreateInfo; +use crate::instance::VkInstState; +use crate::instance::VkPhysicalDeviceFeatures2; +use crate::instance::VkLayerLinkInfo; +use crate::logging::info_wanted; +use crate::logging::call_log_at; +use crate::logging::LogLevel; +use crate::present::call_forget_device_timelines; +use crate::probe::call_build_device; +use crate::probe::call_record_device; +use crate::report::call_forget_reports; +use crate::report::call_report_reading; +use crate::report::call_report_value; +use crate::report::count_text; +use crate::swapchain::call_forget_device_forced_modes; + +#[derive(Clone, Copy, Default)] +pub(crate) struct DeviceCaps { + pub(crate) sampler_anisotropy: bool, + pub(crate) sample_rate_shading: bool, + pub(crate) alpha_to_one: bool, + pub(crate) depth_clamp: bool, + pub(crate) max_anisotropy: f32, + pub(crate) max_lod_bias: f32, + pub(crate) max_lod_level: f32, + pub(crate) portability_subset: bool, + pub(crate) mixed_samples: bool, +} + +pub(crate) struct VkDevState { + pub(crate) device: ash::Device, + pub(crate) phys: vk::PhysicalDevice, + pub(crate) gdpa: vk::PFN_vkGetDeviceProcAddr, + pub(crate) loader_data: Option, + pub(crate) swap_fp: ash::khr::swapchain::DeviceFn, + pub(crate) queue2_fp: Option, + pub(crate) shared_fp: Option, + pub(crate) samplers_fp: Option, + pub(crate) shaders_fp: Option, + pub(crate) ray_khr_fp: Option, + pub(crate) ray_nv_fp: Option, + pub(crate) indirect_memory_fp: Option, + pub(crate) alpha_fp: Option, + pub(crate) alpha_one_fp: Option, + pub(crate) clamp_fp: Option, + pub(crate) caps: DeviceCaps, + pub(crate) instance_handle: u64, + pub(crate) api_version: u32, + pub(crate) extensions: HashSet, + pub(crate) pipeline_key_fp: Option, + pub(crate) pipeline_binaries_fp: Option, +} + +pub(crate) fn device_hook_on(dev: &VkDevState, command: &str) -> bool { + provider_on(dev.api_version, &dev.extensions, command) +} + +pub(crate) fn requested_device_extensions(ci: *const vk::DeviceCreateInfo<'_>) -> HashSet { + cstr_names(unsafe { (*ci).pp_enabled_extension_names }, unsafe { + (*ci).enabled_extension_count + }) +} + +static DEVS: RwLock>>> = RwLock::new(None); +static QUEUE_TO_DEV: RwLock>> = RwLock::new(None); +static CMDBUF_TO_DEV: RwLock>> = RwLock::new(None); + +pub(crate) fn devs_get(h: u64) -> Option> { + DEVS.read() + .ok() + .and_then(|g| g.as_ref().and_then(|m| m.get(&h).cloned())) +} + +pub(crate) fn devs_gdpa(h: u64) -> Option { + DEVS.read() + .ok() + .and_then(|g| g.as_ref().and_then(|m| m.get(&h).map(|d| d.gdpa))) +} + +pub(crate) fn call_devs_put(h: u64, v: VkDevState) { + match DEVS.write() { + Ok(mut g) => { + g.get_or_insert_with(HashMap::new).insert(h, Arc::new(v)); + } + Err(_) => (), + } +} + +fn call_queue_dev_forget(dev: u64) { + match QUEUE_TO_DEV.write() { + Ok(mut g) => g + .iter_mut() + .for_each(|m| m.retain(|_, owner| *owner != dev)), + Err(_) => (), + } +} + +fn call_cmdbuf_dev_forget(dev: u64) { + match CMDBUF_TO_DEV.write() { + Ok(mut g) => g + .iter_mut() + .for_each(|m| m.retain(|_, owner| owner.0 != dev)), + Err(_) => (), + } +} + +pub(crate) fn call_devs_del(h: u64) -> Option> { + call_queue_dev_forget(h); + call_cmdbuf_dev_forget(h); + call_forget_reports(h); + call_forget_device_timelines(h); + call_forget_device_forced_modes(h); + DEVS.write() + .ok() + .and_then(|mut g| g.as_mut().and_then(|m| m.remove(&h))) +} + +fn queue_dev_get(q: u64) -> Option { + QUEUE_TO_DEV + .read() + .ok() + .and_then(|g| g.as_ref().and_then(|m| m.get(&q).copied())) +} + +pub(crate) fn call_queue_dev_put(q: u64, d: u64) { + match QUEUE_TO_DEV.write() { + Ok(mut g) => { + g.get_or_insert_with(HashMap::new).insert(q, d); + } + Err(_) => (), + } +} + +pub(crate) fn queue_owner(queue: vk::Queue) -> Option> { + queue_dev_get(queue.as_raw()).and_then(devs_get) +} + +fn cmdbuf_dev_get(c: u64) -> Option { + CMDBUF_TO_DEV + .read() + .ok() + .and_then(|g| g.as_ref().and_then(|m| m.get(&c).map(|owner| owner.0))) +} + +fn call_cmdbuf_dev_put(c: u64, owner: (u64, u64)) { + match CMDBUF_TO_DEV.write() { + Ok(mut g) => { + g.get_or_insert_with(HashMap::new).insert(c, owner); + } + Err(_) => (), + } +} + +fn call_cmdbuf_dev_del(c: u64) { + match CMDBUF_TO_DEV.write() { + Ok(mut g) => { + g.get_or_insert_with(HashMap::new).remove(&c); + } + Err(_) => (), + } +} + +fn call_cmdbuf_pool_forget(dev: u64, pool: u64) { + match CMDBUF_TO_DEV.write() { + Ok(mut g) => g + .iter_mut() + .for_each(|m| m.retain(|_, owner| *owner != (dev, pool))), + Err(_) => (), + } +} + +pub(crate) fn cmdbuf_owner(buffer: vk::CommandBuffer) -> Option> { + cmdbuf_dev_get(buffer.as_raw()).and_then(devs_get) +} + +fn lod_levels_for(max_dimension: u32) -> f32 { + (max_dimension.max(1) as f32).log2().floor() +} + +pub(crate) fn limit_caps(props: &vk::PhysicalDeviceProperties) -> DeviceCaps { + DeviceCaps { + max_anisotropy: props.limits.max_sampler_anisotropy, + max_lod_bias: props.limits.max_sampler_lod_bias, + max_lod_level: lod_levels_for(props.limits.max_image_dimension2_d), + ..DeviceCaps::default() + } +} + +fn build_caps( + props: &vk::PhysicalDeviceProperties, + asked: &vk::PhysicalDeviceFeatures, + extensions: &HashSet, +) -> DeviceCaps { + DeviceCaps { + sampler_anisotropy: asked.sampler_anisotropy == vk::TRUE, + sample_rate_shading: asked.sample_rate_shading == vk::TRUE, + alpha_to_one: asked.alpha_to_one == vk::TRUE, + depth_clamp: asked.depth_clamp == vk::TRUE, + max_anisotropy: props.limits.max_sampler_anisotropy, + max_lod_bias: props.limits.max_sampler_lod_bias, + max_lod_level: lod_levels_for(props.limits.max_image_dimension2_d), + portability_subset: extensions.contains(EXT_PORTABILITY_SUBSET), + mixed_samples: extensions.contains(EXT_MIXED_SAMPLES), + } +} + +fn chained_features(p_next: *const c_void) -> Option { + walked_nodes(p_next) + .into_iter() + .find(|node| unsafe { (**node).s_type.as_raw() } as u32 == DEVICE_FEATURES_2_TYPE) + .map(|node| unsafe { (*(node as *const VkPhysicalDeviceFeatures2)).features }) +} + +fn plain_features(ci: &vk::DeviceCreateInfo<'_>) -> Option { + match ci.p_enabled_features.is_null() { + true => None, + false => Some(unsafe { *ci.p_enabled_features }), + } +} + +fn asked_features(ci: *const vk::DeviceCreateInfo<'_>) -> vk::PhysicalDeviceFeatures { + match chained_features(unsafe { (*ci).p_next }) { + Some(features) => features, + None => plain_features(unsafe { &*ci }).unwrap_or_default(), + } +} + +fn load_swap_fp( + gdpa: vk::PFN_vkGetDeviceProcAddr, + handle: vk::Device, +) -> ash::khr::swapchain::DeviceFn { + ash::khr::swapchain::DeviceFn::load(|name| unsafe { + mem::transmute(gdpa(handle, name.as_ptr())) + }) +} + +fn call_typed_device_fp( + gdpa: vk::PFN_vkGetDeviceProcAddr, + handle: vk::Device, + name: &str, +) -> Option { + call_next_gdpa(gdpa, handle, name).map(|f| unsafe { mem::transmute_copy(&f) }) +} + + +fn call_loader_data(fp: PfnSetDeviceLoaderData, handle: vk::Device, queue: vk::Queue) { + let _ = unsafe { fp(handle, queue.as_raw() as usize as *mut c_void) }; +} + +pub(crate) fn call_register_queue(dev: &VkDevState, handle: vk::Device, queue: vk::Queue) { + match dev.loader_data { + Some(fp) => call_loader_data(fp, handle, queue), + None => (), + } +} + +fn device_caps( + inst: &VkInstState, + phys: vk::PhysicalDevice, + ci: *const vk::DeviceCreateInfo<'_>, +) -> DeviceCaps { + build_caps( + unsafe { &inst.instance.get_physical_device_properties(phys) }, + &asked_features(ci), + &requested_device_extensions(ci), + ) +} + +fn call_record_command_buffers( + dev: vk::Device, + info: *const vk::CommandBufferAllocateInfo<'_>, + out: *mut vk::CommandBuffer, +) { + (0..unsafe { (*info).command_buffer_count } as usize).for_each(|at| { + call_cmdbuf_dev_put( + unsafe { (*out.add(at)).as_raw() }, + (dev.as_raw(), unsafe { (*info).command_pool.as_raw() }), + ) + }); +} + +fn call_forget_command_buffers(count: u32, buffers: *const vk::CommandBuffer) { + (0..count as usize).for_each(|at| call_cmdbuf_dev_del(unsafe { (*buffers.add(at)).as_raw() })); +} + +pub(crate) fn call_allocate_command_buffers( + d: &VkDevState, + dev: vk::Device, + info: *const vk::CommandBufferAllocateInfo<'_>, + out: *mut vk::CommandBuffer, +) -> vk::Result { + match unsafe { (d.device.fp_v1_0().allocate_command_buffers)(dev, info, out) } { + vk::Result::SUCCESS => { + call_record_command_buffers(dev, info, out); + vk::Result::SUCCESS + } + e => e, + } +} + +pub(crate) fn call_free_command_buffers( + d: &VkDevState, + dev: vk::Device, + pool: vk::CommandPool, + count: u32, + buffers: *const vk::CommandBuffer, +) { + call_forget_command_buffers(count, buffers); + unsafe { (d.device.fp_v1_0().free_command_buffers)(dev, pool, count, buffers) }; +} + +pub(crate) fn call_destroy_command_pool( + d: &VkDevState, + dev: vk::Device, + pool: vk::CommandPool, + alloc: *const vk::AllocationCallbacks<'_>, +) { + call_cmdbuf_pool_forget(dev.as_raw(), pool.as_raw()); + unsafe { (d.device.fp_v1_0().destroy_command_pool)(dev, pool, alloc) }; +} + +fn group_create_node(p_next: *const c_void) -> Option<*const VkDeviceGroupDeviceCreateInfo> { + walked_nodes(p_next) + .into_iter() + .find(|node| unsafe { (**node).s_type.as_raw() } as u32 == DEVICE_GROUP_DEVICE_CREATE_INFO_TYPE) + .map(|node| node as *const VkDeviceGroupDeviceCreateInfo) +} + +fn group_device_ids( + all: &[vk::PhysicalDevice], + node: *const VkDeviceGroupDeviceCreateInfo, +) -> Vec { + (0..unsafe { (*node).physical_device_count } as usize) + .map(|at| device_index(all, unsafe { *(*node).p_physical_devices.add(at) })) + .collect() +} + +fn asked_group(ci: *const vk::DeviceCreateInfo<'_>, all: &[vk::PhysicalDevice]) -> Option> { + group_create_node(unsafe { (*ci).p_next }).map(|node| group_device_ids(all, node)) +} + +fn call_gpu_missed(ids: &[u32], chosen: u32) { + match ids.contains(&chosen) { + true => (), + false => call_log_at(LogLevel::Warn, GPU_MISS_WARN), + } +} + +fn call_gpu_warned(id: u32, chosen: Option, group: Option>) { + match chosen { + Some(value) => call_gpu_missed(&group.unwrap_or_else(|| vec![id]), value), + None => (), + } +} + +fn call_gpu_reported(id: u32, owner: u64, chosen: Option) { + match info_wanted() { + true => match chosen { + Some(forced) => call_report_value(owner, SETTING_GPU, id, forced, count_text, None), + None => call_report_reading(owner, SETTING_GPU, count_text(id)), + }, + false => (), + } +} + +fn call_gpu_lines( + inst: &VkInstState, + phys: vk::PhysicalDevice, + ci: *const vk::DeviceCreateInfo<'_>, + owner: u64, + chosen: Option, +) { + let all = call_all_devices(inst); + let id = device_index(&all, phys); + call_gpu_warned(id, chosen, asked_group(ci, &all)); + call_gpu_reported(id, owner, chosen); +} + +fn gpu_line_wanted(chosen: Option) -> bool { + match chosen { + Some(_) => true, + None => info_wanted(), + } +} + +fn call_probe_device(inst: &VkInstState, phys: vk::PhysicalDevice, caps: &DeviceCaps) { + match env_probe_active() { + true => call_record_device(call_build_device(inst, phys, caps)), + false => (), + } +} + +fn call_report_gpu( + inst: &VkInstState, + phys: vk::PhysicalDevice, + ci: *const vk::DeviceCreateInfo<'_>, + handle: vk::Device, +) { + let chosen = ensure_settings().gpu; + match gpu_line_wanted(chosen) { + true => call_gpu_lines(inst, phys, ci, handle.as_raw(), chosen), + false => (), + } +} + +fn call_register_device( + gdpa: vk::PFN_vkGetDeviceProcAddr, + loader_data: Option, + handle: vk::Device, + inst: &VkInstState, + inst_handle: u64, + phys: vk::PhysicalDevice, + caps: DeviceCaps, + ci: *const vk::DeviceCreateInfo<'_>, +) { + let device = unsafe { + ash::Device::load_with(|name| mem::transmute(gdpa(handle, name.as_ptr())), handle) + }; + call_devs_put( + handle.as_raw(), + VkDevState { + device, + phys, + gdpa, + loader_data, + swap_fp: load_swap_fp(gdpa, handle), + queue2_fp: call_typed_device_fp(gdpa, handle, FN_DEVICE_QUEUE_2), + shared_fp: call_typed_device_fp(gdpa, handle, FN_SHARED_SWAPCHAINS), + samplers_fp: call_typed_device_fp(gdpa, handle, FN_WRITE_SAMPLERS), + shaders_fp: call_typed_device_fp(gdpa, handle, FN_CREATE_SHADERS), + ray_khr_fp: call_typed_device_fp(gdpa, handle, FN_CREATE_RAY_TRACING_KHR), + ray_nv_fp: call_typed_device_fp(gdpa, handle, FN_CREATE_RAY_TRACING_NV), + indirect_memory_fp: call_typed_device_fp(gdpa, handle, FN_PIPELINE_INDIRECT_MEMORY), + alpha_fp: call_typed_device_fp(gdpa, handle, FN_SET_ALPHA_COVERAGE), + alpha_one_fp: call_typed_device_fp(gdpa, handle, FN_SET_ALPHA_ONE), + clamp_fp: call_typed_device_fp(gdpa, handle, FN_SET_DEPTH_CLAMP), + caps, + instance_handle: inst_handle, + api_version: inst.api_version, + extensions: requested_device_extensions(ci), + pipeline_key_fp: call_typed_device_fp(gdpa, handle, FN_GET_PIPELINE_KEY), + pipeline_binaries_fp: call_typed_device_fp(gdpa, handle, FN_CREATE_PIPELINE_BINARIES), + }, + ); + call_probe_device(inst, phys, &caps); + call_report_gpu(inst, phys, ci, handle); + call_log_at(LogLevel::Info, LOG_DEVICE_REGISTERED); +} + +fn call_invoke_create_device( + create_fn: unsafe extern "system" fn(), + link: &VkLayerLinkInfo, + loader_data: Option, + inst: &VkInstState, + inst_handle: u64, + phys: vk::PhysicalDevice, + ci: *const vk::DeviceCreateInfo<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::Device, +) -> vk::Result { + let cf: vk::PFN_vkCreateDevice = unsafe { mem::transmute(create_fn) }; + match unsafe { cf(phys, ci, alloc, out) } { + vk::Result::SUCCESS => { + call_register_device( + link.pfn_next_get_device_proc_addr, + loader_data, + unsafe { *out }, + inst, + inst_handle, + phys, + device_caps(inst, phys, ci), + ci, + ); + vk::Result::SUCCESS + } + e => e, + } +} + +pub(crate) fn call_real_create_device( + link: Option, + loader_data: Option, + phys: vk::PhysicalDevice, + ci: *const vk::DeviceCreateInfo<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::Device, +) -> vk::Result { + match (link, owning_instance(phys)) { + (Some(l), Some((ih, inst))) => call_next_gipa( + l.pfn_next_get_instance_proc_addr, + vk::Instance::from_raw(ih), + FN_CREATE_DEVICE, + ) + .map(|f| call_invoke_create_device(f, &l, loader_data, &inst, ih, phys, ci, alloc, out)) + .unwrap_or(vk::Result::ERROR_INITIALIZATION_FAILED), + (_, _) => vk::Result::ERROR_INITIALIZATION_FAILED, + } +} diff --git a/volt/env.rs b/src/volt/env.rs similarity index 75% rename from volt/env.rs rename to src/volt/env.rs index bb4595e..8ebff25 100644 --- a/volt/env.rs +++ b/src/volt/env.rs @@ -3,7 +3,9 @@ use std::env; use crate::consts::DEFAULT_PROFILE; use crate::consts::ENV_CONFIG_NAME; use crate::consts::ENV_HOME; +use crate::consts::ENV_LIB_PATH; use crate::consts::ENV_LOG; +use crate::consts::LEVEL_WORD_WARN; use crate::consts::ENV_PROBE; fn read_var(key: &str) -> Option { @@ -15,7 +17,7 @@ pub(crate) fn env_string(key: &str, default: &str) -> String { } pub(crate) fn env_log_level() -> String { - env_string(ENV_LOG, "warn") + env_string(ENV_LOG, LEVEL_WORD_WARN) } pub(crate) fn env_config_name() -> String { @@ -30,6 +32,10 @@ pub(crate) fn env_home() -> Option { read_var(ENV_HOME) } -pub fn process_args() -> Vec { +pub(crate) fn env_lib_path() -> Option { + read_var(ENV_LIB_PATH) +} + +pub fn launch_args() -> Vec { env::args().skip(1).collect() } diff --git a/src/volt/instance.rs b/src/volt/instance.rs new file mode 100644 index 0000000..9af8a89 --- /dev/null +++ b/src/volt/instance.rs @@ -0,0 +1,1596 @@ +use std::collections::HashMap; +use std::collections::HashSet; +use std::ffi::c_char; +use std::ffi::c_void; +use std::ffi::CStr; +use std::ffi::CString; +use std::sync::Arc; +use std::mem; +use std::ptr; +use std::sync::RwLock; + +use ash::vk; +use ash::vk::Handle; + +use crate::config::ensure_settings; +use crate::consts::ATTACHMENT_SAMPLE_COUNT_TYPE; +use crate::consts::CHAIN_NODE_REASON; +use crate::consts::CUSTOM_RESOLVE_TYPE; +use crate::consts::DEBUG_UTILS_OBJECT_NAME_TYPE; +use crate::consts::DEVICE_GROUP_PROPERTIES_TYPE; +use crate::consts::DEVICE_GROUP_SIZE; +use crate::consts::DEVICE_GROUP_PRESENT_INFO_TYPE; +use crate::consts::DEVICE_GROUP_SWAPCHAIN_TYPE; +use crate::consts::DISPLAY_PRESENT_INFO_TYPE; +use crate::consts::FRAME_BOUNDARY_TENSORS_TYPE; +use crate::consts::FRAME_BOUNDARY_TYPE; +use crate::consts::PRESENT_ID_2_TYPE; +use crate::consts::PRESENT_ID_TYPE; +use crate::consts::PRESENT_REGIONS_TYPE; +use crate::consts::PRESENT_TIMES_GOOGLE_TYPE; +use crate::consts::PRESENT_TIMINGS_TYPE; +use crate::consts::SET_PRESENT_CONFIG_TYPE; +use crate::consts::SWAPCHAIN_PRESENT_FENCE_TYPE; +use crate::consts::FN_DESTROY_SURFACE; +use crate::consts::FN_DEVICE_GROUPS; +use crate::consts::GRAPHICS_PIPELINE_LIBRARY_TYPE; +use crate::consts::IMAGE_COMPRESSION_CONTROL_TYPE; +use crate::consts::IMAGE_FORMAT_LIST_TYPE; +use crate::consts::IMAGE_USAGE_FLAGS_2_TYPE; +use crate::consts::MULTIVIEW_PER_VIEW_TYPE; +use crate::consts::PIPELINE_BINARY_INFO_TYPE; +use crate::consts::PIPELINE_COMPILER_CONTROL_TYPE; +use crate::consts::PIPELINE_CREATE_FLAGS_2_TYPE; +use crate::consts::PIPELINE_CREATION_FEEDBACK_TYPE; +use crate::consts::PIPELINE_DENSITY_LAYERED_TYPE; +use crate::consts::PIPELINE_DISCARD_RECTANGLE_TYPE; +use crate::consts::PIPELINE_LIBRARY_TYPE; +use crate::consts::PIPELINE_RENDERING_TYPE; +use crate::consts::PIPELINE_REPRESENTATIVE_TYPE; +use crate::consts::PIPELINE_ROBUSTNESS_TYPE; +use crate::consts::PIPELINE_SHADING_RATE_ENUM_TYPE; +use crate::consts::PIPELINE_SHADING_RATE_STATE_TYPE; +use crate::consts::RENDERING_ATTACHMENT_LOCATION_TYPE; +use crate::consts::RENDERING_INPUT_ATTACHMENT_TYPE; +use crate::consts::SHADER_MODULE_CACHE_TYPE; +use crate::consts::SHADER_MODULE_TYPE; +use crate::consts::STAGE_MODULE_IDENTIFIER_TYPE; +use crate::consts::STAGE_SUBGROUP_SIZE_TYPE; +use crate::consts::SWAPCHAIN_COUNTER_TYPE; +use crate::consts::SWAPCHAIN_LATENCY_TYPE; +use crate::consts::SWAPCHAIN_NATIVE_HDR_TYPE; +use crate::consts::SWAPCHAIN_PRESENT_BARRIER_TYPE; +use crate::consts::SWAPCHAIN_PRESENT_SCALING_TYPE; +use crate::consts::VALIDATION_FEATURES_TYPE; +use crate::consts::FIRST_DEVICE; +use crate::consts::FN_DEVICE_GROUPS_KHR; +use crate::consts::FN_SURFACE_CAPS_2; +use crate::consts::FN_CREATE_INSTANCE; +use crate::consts::GPU_EMPTY_WARN; +use crate::consts::HOOK_PROVIDERS; +use crate::consts::LOG_INSTANCE_REGISTERED; +use crate::consts::Provider; +use crate::consts::GROUP_EMPTY_WARN; +use crate::consts::SINGLE_DEVICE; +use crate::consts::SURFACE_CREATORS; +use crate::consts::UNSET_API_VERSION; +use crate::device::limit_caps; +use crate::env::env_probe_active; +use crate::lists::call_warned; +use crate::lists::filtered; +use crate::lists::kept; +use crate::probe::call_build_device; +use crate::probe::call_record_device; +use crate::logging::call_log_at; +use crate::logging::LogLevel; + +pub(crate) type PfnSurfaceCaps2 = unsafe extern "system" fn( + vk::PhysicalDevice, + *const VkPhysicalDeviceSurfaceInfo2, + *mut VkSurfaceCapabilities2, +) -> vk::Result; + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkPhysicalDeviceGroupProperties { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *mut c_void, + pub(crate) physical_device_count: u32, + pub(crate) physical_devices: [vk::PhysicalDevice; DEVICE_GROUP_SIZE], + pub(crate) subset_allocation: vk::Bool32, +} + +pub(crate) type PfnDeviceGroups = unsafe extern "system" fn( + vk::Instance, + *mut u32, + *mut VkPhysicalDeviceGroupProperties, +) -> vk::Result; + +pub(crate) type PfnCreateSharedSwapchains = unsafe extern "system" fn( + vk::Device, + u32, + *const vk::SwapchainCreateInfoKHR<'_>, + *const vk::AllocationCallbacks<'_>, + *mut vk::SwapchainKHR, +) -> vk::Result; + +pub(crate) type PfnWriteSamplers = unsafe extern "system" fn( + vk::Device, + u32, + *const vk::SamplerCreateInfo<'_>, + *const c_void, +) -> vk::Result; + +pub(crate) type PfnCmdSetAlphaToCoverage = + unsafe extern "system" fn(vk::CommandBuffer, vk::Bool32); + +pub(crate) type PfnCmdSetAlphaToOne = + unsafe extern "system" fn(vk::CommandBuffer, vk::Bool32); + +pub(crate) type PfnCmdSetDepthClamp = + unsafe extern "system" fn(vk::CommandBuffer, vk::Bool32); + +pub(crate) type PfnGetDeviceQueue2 = + unsafe extern "system" fn(vk::Device, *const c_void, *mut vk::Queue); + +pub(crate) type PfnSetDeviceLoaderData = + unsafe extern "system" fn(vk::Device, *mut c_void) -> vk::Result; + +pub(crate) type PfnCreateSurface = unsafe extern "system" fn( + vk::Instance, + *const c_void, + *const vk::AllocationCallbacks<'_>, + *mut vk::SurfaceKHR, +) -> vk::Result; + +pub(crate) type PfnDestroySurface = unsafe extern "system" fn( + vk::Instance, + vk::SurfaceKHR, + *const vk::AllocationCallbacks<'_>, +); + +#[repr(C)] +pub(crate) struct VkLayerLink { + pub(crate) p_next: *mut VkLayerLink, + pub(crate) pfn_next_get_instance_proc_addr: vk::PFN_vkGetInstanceProcAddr, + pub(crate) pfn_next_get_device_proc_addr: vk::PFN_vkGetDeviceProcAddr, +} + +pub(crate) struct VkLayerLinkInfo { + pub(crate) pfn_next_get_instance_proc_addr: vk::PFN_vkGetInstanceProcAddr, + pub(crate) pfn_next_get_device_proc_addr: vk::PFN_vkGetDeviceProcAddr, +} + +#[repr(C)] +pub(crate) struct VkLayerCreateInfo { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) function: i32, + pub(crate) u_layer_info: *mut VkLayerLink, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkPhysicalDeviceSurfaceInfo2 { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) surface: vk::SurfaceKHR, +} + +#[repr(C)] +pub(crate) struct VkSurfaceCapabilities2 { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *mut c_void, + pub(crate) surface_capabilities: vk::SurfaceCapabilitiesKHR, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkHandle(pub(crate) u64); + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkChainNode { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *mut c_void, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkPipelineCreateInfoKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *mut c_void, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkPipelineBinaryCreateInfoKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) p_keys_and_data_info: *const c_void, + pub(crate) pipeline: vk::Pipeline, + pub(crate) p_pipeline_create_info: *const VkPipelineCreateInfoKHR, +} + +pub(crate) type PfnGetPipelineKey = unsafe extern "system" fn( + vk::Device, + *const VkPipelineCreateInfoKHR, + *mut c_void, +) -> vk::Result; + +pub(crate) type PfnCreatePipelineBinaries = unsafe extern "system" fn( + vk::Device, + *const VkPipelineBinaryCreateInfoKHR, + *const vk::AllocationCallbacks<'_>, + *mut c_void, +) -> vk::Result; + +#[repr(C)] +pub(crate) struct VkDeviceGroupDeviceCreateInfo { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) physical_device_count: u32, + pub(crate) p_physical_devices: *const vk::PhysicalDevice, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkPhysicalDeviceFeatures2 { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *mut c_void, + pub(crate) features: vk::PhysicalDeviceFeatures, +} + +#[repr(C)] +pub(crate) struct VkSurfacePresentModeKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *mut c_void, + pub(crate) present_mode: vk::PresentModeKHR, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkSwapchainPresentModeInfoKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) swapchain_count: u32, + pub(crate) p_present_modes: *const vk::PresentModeKHR, +} + +#[repr(C)] +pub(crate) struct VkDeviceGroupPresentInfoKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) swapchain_count: u32, + pub(crate) p_device_masks: *const u32, + pub(crate) mode: vk::Flags, +} + +#[repr(C)] +pub(crate) struct VkDisplayPresentInfoKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) src_rect: vk::Rect2D, + pub(crate) dst_rect: vk::Rect2D, + pub(crate) persistent: vk::Bool32, +} + +#[repr(C)] +pub(crate) struct VkFrameBoundaryEXT { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) flags: vk::Flags, + pub(crate) frame_id: u64, + pub(crate) image_count: u32, + pub(crate) p_images: *const vk::Image, + pub(crate) buffer_count: u32, + pub(crate) p_buffers: *const vk::Buffer, + pub(crate) tag_name: u64, + pub(crate) tag_size: usize, + pub(crate) p_tag: *const c_void, +} + +#[repr(C)] +pub(crate) struct VkFrameBoundaryTensorsARM { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) tensor_count: u32, + pub(crate) p_tensors: *const VkHandle, +} + +#[repr(C)] +pub(crate) struct VkPresentId2KHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) swapchain_count: u32, + pub(crate) p_present_ids: *const u64, +} + +#[repr(C)] +pub(crate) struct VkPresentIdKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) swapchain_count: u32, + pub(crate) p_present_ids: *const u64, +} + +#[repr(C)] +pub(crate) struct VkPresentRegionsKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) swapchain_count: u32, + pub(crate) p_regions: *const c_void, +} + +#[repr(C)] +pub(crate) struct VkPresentTimesInfoGOOGLE { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) swapchain_count: u32, + pub(crate) p_times: *const c_void, +} + +#[repr(C)] +pub(crate) struct VkPresentTimingsInfoEXT { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) swapchain_count: u32, + pub(crate) p_timing_infos: *const c_void, +} + +#[repr(C)] +pub(crate) struct VkSetPresentConfigNV { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) num_frames_per_batch: u32, + pub(crate) present_config_feedback: u32, +} + +#[repr(C)] +pub(crate) struct VkSwapchainPresentFenceInfoKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) swapchain_count: u32, + pub(crate) p_fences: *const vk::Fence, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkPresentModeList { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *mut c_void, + pub(crate) present_mode_count: u32, + pub(crate) p_present_modes: *mut vk::PresentModeKHR, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkDescriptorMappingSourceConstantOffsetEXT { + pub(crate) heap_offset: u32, + pub(crate) heap_array_stride: u32, + pub(crate) p_embedded_sampler: *const vk::SamplerCreateInfo<'static>, + pub(crate) sampler_heap_offset: u32, + pub(crate) sampler_heap_array_stride: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkDescriptorMappingSourcePushIndexEXT { + pub(crate) heap_offset: u32, + pub(crate) push_offset: u32, + pub(crate) heap_index_stride: u32, + pub(crate) heap_array_stride: u32, + pub(crate) p_embedded_sampler: *const vk::SamplerCreateInfo<'static>, + pub(crate) use_combined_image_sampler_index: vk::Bool32, + pub(crate) sampler_heap_offset: u32, + pub(crate) sampler_push_offset: u32, + pub(crate) sampler_heap_index_stride: u32, + pub(crate) sampler_heap_array_stride: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkDescriptorMappingSourceIndirectIndexEXT { + pub(crate) heap_offset: u32, + pub(crate) push_offset: u32, + pub(crate) address_offset: u32, + pub(crate) heap_index_stride: u32, + pub(crate) heap_array_stride: u32, + pub(crate) p_embedded_sampler: *const vk::SamplerCreateInfo<'static>, + pub(crate) use_combined_image_sampler_index: vk::Bool32, + pub(crate) sampler_heap_offset: u32, + pub(crate) sampler_push_offset: u32, + pub(crate) sampler_address_offset: u32, + pub(crate) sampler_heap_index_stride: u32, + pub(crate) sampler_heap_array_stride: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkDescriptorMappingSourceIndirectIndexArrayEXT { + pub(crate) heap_offset: u32, + pub(crate) push_offset: u32, + pub(crate) address_offset: u32, + pub(crate) heap_index_stride: u32, + pub(crate) p_embedded_sampler: *const vk::SamplerCreateInfo<'static>, + pub(crate) use_combined_image_sampler_index: vk::Bool32, + pub(crate) sampler_heap_offset: u32, + pub(crate) sampler_push_offset: u32, + pub(crate) sampler_address_offset: u32, + pub(crate) sampler_heap_index_stride: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkDescriptorMappingSourceShaderRecordIndexEXT { + pub(crate) heap_offset: u32, + pub(crate) shader_record_offset: u32, + pub(crate) heap_index_stride: u32, + pub(crate) heap_array_stride: u32, + pub(crate) p_embedded_sampler: *const vk::SamplerCreateInfo<'static>, + pub(crate) use_combined_image_sampler_index: vk::Bool32, + pub(crate) sampler_heap_offset: u32, + pub(crate) sampler_shader_record_offset: u32, + pub(crate) sampler_heap_index_stride: u32, + pub(crate) sampler_heap_array_stride: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkDescriptorMappingSourceHeapDataEXT { + pub(crate) heap_offset: u32, + pub(crate) push_offset: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkDescriptorMappingSourceIndirectAddressEXT { + pub(crate) push_offset: u32, + pub(crate) address_offset: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) union VkDescriptorMappingSourceDataEXT { + pub(crate) constant_offset: VkDescriptorMappingSourceConstantOffsetEXT, + pub(crate) push_index: VkDescriptorMappingSourcePushIndexEXT, + pub(crate) indirect_index: VkDescriptorMappingSourceIndirectIndexEXT, + pub(crate) indirect_index_array: VkDescriptorMappingSourceIndirectIndexArrayEXT, + pub(crate) heap_data: VkDescriptorMappingSourceHeapDataEXT, + pub(crate) push_data_offset: u32, + pub(crate) push_address_offset: u32, + pub(crate) indirect_address: VkDescriptorMappingSourceIndirectAddressEXT, + pub(crate) shader_record_index: VkDescriptorMappingSourceShaderRecordIndexEXT, + pub(crate) shader_record_data_offset: u32, + pub(crate) shader_record_address_offset: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkDescriptorSetAndBindingMappingEXT { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) descriptor_set: u32, + pub(crate) first_binding: u32, + pub(crate) binding_count: u32, + pub(crate) resource_mask: vk::Flags, + pub(crate) source: i32, + pub(crate) source_data: VkDescriptorMappingSourceDataEXT, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkShaderDescriptorSetAndBindingMappingInfoEXT { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) mapping_count: u32, + pub(crate) p_mappings: *const VkDescriptorSetAndBindingMappingEXT, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkShaderCreateInfoEXT { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) flags: vk::Flags, + pub(crate) stage: vk::Flags, + pub(crate) next_stage: vk::Flags, + pub(crate) code_type: i32, + pub(crate) code_size: usize, + pub(crate) p_code: *const c_void, + pub(crate) p_name: *const c_char, + pub(crate) set_layout_count: u32, + pub(crate) p_set_layouts: *const vk::DescriptorSetLayout, + pub(crate) push_constant_range_count: u32, + pub(crate) p_push_constant_ranges: *const c_void, + pub(crate) p_specialization_info: *const c_void, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkGraphicsShaderGroupCreateInfoNV { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) stage_count: u32, + pub(crate) p_stages: *const vk::PipelineShaderStageCreateInfo<'static>, + pub(crate) p_vertex_input_state: *const c_void, + pub(crate) p_tessellation_state: *const c_void, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkGraphicsPipelineShaderGroupsCreateInfoNV { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) group_count: u32, + pub(crate) p_groups: *const VkGraphicsShaderGroupCreateInfoNV, + pub(crate) pipeline_count: u32, + pub(crate) p_pipelines: *const vk::Pipeline, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkRayTracingPipelineCreateInfoKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) flags: vk::Flags, + pub(crate) stage_count: u32, + pub(crate) p_stages: *const vk::PipelineShaderStageCreateInfo<'static>, + pub(crate) group_count: u32, + pub(crate) p_groups: *const c_void, + pub(crate) max_pipeline_ray_recursion_depth: u32, + pub(crate) p_library_info: *const c_void, + pub(crate) p_library_interface: *const c_void, + pub(crate) p_dynamic_state: *const c_void, + pub(crate) layout: vk::PipelineLayout, + pub(crate) base_pipeline_handle: vk::Pipeline, + pub(crate) base_pipeline_index: i32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkRayTracingPipelineCreateInfoNV { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) flags: vk::Flags, + pub(crate) stage_count: u32, + pub(crate) p_stages: *const vk::PipelineShaderStageCreateInfo<'static>, + pub(crate) group_count: u32, + pub(crate) p_groups: *const c_void, + pub(crate) max_recursion_depth: u32, + pub(crate) layout: vk::PipelineLayout, + pub(crate) base_pipeline_handle: vk::Pipeline, + pub(crate) base_pipeline_index: i32, +} + +pub(crate) type PfnCreateShaders = unsafe extern "system" fn( + vk::Device, + u32, + *const VkShaderCreateInfoEXT, + *const vk::AllocationCallbacks<'_>, + *mut VkHandle, +) -> vk::Result; + +pub(crate) type PfnCreateRayTracingKHR = unsafe extern "system" fn( + vk::Device, + VkHandle, + vk::PipelineCache, + u32, + *const VkRayTracingPipelineCreateInfoKHR, + *const vk::AllocationCallbacks<'_>, + *mut vk::Pipeline, +) -> vk::Result; + +pub(crate) type PfnCreateRayTracingNV = unsafe extern "system" fn( + vk::Device, + vk::PipelineCache, + u32, + *const VkRayTracingPipelineCreateInfoNV, + *const vk::AllocationCallbacks<'_>, + *mut vk::Pipeline, +) -> vk::Result; + +pub(crate) type PfnPipelineIndirectMemory = unsafe extern "system" fn( + vk::Device, + *const vk::ComputePipelineCreateInfo<'_>, + *mut c_void, +); + +#[repr(C)] +pub(crate) struct VkDeviceGroupSwapchainCreateInfoKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) modes: vk::Flags, +} + +#[repr(C)] +pub(crate) struct VkImageCompressionControlEXT { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) flags: vk::Flags, + pub(crate) compression_control_plane_count: u32, + pub(crate) p_fixed_rate_flags: *mut vk::Flags, +} + +#[repr(C)] +pub(crate) struct VkImageFormatListCreateInfo { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) view_format_count: u32, + pub(crate) p_view_formats: *const vk::Format, +} + +#[repr(C)] +pub(crate) struct VkImageUsageFlags2CreateInfoKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *mut c_void, + pub(crate) usage: u64, +} + +#[repr(C)] +pub(crate) struct VkSwapchainCounterCreateInfoEXT { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) surface_counters: vk::Flags, +} + +#[repr(C)] +pub(crate) struct VkSwapchainDisplayNativeHdrCreateInfoAMD { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) local_dimming_enable: vk::Bool32, +} + +#[repr(C)] +pub(crate) struct VkSwapchainLatencyCreateInfoNV { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) latency_mode_enable: vk::Bool32, +} + +#[repr(C)] +pub(crate) struct VkSwapchainPresentBarrierCreateInfoNV { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *mut c_void, + pub(crate) present_barrier_enable: vk::Bool32, +} + +#[repr(C)] +pub(crate) struct VkSwapchainPresentScalingCreateInfoKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) scaling_behavior: vk::Flags, + pub(crate) present_gravity_x: vk::Flags, + pub(crate) present_gravity_y: vk::Flags, +} + +#[repr(C)] +pub(crate) struct VkCustomResolveCreateInfoEXT { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) custom_resolve: vk::Bool32, + pub(crate) color_attachment_count: u32, + pub(crate) p_color_attachment_formats: *const vk::Format, + pub(crate) depth_attachment_format: vk::Format, + pub(crate) stencil_attachment_format: vk::Format, +} + +#[repr(C)] +pub(crate) struct VkDebugUtilsObjectNameInfoEXT { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) object_type: i32, + pub(crate) object_handle: u64, + pub(crate) p_object_name: *const c_char, +} + +#[repr(C)] +pub(crate) struct VkPipelineRobustnessCreateInfo { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) storage_buffers: i32, + pub(crate) uniform_buffers: i32, + pub(crate) vertex_inputs: i32, + pub(crate) images: i32, +} + +#[repr(C)] +pub(crate) struct VkPipelineShaderStageModuleIdentifierCreateInfoEXT { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) identifier_size: u32, + pub(crate) p_identifier: *const u8, +} + +#[repr(C)] +pub(crate) struct VkPipelineShaderStageRequiredSubgroupSizeCreateInfo { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) required_subgroup_size: u32, +} + +#[repr(C)] +pub(crate) struct VkShaderModuleCreateInfo { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) flags: vk::Flags, + pub(crate) code_size: usize, + pub(crate) p_code: *const u32, +} + +#[repr(C)] +pub(crate) struct VkShaderModuleValidationCacheCreateInfoEXT { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) validation_cache: VkHandle, +} + +#[repr(C)] +pub(crate) struct VkValidationFeaturesEXT { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) enabled_validation_feature_count: u32, + pub(crate) p_enabled_validation_features: *const i32, + pub(crate) disabled_validation_feature_count: u32, + pub(crate) p_disabled_validation_features: *const i32, +} + +#[repr(C)] +pub(crate) struct VkAttachmentSampleCountInfoAMD { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) color_attachment_count: u32, + pub(crate) p_color_attachment_samples: *const vk::Flags, + pub(crate) depth_stencil_attachment_samples: vk::Flags, +} + +#[repr(C)] +pub(crate) struct VkGraphicsPipelineLibraryCreateInfoEXT { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) flags: vk::Flags, +} + +#[repr(C)] +pub(crate) struct VkMultiviewPerViewAttributesInfoNVX { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) per_view_attributes: vk::Bool32, + pub(crate) per_view_attributes_position_x_only: vk::Bool32, +} + +#[repr(C)] +pub(crate) struct VkPipelineBinaryInfoKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) binary_count: u32, + pub(crate) p_pipeline_binaries: *const VkHandle, +} + +#[repr(C)] +pub(crate) struct VkPipelineCompilerControlCreateInfoAMD { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) compiler_control_flags: vk::Flags, +} + +#[repr(C)] +pub(crate) struct VkPipelineCreateFlags2CreateInfo { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) flags: u64, +} + +#[repr(C)] +pub(crate) struct VkPipelineCreationFeedbackCreateInfo { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) p_pipeline_creation_feedback: *mut c_void, + pub(crate) pipeline_stage_creation_feedback_count: u32, + pub(crate) p_pipeline_stage_creation_feedbacks: *mut c_void, +} + +#[repr(C)] +pub(crate) struct VkPipelineDiscardRectangleStateCreateInfoEXT { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) flags: vk::Flags, + pub(crate) discard_rectangle_mode: i32, + pub(crate) discard_rectangle_count: u32, + pub(crate) p_discard_rectangles: *const vk::Rect2D, +} + +#[repr(C)] +pub(crate) struct VkPipelineFragmentDensityMapLayeredCreateInfoVALVE { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) max_fragment_density_map_layers: u32, +} + +#[repr(C)] +pub(crate) struct VkPipelineFragmentShadingRateEnumStateCreateInfoNV { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) shading_rate_type: i32, + pub(crate) shading_rate: i32, + pub(crate) combiner_ops: [i32; 2], +} + +#[repr(C)] +pub(crate) struct VkPipelineFragmentShadingRateStateCreateInfoKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) fragment_size: vk::Extent2D, + pub(crate) combiner_ops: [i32; 2], +} + +#[repr(C)] +pub(crate) struct VkPipelineLibraryCreateInfoKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) library_count: u32, + pub(crate) p_libraries: *const vk::Pipeline, +} + +#[repr(C)] +pub(crate) struct VkPipelineRenderingCreateInfo { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) view_mask: u32, + pub(crate) color_attachment_count: u32, + pub(crate) p_color_attachment_formats: *const vk::Format, + pub(crate) depth_attachment_format: vk::Format, + pub(crate) stencil_attachment_format: vk::Format, +} + +#[repr(C)] +pub(crate) struct VkPipelineRepresentativeFragmentTestStateCreateInfoNV { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) representative_fragment_test_enable: vk::Bool32, +} + +#[repr(C)] +pub(crate) struct VkRenderingAttachmentLocationInfo { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) color_attachment_count: u32, + pub(crate) p_color_attachment_locations: *const u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub(crate) struct VkSwapchainPresentModesCreateInfoKHR { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) present_mode_count: u32, + pub(crate) p_present_modes: *const vk::PresentModeKHR, +} + +#[repr(C)] +pub(crate) struct VkRenderingInputAttachmentIndexInfo { + pub(crate) s_type: vk::StructureType, + pub(crate) p_next: *const c_void, + pub(crate) color_attachment_count: u32, + pub(crate) p_color_attachment_input_indices: *const u32, + pub(crate) p_depth_input_attachment_index: *const u32, + pub(crate) p_stencil_input_attachment_index: *const u32, +} + +#[derive(Clone)] +pub(crate) struct VkInstState { + pub(crate) instance: ash::Instance, + pub(crate) gipa: vk::PFN_vkGetInstanceProcAddr, + pub(crate) surface_fp: ash::khr::surface::InstanceFn, + pub(crate) caps2_fp: Option, + pub(crate) groups_fp: Option, + pub(crate) groups_khr_fp: Option, + pub(crate) surface_fps: HashMap<&'static str, PfnCreateSurface>, + pub(crate) destroy_surface_fp: Option, + pub(crate) api_version: u32, + pub(crate) extensions: Arc>, +} + +pub(crate) fn provider_on(api_version: u32, extensions: &HashSet, command: &str) -> bool { + HOOK_PROVIDERS + .iter() + .filter(|(name, _)| *name == command) + .any(|(_, provider)| match provider { + Provider::Version(least) => api_version >= *least, + Provider::Ext(name) => extensions.contains(*name), + }) +} + +pub(crate) fn cstr_names(names: *const *const c_char, count: u32) -> HashSet { + (0..count as usize) + .map(|at| { + unsafe { CStr::from_ptr(*names.add(at)) } + .to_string_lossy() + .into_owned() + }) + .collect() +} + +static INSTS: RwLock>> = RwLock::new(None); +static PHYS_OWNER: RwLock>> = RwLock::new(None); +static SURFACE_TAGS: RwLock>> = RwLock::new(None); + +fn phys_owner_get(phys: u64) -> Option { + PHYS_OWNER + .read() + .ok() + .and_then(|g| g.as_ref().and_then(|m| m.get(&phys).copied())) +} + +fn call_phys_owner_put(phys: u64, inst: u64) { + match PHYS_OWNER.write() { + Ok(mut g) => { + g.get_or_insert_with(HashMap::new).insert(phys, inst); + } + Err(_) => (), + } +} + +fn call_phys_owner_forget(inst: u64) { + match PHYS_OWNER.write() { + Ok(mut g) => g + .iter_mut() + .for_each(|m| m.retain(|_, owner| *owner != inst)), + Err(_) => (), + } +} + +pub(crate) fn insts_get(h: u64) -> Option { + INSTS.read().ok().and_then(|g| g.as_ref().and_then(|m| m.get(&h).cloned())) +} + +pub(crate) fn call_insts_put(h: u64, v: VkInstState) { + match INSTS.write() { + Ok(mut g) => { + g.get_or_insert_with(HashMap::new).insert(h, v); + } + Err(_) => (), + } +} + +pub(crate) fn call_insts_del(h: u64) { + call_phys_owner_forget(h); + call_surface_tags_forget(h); + match INSTS.write() { + Ok(mut g) => { + g.get_or_insert_with(HashMap::new).remove(&h); + } + Err(_) => (), + } +} + +pub(crate) fn owning_instance(phys: vk::PhysicalDevice) -> Option<(u64, VkInstState)> { + phys_owner_get(phys.as_raw()).and_then(|h| insts_get(h).map(|st| (h, st))) +} + +fn call_surface_tag_put(inst: u64, surface: u64, tag: &'static str) { + match SURFACE_TAGS.write() { + Ok(mut g) => { + g.get_or_insert_with(HashMap::new).insert((inst, surface), tag); + } + Err(_) => (), + } +} + +fn call_surface_tag_del(inst: u64, surface: u64) { + match SURFACE_TAGS.write() { + Ok(mut g) => { + g.get_or_insert_with(HashMap::new).remove(&(inst, surface)); + } + Err(_) => (), + } +} + +fn call_surface_tags_forget(inst: u64) { + match SURFACE_TAGS.write() { + Ok(mut g) => g + .iter_mut() + .for_each(|m| m.retain(|(owner, _), _| *owner != inst)), + Err(_) => (), + } +} + +pub(crate) fn surface_tag(inst: vk::Instance, surface: vk::SurfaceKHR) -> Option<&'static str> { + SURFACE_TAGS.read().ok().and_then(|g| { + g.as_ref() + .and_then(|m| m.get(&(inst.as_raw(), surface.as_raw())).copied()) + }) +} + +fn call_tagged_result( + result: vk::Result, + inst: vk::Instance, + tag: &'static str, + out: *mut vk::SurfaceKHR, +) -> vk::Result { + match result { + vk::Result::SUCCESS => { + call_surface_tag_put(inst.as_raw(), unsafe { (*out).as_raw() }, tag); + vk::Result::SUCCESS + } + e => e, + } +} + +pub(crate) fn call_create_tagged_surface( + name: &'static str, + tag: &'static str, + inst: vk::Instance, + ci: *const c_void, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::SurfaceKHR, +) -> vk::Result { + match insts_get(inst.as_raw()).and_then(|st| st.surface_fps.get(name).copied()) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some(fp) => call_tagged_result(unsafe { fp(inst, ci, alloc, out) }, inst, tag, out), + } +} + +pub(crate) fn call_destroy_tagged_surface( + inst: vk::Instance, + surface: vk::SurfaceKHR, + alloc: *const vk::AllocationCallbacks<'_>, +) { + call_surface_tag_del(inst.as_raw(), surface.as_raw()); + match insts_get(inst.as_raw()).and_then(|st| st.destroy_surface_fp) { + Some(fp) => unsafe { fp(inst, surface, alloc) }, + None => (), + } +} + +pub(crate) fn call_all_devices(inst: &VkInstState) -> Vec { + call_owned_devices(&inst.instance) +} + +pub(crate) fn device_index(all: &[vk::PhysicalDevice], phys: vk::PhysicalDevice) -> u32 { + all.iter() + .position(|device| *device == phys) + .map(|at| at as u32 + FIRST_DEVICE) + .unwrap_or(FIRST_DEVICE) +} + +fn indexed(devices: Vec) -> Vec<(usize, vk::PhysicalDevice)> { + devices.into_iter().enumerate().collect() +} + +fn plain(pairs: Vec<(usize, vk::PhysicalDevice)>) -> Vec { + pairs.into_iter().map(|(_, device)| device).collect() +} + +fn device_position(pair: &(usize, vk::PhysicalDevice)) -> u32 { + pair.0 as u32 + FIRST_DEVICE +} + +fn call_gpu_filtered( + devices: Vec, + choice: Option, +) -> Vec { + plain(call_warned( + filtered(indexed(devices), choice, |pair| Some(device_position(pair))), + GPU_EMPTY_WARN, + )) +} + +fn group_devices(group: &VkPhysicalDeviceGroupProperties) -> Vec { + group.physical_devices[..group.physical_device_count as usize].to_vec() +} + +fn subset_of(kept_count: usize, original: vk::Bool32) -> vk::Bool32 { + match kept_count { + SINGLE_DEVICE => vk::FALSE, + _ => original, + } +} + +fn narrowed_group( + group: VkPhysicalDeviceGroupProperties, + allowed: &[vk::PhysicalDevice], +) -> VkPhysicalDeviceGroupProperties { + let held: Vec = group_devices(&group) + .into_iter() + .filter(|device| allowed.contains(device)) + .collect(); + let mut narrowed = group; + narrowed.physical_device_count = held.len() as u32; + (0..held.len()).for_each(|at| narrowed.physical_devices[at] = held[at]); + narrowed.subset_allocation = subset_of(held.len(), group.subset_allocation); + narrowed +} + +fn call_group_filtered( + groups: Vec, + allowed: Vec, + choice: Option, +) -> Vec { + match choice { + Some(_) => call_warned( + kept( + groups + .into_iter() + .map(|group| narrowed_group(group, &allowed)) + .collect(), + |group| group.physical_device_count > 0, + ), + GROUP_EMPTY_WARN, + ), + None => groups, + } +} + +fn copy_count(requested: u32, available: usize) -> usize { + (requested as usize).min(available) +} + +fn completeness(written: usize, available: usize) -> vk::Result { + match written == available { + true => vk::Result::SUCCESS, + false => vk::Result::INCOMPLETE, + } +} + +fn non_null_ci(p: *const VkLayerCreateInfo) -> Option<*const VkLayerCreateInfo> { + match p.is_null() { + true => None, + false => Some(p), + } +} + +pub(crate) fn chain_layer_info( + p_next: *const c_void, + want: u32, + function: i32, +) -> *mut VkLayerCreateInfo { + std::iter::successors(non_null_ci(p_next as *const VkLayerCreateInfo), |p| { + non_null_ci(unsafe { (**p).p_next as *const VkLayerCreateInfo }) + }) + .find(|p| unsafe { (**p).s_type.as_raw() as u32 == want && (**p).function == function }) + .map(|p| p as *mut VkLayerCreateInfo) + .unwrap_or(ptr::null_mut()) +} + +pub(crate) struct Relinked { + pub(crate) head: *const c_void, + #[allow(dead_code)] + pub(crate) blocks: Vec>, +} + +fn block_words() -> usize { + mem::size_of::().div_ceil(mem::size_of::()).max(1) +} + +fn copied(node: *const VkChainNode) -> Vec { + let mut block = vec![0u64; block_words::()]; + unsafe { ptr::write(block.as_mut_ptr() as *mut T, ptr::read(node as *const T)) }; + block +} + +pub(crate) fn copied_node(node: *const VkChainNode) -> Option> { + match chain_node_type(node) { + DEVICE_GROUP_SWAPCHAIN_TYPE => Some(copied::(node)), + IMAGE_COMPRESSION_CONTROL_TYPE => Some(copied::(node)), + IMAGE_FORMAT_LIST_TYPE => Some(copied::(node)), + IMAGE_USAGE_FLAGS_2_TYPE => Some(copied::(node)), + SWAPCHAIN_COUNTER_TYPE => Some(copied::(node)), + SWAPCHAIN_NATIVE_HDR_TYPE => Some(copied::(node)), + SWAPCHAIN_LATENCY_TYPE => Some(copied::(node)), + SWAPCHAIN_PRESENT_BARRIER_TYPE => Some(copied::(node)), + SWAPCHAIN_PRESENT_SCALING_TYPE => Some(copied::(node)), + CUSTOM_RESOLVE_TYPE => Some(copied::(node)), + DEBUG_UTILS_OBJECT_NAME_TYPE => Some(copied::(node)), + PIPELINE_ROBUSTNESS_TYPE => Some(copied::(node)), + STAGE_MODULE_IDENTIFIER_TYPE => Some(copied::(node)), + STAGE_SUBGROUP_SIZE_TYPE => Some(copied::(node)), + SHADER_MODULE_TYPE => Some(copied::(node)), + SHADER_MODULE_CACHE_TYPE => Some(copied::(node)), + VALIDATION_FEATURES_TYPE => Some(copied::(node)), + ATTACHMENT_SAMPLE_COUNT_TYPE => Some(copied::(node)), + GRAPHICS_PIPELINE_LIBRARY_TYPE => Some(copied::(node)), + MULTIVIEW_PER_VIEW_TYPE => Some(copied::(node)), + PIPELINE_BINARY_INFO_TYPE => Some(copied::(node)), + PIPELINE_COMPILER_CONTROL_TYPE => Some(copied::(node)), + PIPELINE_CREATE_FLAGS_2_TYPE => Some(copied::(node)), + PIPELINE_CREATION_FEEDBACK_TYPE => Some(copied::(node)), + PIPELINE_DISCARD_RECTANGLE_TYPE => Some(copied::(node)), + PIPELINE_DENSITY_LAYERED_TYPE => Some(copied::(node)), + PIPELINE_SHADING_RATE_ENUM_TYPE => Some(copied::(node)), + PIPELINE_SHADING_RATE_STATE_TYPE => Some(copied::(node)), + PIPELINE_LIBRARY_TYPE => Some(copied::(node)), + PIPELINE_RENDERING_TYPE => Some(copied::(node)), + PIPELINE_REPRESENTATIVE_TYPE => Some(copied::(node)), + RENDERING_ATTACHMENT_LOCATION_TYPE => Some(copied::(node)), + RENDERING_INPUT_ATTACHMENT_TYPE => Some(copied::(node)), + DEVICE_GROUP_PRESENT_INFO_TYPE => Some(copied::(node)), + DISPLAY_PRESENT_INFO_TYPE => Some(copied::(node)), + FRAME_BOUNDARY_TYPE => Some(copied::(node)), + FRAME_BOUNDARY_TENSORS_TYPE => Some(copied::(node)), + PRESENT_ID_2_TYPE => Some(copied::(node)), + PRESENT_ID_TYPE => Some(copied::(node)), + PRESENT_REGIONS_TYPE => Some(copied::(node)), + PRESENT_TIMES_GOOGLE_TYPE => Some(copied::(node)), + PRESENT_TIMINGS_TYPE => Some(copied::(node)), + SET_PRESENT_CONFIG_TYPE => Some(copied::(node)), + SWAPCHAIN_PRESENT_FENCE_TYPE => Some(copied::(node)), + _ => None, + } +} + +fn const_node(p: *const c_void) -> Option<*const VkChainNode> { + match p.is_null() { + true => None, + false => Some(p as *const VkChainNode), + } +} + +fn mut_node(p: *mut c_void) -> Option<*mut VkChainNode> { + match p.is_null() { + true => None, + false => Some(p as *mut VkChainNode), + } +} + +pub(crate) fn walked_nodes(head: *const c_void) -> Vec<*const VkChainNode> { + std::iter::successors(const_node(head), |node| { + const_node(unsafe { (**node).p_next }) + }) + .collect() +} + +pub(crate) fn filled_nodes(head: *mut c_void) -> Vec<*mut VkChainNode> { + std::iter::successors(mut_node(head), |node| mut_node(unsafe { (**node).p_next })) + .collect() +} + +fn chain_node_type(node: *const VkChainNode) -> u32 { + unsafe { (*node).s_type.as_raw() as u32 } +} + +pub(crate) fn chain_find(head: *const c_void, want: u32) -> Option<*const VkChainNode> { + walked_nodes(head) + .into_iter() + .find(|node| chain_node_type(*node) == want) +} + +fn nodes_in_front(head: *const c_void, want: u32) -> Vec<*const VkChainNode> { + walked_nodes(head) + .into_iter() + .take_while(|node| chain_node_type(*node) != want) + .collect() +} + +fn copied_nodes(front: Vec<*const VkChainNode>) -> Option>> { + front.into_iter().map(copied_node).collect() +} + +fn chained_blocks(blocks: &mut Vec>, tail: *const c_void) -> *const c_void { + (0..blocks.len()).rev().fold(tail, |next, at| { + unsafe { (*(blocks[at].as_mut_ptr() as *mut VkChainNode)).p_next = next as *mut c_void }; + blocks[at].as_ptr() as *const c_void + }) +} + +fn relinked_from(mut blocks: Vec>, replacement: *const c_void) -> Relinked { + let head = chained_blocks(&mut blocks, replacement); + Relinked { head, blocks } +} + +pub(crate) fn relinked_chain( + head: *const c_void, + target: u32, + replacement: *const c_void, +) -> Option { + copied_nodes(nodes_in_front(head, target)).map(|blocks| relinked_from(blocks, replacement)) +} + +pub(crate) fn call_relinked_chain( + head: *const c_void, + target: u32, + replacement: *const c_void, + setting: &'static str, +) -> Option { + match relinked_chain(head, target, replacement) { + Some(built) => Some(built), + None => { + call_log_at( + LogLevel::Warn, + &format!("{}: {}", setting, CHAIN_NODE_REASON), + ); + None + } + } +} + +pub(crate) fn call_loader_data_fn(node: *mut VkLayerCreateInfo) -> Option { + unsafe { node.as_ref() } + .map(|info| info.u_layer_info) + .filter(|link| !link.is_null()) + .map(|link| unsafe { mem::transmute(link) }) +} + +fn call_advanced_link(link: *mut VkLayerCreateInfo) -> VkLayerLinkInfo { + let li = unsafe { (*link).u_layer_info }; + let out = VkLayerLinkInfo { + pfn_next_get_instance_proc_addr: unsafe { (*li).pfn_next_get_instance_proc_addr }, + pfn_next_get_device_proc_addr: unsafe { (*li).pfn_next_get_device_proc_addr }, + }; + unsafe { (*link).u_layer_info = (*li).p_next }; + out +} + +pub(crate) fn call_advance_chain(link: *mut VkLayerCreateInfo) -> Option { + match link.is_null() || unsafe { (*link).u_layer_info.is_null() } { + true => None, + false => Some(call_advanced_link(link)), + } +} + +pub(crate) fn call_next_gipa(gipa: vk::PFN_vkGetInstanceProcAddr, inst: vk::Instance, name: &str) -> vk::PFN_vkVoidFunction { + let c = CString::new(name).unwrap_or_default(); + unsafe { gipa(inst, c.as_ptr()) } +} + +pub(crate) fn call_next_gdpa(gdpa: vk::PFN_vkGetDeviceProcAddr, dev: vk::Device, name: &str) -> vk::PFN_vkVoidFunction { + let c = CString::new(name).unwrap_or_default(); + unsafe { gdpa(dev, c.as_ptr()) } +} + +fn call_write_count(list: &[T], count: *mut u32) -> vk::Result { + unsafe { *count = list.len() as u32 }; + vk::Result::SUCCESS +} + +fn call_write_items(list: &[T], count: *mut u32, out: *mut T) -> vk::Result { + let n = copy_count(unsafe { *count }, list.len()); + (0..n).for_each(|i| unsafe { *out.add(i) = list[i] }); + unsafe { *count = n as u32 }; + completeness(n, list.len()) +} + +pub(crate) fn call_write_list(list: &[T], count: *mut u32, out: *mut T) -> vk::Result { + match out.is_null() { + true => call_write_count(list, count), + false => call_write_items(list, count, out), + } +} + +fn call_enumerate_through( + st: &VkInstState, + count: *mut u32, + devices: *mut vk::PhysicalDevice, +) -> vk::Result { + match unsafe { st.instance.enumerate_physical_devices() } { + Ok(all) => call_write_list(&call_gpu_filtered(all, ensure_settings().gpu), count, devices), + Err(e) => e, + } +} + +pub(crate) fn call_filtered_enumerate( + inst: vk::Instance, + count: *mut u32, + devices: *mut vk::PhysicalDevice, +) -> vk::Result { + match insts_get(inst.as_raw()) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some(st) => call_enumerate_through(&st, count, devices), + } +} + +fn empty_group() -> VkPhysicalDeviceGroupProperties { + VkPhysicalDeviceGroupProperties { + s_type: vk::StructureType::from_raw(DEVICE_GROUP_PROPERTIES_TYPE as i32), + p_next: ptr::null_mut(), + physical_device_count: 0, + physical_devices: [vk::PhysicalDevice::null(); DEVICE_GROUP_SIZE], + subset_allocation: vk::FALSE, + } +} + +fn call_query_groups( + handle: vk::Instance, + fp: PfnDeviceGroups, +) -> Vec { + let mut n: u32 = 0; + let r1 = unsafe { fp(handle, &mut n, ptr::null_mut()) }; + let mut v: Vec = + (0..n).map(|_| empty_group()).collect(); + let r2 = unsafe { fp(handle, &mut n, v.as_mut_ptr()) }; + match (r1, r2) { + (vk::Result::SUCCESS, vk::Result::SUCCESS) => v.into_iter().take(n as usize).collect(), + (vk::Result::SUCCESS, vk::Result::INCOMPLETE) => call_query_groups(handle, fp), + (_, _) => Vec::new(), + } +} + +fn call_allowed_devices(st: &VkInstState) -> Vec { + call_gpu_filtered(call_all_devices(st), ensure_settings().gpu) +} + +fn call_groups_through( + st: &VkInstState, + handle: vk::Instance, + fp: PfnDeviceGroups, + count: *mut u32, + groups: *mut VkPhysicalDeviceGroupProperties, +) -> vk::Result { + call_write_list( + &call_group_filtered( + call_query_groups(handle, fp), + call_allowed_devices(st), + ensure_settings().gpu, + ), + count, + groups, + ) +} + +fn call_groups_with( + inst: vk::Instance, + found: Option<(VkInstState, PfnDeviceGroups)>, + count: *mut u32, + groups: *mut VkPhysicalDeviceGroupProperties, +) -> vk::Result { + match found { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some((st, fp)) => call_groups_through(&st, inst, fp, count, groups), + } +} + +pub(crate) fn call_filtered_groups( + inst: vk::Instance, + count: *mut u32, + groups: *mut VkPhysicalDeviceGroupProperties, +) -> vk::Result { + call_groups_with( + inst, + insts_get(inst.as_raw()).and_then(|st| st.groups_fp.map(|fp| (st, fp))), + count, + groups, + ) +} + +pub(crate) fn call_filtered_groups_khr( + inst: vk::Instance, + count: *mut u32, + groups: *mut VkPhysicalDeviceGroupProperties, +) -> vk::Result { + call_groups_with( + inst, + insts_get(inst.as_raw()).and_then(|st| st.groups_khr_fp.map(|fp| (st, fp))), + count, + groups, + ) +} + +fn load_surface_fp( + gipa: vk::PFN_vkGetInstanceProcAddr, + handle: vk::Instance, +) -> ash::khr::surface::InstanceFn { + ash::khr::surface::InstanceFn::load(|name| unsafe { + mem::transmute(gipa(handle, name.as_ptr())) + }) +} + +fn call_typed_instance_fp( + gipa: vk::PFN_vkGetInstanceProcAddr, + handle: vk::Instance, + name: &str, +) -> Option { + call_next_gipa(gipa, handle, name).map(|f| unsafe { mem::transmute_copy(&f) }) +} + +fn call_owned_devices(instance: &ash::Instance) -> Vec { + unsafe { instance.enumerate_physical_devices() }.unwrap_or_default() +} + +fn call_probe_first_device(st: &VkInstState, phys: vk::PhysicalDevice) { + let props = unsafe { st.instance.get_physical_device_properties(phys) }; + call_record_device(call_build_device(st, phys, &limit_caps(&props))); +} + +fn call_probe_devices(st: &VkInstState) { + match (env_probe_active(), call_allowed_devices(st).first().copied()) { + (true, Some(phys)) => call_probe_first_device(st, phys), + (_, _) => (), + } +} + +fn call_remember_owner(handle: vk::Instance, devices: Vec) { + devices + .into_iter() + .for_each(|phys| call_phys_owner_put(phys.as_raw(), handle.as_raw())); +} + +fn call_surface_creators( + gipa: vk::PFN_vkGetInstanceProcAddr, + handle: vk::Instance, +) -> HashMap<&'static str, PfnCreateSurface> { + SURFACE_CREATORS + .iter() + .filter_map(|(name, _)| { + call_typed_instance_fp::(gipa, handle, name).map(|fp| (*name, fp)) + }) + .collect() +} + +fn requested_api_version(ci: *const vk::InstanceCreateInfo<'_>) -> u32 { + match unsafe { (*ci).p_application_info.as_ref() } { + None => vk::API_VERSION_1_0, + Some(info) => match info.api_version { + UNSET_API_VERSION => vk::API_VERSION_1_0, + asked => asked, + }, + } +} + +fn requested_instance_extensions(ci: *const vk::InstanceCreateInfo<'_>) -> HashSet { + cstr_names(unsafe { (*ci).pp_enabled_extension_names }, unsafe { + (*ci).enabled_extension_count + }) +} + +fn call_register_instance( + gipa: vk::PFN_vkGetInstanceProcAddr, + handle: vk::Instance, + api_version: u32, + extensions: HashSet, +) { + let static_fn = ash::StaticFn { get_instance_proc_addr: gipa }; + let instance = unsafe { ash::Instance::load(&static_fn, handle) }; + let devices = call_owned_devices(&instance); + call_remember_owner(handle, devices); + let state = VkInstState { + instance, + gipa, + surface_fp: load_surface_fp(gipa, handle), + caps2_fp: call_typed_instance_fp(gipa, handle, FN_SURFACE_CAPS_2), + groups_fp: call_typed_instance_fp(gipa, handle, FN_DEVICE_GROUPS), + groups_khr_fp: call_typed_instance_fp(gipa, handle, FN_DEVICE_GROUPS_KHR), + surface_fps: call_surface_creators(gipa, handle), + destroy_surface_fp: call_typed_instance_fp(gipa, handle, FN_DESTROY_SURFACE), + api_version, + extensions: Arc::new(extensions), + }; + call_probe_devices(&state); + call_insts_put(handle.as_raw(), state); + call_log_at(LogLevel::Info, LOG_INSTANCE_REGISTERED); +} + +fn call_invoke_create_instance( + create_fn: unsafe extern "system" fn(), + gipa: vk::PFN_vkGetInstanceProcAddr, + ci: *const vk::InstanceCreateInfo<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::Instance, +) -> vk::Result { + let cf: vk::PFN_vkCreateInstance = unsafe { mem::transmute(create_fn) }; + match unsafe { cf(ci, alloc, out) } { + vk::Result::SUCCESS => { + call_register_instance( + gipa, + unsafe { *out }, + requested_api_version(ci), + requested_instance_extensions(ci), + ); + vk::Result::SUCCESS + } + e => e, + } +} + +pub(crate) fn call_real_create_instance( + link: Option, + ci: *const vk::InstanceCreateInfo<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::Instance, +) -> vk::Result { + match link { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some(l) => call_next_gipa(l.pfn_next_get_instance_proc_addr, vk::Instance::null(), FN_CREATE_INSTANCE) + .map(|f| call_invoke_create_instance(f, l.pfn_next_get_instance_proc_addr, ci, alloc, out)) + .unwrap_or(vk::Result::ERROR_INITIALIZATION_FAILED), + } +} diff --git a/volt/launcher.rs b/src/volt/launcher.rs similarity index 63% rename from volt/launcher.rs rename to src/volt/launcher.rs index 6c048f0..bd69626 100644 --- a/volt/launcher.rs +++ b/src/volt/launcher.rs @@ -5,32 +5,49 @@ use std::process::Command; use crate::config::config_dir; use crate::config::config_path; -use crate::config::sanitize_name; +use crate::config::home_dir; +use crate::config::call_sanitize_name; use crate::consts::DEFAULT_CONFIG; use crate::consts::DEFAULT_PROFILE; use crate::consts::ENABLE_VALUE; use crate::consts::ENV_CONFIG_NAME; use crate::consts::ENV_ENABLE; +use crate::consts::ENV_LIB_PATH; use crate::consts::EXIT_EXEC_FAILED; use crate::consts::EXIT_OK; use crate::consts::EXIT_USAGE; use crate::consts::FLATPAK_CMD; +use crate::consts::FLATPAK_CONFIG_RO; +use crate::consts::FLATPAK_CONFIG_RW; use crate::consts::FLATPAK_INJECT; use crate::consts::ENV_PROBE; +use crate::consts::EXEC_FAILED; use crate::consts::FLATPAK_RUN; +use crate::consts::FLATPAK_SUFFIX; +use crate::consts::FLAG_HELP_LONG; +use crate::consts::FLAG_HELP_SHORT; +use crate::consts::FLAG_SEPARATOR; +use crate::consts::LOG_NO_APP_ID; +use crate::consts::LIB_DIR_32; +use crate::consts::LIB_DIR_64; +use crate::consts::PATH_SEP; use crate::consts::PROBE_FLAG; use crate::consts::PROBE_UNSET; use crate::consts::USAGE; -use crate::logging::init_log_level; -use crate::logging::log_at; +use crate::consts::USER_LIB_REL; +use crate::env::env_lib_path; +use crate::logging::call_init_log_level; +use crate::logging::call_log_at; use crate::logging::LogLevel; fn is_help_flag(a: &str) -> bool { - a == "--help" || a == "-h" + a == FLAG_HELP_LONG || a == FLAG_HELP_SHORT } fn wants_help(args: &[String]) -> bool { - args.iter().take_while(|a| **a != "--").any(|a| is_help_flag(a)) + args.iter() + .take_while(|a| **a != FLAG_SEPARATOR) + .any(|a| is_help_flag(a)) } fn is_probe_flag(a: &str) -> bool { @@ -46,7 +63,7 @@ fn is_flag(a: &str) -> bool { } fn head_profile(head: &[String]) -> String { - sanitize_name( + call_sanitize_name( head.iter() .find(|a| !is_flag(a)) .map(String::as_str) @@ -62,7 +79,10 @@ fn probe_value(probe: bool) -> &'static str { } fn split_args(args: &[String]) -> (Vec, Vec) { - let pos = args.iter().position(|a| a == "--").unwrap_or(args.len()); + let pos = args + .iter() + .position(|a| a == FLAG_SEPARATOR) + .unwrap_or(args.len()); ( args[..pos].to_vec(), args.get(pos + 1..).unwrap_or(&[]).to_vec(), @@ -70,7 +90,7 @@ fn split_args(args: &[String]) -> (Vec, Vec) { } fn is_flatpak_bin(name: &str) -> bool { - name == FLATPAK_CMD || name.ends_with("/flatpak") + name == FLATPAK_CMD || name.ends_with(FLATPAK_SUFFIX) } fn is_flatpak_run(cmd: &[String]) -> bool { @@ -113,6 +133,13 @@ fn flatpak_trailing(cmd: &[String]) -> Vec { .unwrap_or_default() } +fn config_share(probe: bool) -> &'static str { + match probe { + true => FLATPAK_CONFIG_RW, + false => FLATPAK_CONFIG_RO, + } +} + fn build_flatpak_args( profile: &str, app_id: &str, @@ -126,6 +153,7 @@ fn build_flatpak_args( format!("--command={}", FLATPAK_INJECT), format!("--env={}={}", ENV_CONFIG_NAME, profile), format!("--env={}={}", ENV_PROBE, probe_value(probe)), + config_share(probe).to_string(), ], flags.to_vec(), vec![app_id.to_string()], @@ -134,7 +162,7 @@ fn build_flatpak_args( .concat() } -fn write_default_config(path: &PathBuf) { +fn call_write_default_config(path: &PathBuf) { let _ = fs::create_dir_all(config_dir()); match path.exists() { true => (), @@ -144,21 +172,41 @@ fn write_default_config(path: &PathBuf) { } } -fn exec_native(cmd: &[String], profile: &str, probe: bool) -> i32 { +fn lib_dir(arch: &str) -> String { + home_dir() + .join(USER_LIB_REL) + .join(arch) + .to_string_lossy() + .into_owned() +} + +fn lib_dirs() -> String { + [lib_dir(LIB_DIR_64), lib_dir(LIB_DIR_32)].join(PATH_SEP) +} + +fn lib_path(existing: Option) -> String { + match existing { + Some(current) => [current, lib_dirs()].join(PATH_SEP), + None => lib_dirs(), + } +} + +fn call_exec_native(cmd: &[String], profile: &str, probe: bool) -> i32 { let err = Command::new(&cmd[0]) .args(&cmd[1..]) .env(ENV_ENABLE, ENABLE_VALUE) .env(ENV_CONFIG_NAME, profile) .env(ENV_PROBE, probe_value(probe)) + .env(ENV_LIB_PATH, lib_path(env_lib_path())) .exec(); - log_at(LogLevel::Error, &format!("exec failed: {}", err)); + call_log_at(LogLevel::Error, &[EXEC_FAILED, err.to_string().as_str()].concat()); EXIT_EXEC_FAILED } -fn exec_flatpak(cmd: &[String], profile: &str, probe: bool) -> i32 { +fn call_exec_flatpak(cmd: &[String], profile: &str, probe: bool) -> i32 { match flatpak_app_id(cmd) { None => { - log_at(LogLevel::Error, "flatpak run: no app id found"); + call_log_at(LogLevel::Error, LOG_NO_APP_ID); EXIT_USAGE } Some(app_id) => { @@ -170,39 +218,39 @@ fn exec_flatpak(cmd: &[String], profile: &str, probe: bool) -> i32 { &flatpak_trailing(cmd), ); let err = Command::new(FLATPAK_CMD).args(&args).exec(); - log_at(LogLevel::Error, &format!("exec failed: {}", err)); + call_log_at(LogLevel::Error, &[EXEC_FAILED, err.to_string().as_str()].concat()); EXIT_EXEC_FAILED } } } -fn launch_cmd(head: &[String], cmd: &[String]) -> i32 { +fn call_launch_cmd(head: &[String], cmd: &[String]) -> i32 { let profile = head_profile(head); - write_default_config(&config_path(&profile)); + call_write_default_config(&config_path(&profile)); match is_flatpak_run(cmd) { - true => exec_flatpak(cmd, &profile, wants_probe(head)), - false => exec_native(cmd, &profile, wants_probe(head)), + true => call_exec_flatpak(cmd, &profile, wants_probe(head)), + false => call_exec_native(cmd, &profile, wants_probe(head)), } } -fn launch(args: Vec) -> i32 { +fn call_launch(args: Vec) -> i32 { let (head, cmd) = split_args(&args); match cmd.is_empty() { true => { print!("{}", USAGE); EXIT_USAGE } - false => launch_cmd(&head, &cmd), + false => call_launch_cmd(&head, &cmd), } } -pub fn run_launcher(args: Vec) -> i32 { - init_log_level(); +pub fn call_run_launcher(args: Vec) -> i32 { + call_init_log_level(); match wants_help(&args) { true => { print!("{}", USAGE); EXIT_OK } - false => launch(args), + false => call_launch(args), } } diff --git a/src/volt/layer.rs b/src/volt/layer.rs new file mode 100644 index 0000000..d164c74 --- /dev/null +++ b/src/volt/layer.rs @@ -0,0 +1,871 @@ +use std::ffi::c_char; +use std::ffi::c_void; +use std::ffi::CStr; +use std::mem; +use std::ptr; +use std::sync::Arc; + +use ash::vk; +use ash::vk::Handle; + +use crate::config::ensure_settings; +use crate::config::Settings; +use crate::consts::FN_ALLOCATE_COMMAND_BUFFERS; +use crate::consts::FN_CREATE_COMPUTE_PIPELINES; +use crate::consts::FN_CREATE_DEVICE; +use crate::consts::FN_CREATE_GRAPHICS_PIPELINES; +use crate::consts::FN_CREATE_INSTANCE; +use crate::consts::FN_CREATE_PIPELINE_BINARIES; +use crate::consts::FN_CREATE_SAMPLER; +use crate::consts::FN_DESTROY_COMMAND_POOL; +use crate::consts::FN_DESTROY_DEVICE; +use crate::consts::FN_DESTROY_INSTANCE; +use crate::consts::FN_DEVICE_QUEUE; +use crate::consts::FN_ENUMERATE_DEVICES; +use crate::consts::FN_FREE_COMMAND_BUFFERS; +use crate::consts::FN_GET_DEVICE_PROC_ADDR; +use crate::consts::FN_GET_INSTANCE_PROC_ADDR; +use crate::consts::LOG_QUEUE_2_UNREGISTERED; +use crate::consts::LOG_QUEUE_UNREGISTERED; +use crate::consts::FN_GET_PIPELINE_KEY; +use crate::consts::FN_CREATE_RAY_TRACING_KHR; +use crate::consts::FN_CREATE_RAY_TRACING_NV; +use crate::consts::FN_CREATE_SHADERS; +use crate::consts::FN_CREATE_SWAPCHAIN; +use crate::consts::FN_CREATE_WAYLAND_SURFACE; +use crate::consts::FN_PIPELINE_INDIRECT_MEMORY; +use crate::consts::FN_CREATE_XCB_SURFACE; +use crate::consts::FN_CREATE_XLIB_SURFACE; +use crate::consts::FN_DESTROY_SURFACE; +use crate::consts::FN_DESTROY_SWAPCHAIN; +use crate::consts::FN_DEVICE_GROUPS; +use crate::consts::FN_DEVICE_GROUPS_KHR; +use crate::consts::FN_DEVICE_QUEUE_2; +use crate::consts::FN_QUEUE_PRESENT; +use crate::consts::FN_SET_ALPHA_COVERAGE; +use crate::consts::FN_SET_ALPHA_ONE; +use crate::consts::FN_SET_DEPTH_CLAMP; +use crate::consts::FN_SHARED_SWAPCHAINS; +use crate::consts::FN_SURFACE_CAPS; +use crate::consts::FN_SURFACE_CAPS_2; +use crate::consts::FN_SURFACE_PRESENT_MODES; +use crate::consts::FN_WRITE_SAMPLERS; +use crate::consts::CORE_10_DEVICE_HOOKS; +use crate::consts::LAYER_DATA_CALLBACK; +use crate::consts::LAYER_IFACE_VERSION; +use crate::consts::LAYER_LINK_INFO; +use crate::consts::LAYER_NEGOTIATE_INTERFACE_STRUCT; +use crate::consts::LOADER_DEVICE_CREATE_INFO_TYPE; +use crate::consts::LOADER_INSTANCE_CREATE_INFO_TYPE; +use crate::consts::LimitStage; +use crate::consts::NULL_OK; +use crate::consts::TAG_WAYLAND; +use crate::consts::TAG_XCB; +use crate::consts::UNOWNED_QUEUE_ERROR; +use crate::device::call_allocate_command_buffers; +use crate::device::call_destroy_command_pool; +use crate::device::call_free_command_buffers; +use crate::device::call_real_create_device; +use crate::device::call_register_queue; +use crate::device::cmdbuf_owner; +use crate::device::device_hook_on; +use crate::device::call_devs_del; +use crate::device::devs_gdpa; +use crate::device::devs_get; +use crate::device::call_queue_dev_put; +use crate::device::queue_owner; +use crate::device::VkDevState; +use crate::instance::call_advance_chain; +use crate::instance::call_create_tagged_surface; +use crate::instance::call_destroy_tagged_surface; +use crate::instance::call_filtered_enumerate; +use crate::instance::call_filtered_groups; +use crate::instance::call_filtered_groups_khr; +use crate::instance::call_loader_data_fn; +use crate::instance::call_next_gdpa; +use crate::instance::call_next_gipa; +use crate::instance::call_real_create_instance; +use crate::instance::chain_layer_info; +use crate::instance::call_insts_del; +use crate::instance::insts_get; +use crate::instance::provider_on; +use crate::instance::VkHandle; +use crate::instance::VkPhysicalDeviceGroupProperties; +use crate::instance::VkPhysicalDeviceSurfaceInfo2; +use crate::instance::VkPipelineBinaryCreateInfoKHR; +use crate::instance::VkPipelineCreateInfoKHR; +use crate::instance::VkRayTracingPipelineCreateInfoKHR; +use crate::instance::VkRayTracingPipelineCreateInfoNV; +use crate::instance::VkShaderCreateInfoEXT; +use crate::instance::VkSurfaceCapabilities2; +use crate::logging::call_init_log_level; +use crate::logging::call_log_at; +use crate::logging::LogLevel; +use crate::pipeline::call_create_compute_pipelines; +use crate::pipeline::call_create_pipeline_binaries; +use crate::pipeline::call_create_graphics_pipelines; +use crate::pipeline::call_create_ray_tracing_khr; +use crate::pipeline::call_create_ray_tracing_nv; +use crate::pipeline::call_create_shaders; +use crate::pipeline::call_get_pipeline_key; +use crate::pipeline::call_pipeline_indirect_memory; +use crate::pipeline::call_set_alpha_coverage; +use crate::pipeline::call_set_alpha_one; +use crate::pipeline::call_set_depth_clamp; +use crate::present::call_forget_timeline; +use crate::present::call_present_frame; +use crate::present::call_limit_frame; +use crate::sampler::call_create_sampler; +use crate::sampler::call_write_sampler_descriptors; +use crate::swapchain::call_create_shared_swapchains; +use crate::swapchain::call_forget_forced_mode; +use crate::swapchain::call_create_swapchain; +use crate::swapchain::call_surface_capabilities; +use crate::swapchain::call_surface_capabilities2; +use crate::swapchain::call_surface_present_modes; + +pub(crate) fn negotiated_version(offered: u32, wanted: u32) -> Option { + match offered < wanted { + true => None, + false => Some(wanted), + } +} + +#[repr(C)] +struct VkNegotiateLayerInterface { + s_type: i32, + p_next: *mut c_void, + loader_layer_interface_version: u32, + pfn_get_instance_proc_addr: Option, + pfn_get_device_proc_addr: Option, + pfn_get_physical_device_proc_addr: Option, +} + +fn cstr_to_str<'a>(p: *const c_char) -> &'a str { + match p.is_null() { + true => "", + false => unsafe { CStr::from_ptr(p).to_str().unwrap_or("") }, + } +} + +fn null_ok_name(name: &str) -> bool { + NULL_OK.contains(&name) +} + +fn instance_symbol(name: &str) -> Option<*mut c_void> { + match name { + FN_GET_INSTANCE_PROC_ADDR => Some(vkGetInstanceProcAddr as *mut c_void), + FN_GET_DEVICE_PROC_ADDR => Some(vkGetDeviceProcAddr as *mut c_void), + FN_CREATE_INSTANCE => Some(vkCreateInstance as *mut c_void), + FN_DESTROY_INSTANCE => Some(vkDestroyInstance as *mut c_void), + FN_CREATE_DEVICE => Some(vkCreateDevice as *mut c_void), + FN_ENUMERATE_DEVICES => Some(vkEnumeratePhysicalDevices as *mut c_void), + _ => None, + } +} + +fn instance_surface_symbol(name: &str) -> Option<*mut c_void> { + match name { + FN_SURFACE_PRESENT_MODES => Some(vkGetPhysicalDeviceSurfacePresentModesKHR as *mut c_void), + FN_SURFACE_CAPS => Some(vkGetPhysicalDeviceSurfaceCapabilitiesKHR as *mut c_void), + _ => None, + } +} + +fn instance_resolves(inst: vk::Instance, name: &str) -> bool { + insts_get(inst.as_raw()) + .and_then(|st| call_next_gipa(st.gipa, inst, name)) + .is_some() +} + +fn instance_gated_surface(inst: vk::Instance, name: &str) -> Option<*mut c_void> { + instance_surface_symbol(name).filter(|_| { + insts_get(inst.as_raw()) + .map(|st| provider_on(st.api_version, &st.extensions, name)) + .unwrap_or(false) + && instance_resolves(inst, name) + }) +} + +fn device_symbol(name: &str) -> Option<*mut c_void> { + match name { + FN_GET_DEVICE_PROC_ADDR => Some(vkGetDeviceProcAddr as *mut c_void), + FN_DESTROY_DEVICE => Some(vkDestroyDevice as *mut c_void), + FN_CREATE_GRAPHICS_PIPELINES => Some(vkCreateGraphicsPipelines as *mut c_void), + FN_CREATE_COMPUTE_PIPELINES => Some(vkCreateComputePipelines as *mut c_void), + FN_CREATE_SAMPLER => Some(vkCreateSampler as *mut c_void), + FN_ALLOCATE_COMMAND_BUFFERS => Some(vkAllocateCommandBuffers as *mut c_void), + FN_FREE_COMMAND_BUFFERS => Some(vkFreeCommandBuffers as *mut c_void), + FN_DESTROY_COMMAND_POOL => Some(vkDestroyCommandPool as *mut c_void), + FN_DEVICE_QUEUE => Some(volt_GetDeviceQueue as *mut c_void), + _ => None, + } +} + +fn device_provided_symbol(name: &str) -> Option<*mut c_void> { + match name { + FN_DEVICE_QUEUE_2 => Some(volt_GetDeviceQueue2 as *mut c_void), + FN_CREATE_SWAPCHAIN => Some(vkCreateSwapchainKHR as *mut c_void), + FN_DESTROY_SWAPCHAIN => Some(vkDestroySwapchainKHR as *mut c_void), + FN_QUEUE_PRESENT => Some(vkQueuePresentKHR as *mut c_void), + _ => None, + } +} + +fn device_resolves(dev: vk::Device, name: &str) -> bool { + devs_gdpa(dev.as_raw()) + .and_then(|gdpa| call_next_gdpa(gdpa, dev, name)) + .is_some() +} + +fn device_provided_hooked(dev: vk::Device, name: &str) -> Option<*mut c_void> { + device_provided_symbol(name).filter(|_| { + devs_get(dev.as_raw()) + .map(|d| device_hook_on(&d, name)) + .unwrap_or(false) + && device_resolves(dev, name) + }) +} + +fn instance_extension_hook(name: &str) -> Option<*mut c_void> { + match name { + FN_SURFACE_CAPS_2 => Some(vkGetPhysicalDeviceSurfaceCapabilities2KHR as *mut c_void), + FN_DEVICE_GROUPS => Some(vkEnumeratePhysicalDeviceGroups as *mut c_void), + FN_DEVICE_GROUPS_KHR => Some(vkEnumeratePhysicalDeviceGroupsKHR as *mut c_void), + FN_CREATE_XCB_SURFACE => Some(vkCreateXcbSurfaceKHR as *mut c_void), + FN_CREATE_XLIB_SURFACE => Some(vkCreateXlibSurfaceKHR as *mut c_void), + FN_CREATE_WAYLAND_SURFACE => Some(vkCreateWaylandSurfaceKHR as *mut c_void), + FN_DESTROY_SURFACE => Some(vkDestroySurfaceKHR as *mut c_void), + _ => None, + } +} + +fn instance_pointer_present(st: &crate::instance::VkInstState, name: &str) -> bool { + match name { + FN_SURFACE_CAPS_2 => st.caps2_fp.is_some(), + FN_DEVICE_GROUPS => st.groups_fp.is_some(), + FN_DEVICE_GROUPS_KHR => st.groups_khr_fp.is_some(), + FN_DESTROY_SURFACE => st.destroy_surface_fp.is_some(), + other => st.surface_fps.contains_key(other), + } +} + +fn instance_fp_present(inst: vk::Instance, name: &str) -> bool { + match insts_get(inst.as_raw()) { + Some(st) => { + provider_on(st.api_version, &st.extensions, name) + && instance_pointer_present(&st, name) + } + None => false, + } +} + +fn instance_hooked_symbol(inst: vk::Instance, name: &str) -> Option<*mut c_void> { + instance_extension_hook(name).filter(|_| instance_fp_present(inst, name)) +} + +fn device_extension_hook(name: &str) -> Option<*mut c_void> { + match name { + FN_GET_PIPELINE_KEY => Some(vkGetPipelineKeyKHR as *mut c_void), + FN_CREATE_PIPELINE_BINARIES => Some(vkCreatePipelineBinariesKHR as *mut c_void), + FN_SHARED_SWAPCHAINS => Some(vkCreateSharedSwapchainsKHR as *mut c_void), + FN_WRITE_SAMPLERS => Some(vkWriteSamplerDescriptorsEXT as *mut c_void), + FN_CREATE_SHADERS => Some(vkCreateShadersEXT as *mut c_void), + FN_CREATE_RAY_TRACING_KHR => Some(vkCreateRayTracingPipelinesKHR as *mut c_void), + FN_CREATE_RAY_TRACING_NV => Some(vkCreateRayTracingPipelinesNV as *mut c_void), + FN_PIPELINE_INDIRECT_MEMORY => Some(vkGetPipelineIndirectMemoryRequirementsNV as *mut c_void), + FN_SET_ALPHA_COVERAGE => Some(vkCmdSetAlphaToCoverageEnableEXT as *mut c_void), + FN_SET_ALPHA_ONE => Some(vkCmdSetAlphaToOneEnableEXT as *mut c_void), + FN_SET_DEPTH_CLAMP => Some(vkCmdSetDepthClampEnableEXT as *mut c_void), + _ => None, + } +} + +fn device_pointer_present(d: &VkDevState, name: &str) -> bool { + match name { + FN_GET_PIPELINE_KEY => d.pipeline_key_fp.is_some(), + FN_CREATE_PIPELINE_BINARIES => d.pipeline_binaries_fp.is_some(), + FN_SHARED_SWAPCHAINS => d.shared_fp.is_some(), + FN_WRITE_SAMPLERS => d.samplers_fp.is_some(), + FN_CREATE_SHADERS => d.shaders_fp.is_some(), + FN_CREATE_RAY_TRACING_KHR => d.ray_khr_fp.is_some(), + FN_CREATE_RAY_TRACING_NV => d.ray_nv_fp.is_some(), + FN_PIPELINE_INDIRECT_MEMORY => d.indirect_memory_fp.is_some(), + FN_SET_ALPHA_COVERAGE => d.alpha_fp.is_some(), + FN_SET_ALPHA_ONE => d.alpha_one_fp.is_some(), + FN_SET_DEPTH_CLAMP => d.clamp_fp.is_some(), + _ => false, + } +} + +fn device_fp_present(dev: vk::Device, name: &str) -> bool { + match devs_get(dev.as_raw()) { + Some(d) => device_hook_on(&d, name) && device_pointer_present(&d, name), + None => false, + } +} + +fn device_hooked_symbol(dev: vk::Device, name: &str) -> Option<*mut c_void> { + device_extension_hook(name).filter(|_| device_fp_present(dev, name)) +} + +fn device_core_symbol(name: &str) -> Option<*mut c_void> { + device_symbol(name).filter(|_| CORE_10_DEVICE_HOOKS.contains(&name)) +} + +fn null_ok_ptr(name: &str) -> *mut c_void { + match name { + FN_GET_INSTANCE_PROC_ADDR => vkGetInstanceProcAddr as *mut c_void, + FN_CREATE_INSTANCE => vkCreateInstance as *mut c_void, + _ => ptr::null_mut(), + } +} + +fn forward_device_proc(dev: vk::Device, name: &str) -> vk::PFN_vkVoidFunction { + match devs_gdpa(dev.as_raw()) { + Some(gdpa) => call_next_gdpa(gdpa, dev, name), + None => None, + } +} + +fn forward_instance_proc(inst: vk::Instance, name: &str) -> vk::PFN_vkVoidFunction { + match insts_get(inst.as_raw()) { + Some(st) => call_next_gipa(st.gipa, inst, name), + None => None, + } +} + +fn instance_path_symbol(inst: vk::Instance, name: &str) -> Option<*mut c_void> { + match ( + instance_symbol(name), + device_core_symbol(name), + instance_gated_surface(inst, name), + instance_hooked_symbol(inst, name), + ) { + (Some(p), _, _, _) => Some(p), + (None, Some(p), _, _) => Some(p), + (None, None, Some(p), _) => Some(p), + (None, None, None, found) => found, + } +} + +fn resolve_instance_proc(inst: vk::Instance, name: &str) -> vk::PFN_vkVoidFunction { + match instance_path_symbol(inst, name) { + Some(p) => unsafe { mem::transmute(p) }, + None => forward_instance_proc(inst, name), + } +} + +fn resolve_null_instance_proc(name: &str) -> vk::PFN_vkVoidFunction { + match null_ok_name(name) { + true => unsafe { mem::transmute(null_ok_ptr(name)) }, + false => None, + } +} + +fn call_destroy_through( + d: unsafe extern "system" fn(), + inst: vk::Instance, + alloc: *const vk::AllocationCallbacks<'_>, +) { + let df: vk::PFN_vkDestroyInstance = unsafe { mem::transmute(d) }; + unsafe { df(inst, alloc) }; +} + +fn call_chain_destroy_instance(gipa: vk::PFN_vkGetInstanceProcAddr, inst: vk::Instance, alloc: *const vk::AllocationCallbacks<'_>) { + match call_next_gipa(gipa, inst, FN_DESTROY_INSTANCE) { + Some(d) => call_destroy_through(d, inst, alloc), + None => (), + } +} + +fn call_unowned_present() -> vk::Result { + call_log_at(LogLevel::Error, UNOWNED_QUEUE_ERROR); + vk::Result::ERROR_INITIALIZATION_FAILED +} + +fn call_forward_present( + owner: Option>, + queue: vk::Queue, + info: *const vk::PresentInfoKHR<'_>, +) -> vk::Result { + match owner { + Some(d) => call_present_frame(&d, queue, info), + None => call_unowned_present(), + } +} + +fn call_staged_limit( + stage: LimitStage, + s: &Settings, + dev: Option, + info: *const vk::PresentInfoKHR<'_>, +) { + match dev { + Some(handle) => call_limit_frame(stage, s, handle, info), + None => (), + } +} + +fn call_after_present( + presented: vk::Result, + s: &Settings, + dev: Option, + info: *const vk::PresentInfoKHR<'_>, +) -> vk::Result { + call_staged_limit(LimitStage::After, s, dev, info); + presented +} + +fn call_limited_present( + owner: Option>, + queue: vk::Queue, + info: *const vk::PresentInfoKHR<'_>, +) -> vk::Result { + let s = ensure_settings(); + let dev = owner.as_ref().map(|d| d.device.handle().as_raw()); + call_staged_limit(LimitStage::Before, s, dev, info); + call_after_present(call_forward_present(owner, queue, info), s, dev, info) +} + +extern "system" fn volt_GetDeviceQueue(dev: vk::Device, qfam: u32, qidx: u32, out: *mut vk::Queue) { + match devs_get(dev.as_raw()) { + Some(d) => { + let q = unsafe { d.device.get_device_queue(qfam, qidx) }; + call_register_queue(&d, dev, q); + call_queue_dev_put(q.as_raw(), dev.as_raw()); + unsafe { *out = q }; + } + None => call_log_at(LogLevel::Warn, LOG_QUEUE_UNREGISTERED), + } +} + +extern "system" fn volt_GetDeviceQueue2(dev: vk::Device, info: *const c_void, out: *mut vk::Queue) { + match devs_get(dev.as_raw()).and_then(|d| d.queue2_fp.map(|fp| (d, fp))) { + Some((d, fp)) => { + unsafe { fp(dev, info, out) }; + let q = unsafe { *out }; + call_register_queue(&d, dev, q); + call_queue_dev_put(q.as_raw(), dev.as_raw()); + } + None => call_log_at(LogLevel::Warn, LOG_QUEUE_2_UNREGISTERED), + } +} + +extern "system" fn vkGetInstanceProcAddr(inst: vk::Instance, name: *const c_char) -> vk::PFN_vkVoidFunction { + let n = cstr_to_str(name); + match inst == vk::Instance::null() { + true => resolve_null_instance_proc(n), + false => resolve_instance_proc(inst, n), + } +} + +extern "system" fn vkGetDeviceProcAddr(dev: vk::Device, name: *const c_char) -> vk::PFN_vkVoidFunction { + let n = cstr_to_str(name); + match ( + device_core_symbol(n), + device_provided_hooked(dev, n), + device_hooked_symbol(dev, n), + ) { + (Some(p), _, _) => unsafe { mem::transmute(p) }, + (None, Some(p), _) => unsafe { mem::transmute(p) }, + (None, None, Some(p)) => unsafe { mem::transmute(p) }, + (None, None, None) => forward_device_proc(dev, n), + } +} + +extern "system" fn vkCreateInstance( + ci: *const vk::InstanceCreateInfo<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::Instance, +) -> vk::Result { + call_init_log_level(); + call_real_create_instance( + call_advance_chain(chain_layer_info( + unsafe { (*ci).p_next }, + LOADER_INSTANCE_CREATE_INFO_TYPE, + LAYER_LINK_INFO, + )), + ci, + alloc, + out, + ) +} + +extern "system" fn vkDestroyInstance(inst: vk::Instance, alloc: *const vk::AllocationCallbacks<'_>) { + let st = insts_get(inst.as_raw()); + call_insts_del(inst.as_raw()); + match st { + Some(s) => call_chain_destroy_instance(s.gipa, inst, alloc), + None => (), + } +} + +extern "system" fn vkEnumeratePhysicalDevices( + inst: vk::Instance, + count: *mut u32, + devices: *mut vk::PhysicalDevice, +) -> vk::Result { + call_filtered_enumerate(inst, count, devices) +} + +extern "system" fn vkEnumeratePhysicalDeviceGroups( + inst: vk::Instance, + count: *mut u32, + groups: *mut VkPhysicalDeviceGroupProperties, +) -> vk::Result { + call_filtered_groups(inst, count, groups) +} + +extern "system" fn vkEnumeratePhysicalDeviceGroupsKHR( + inst: vk::Instance, + count: *mut u32, + groups: *mut VkPhysicalDeviceGroupProperties, +) -> vk::Result { + call_filtered_groups_khr(inst, count, groups) +} + +extern "system" fn vkCreateDevice( + phys: vk::PhysicalDevice, + ci: *const vk::DeviceCreateInfo<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::Device, +) -> vk::Result { + call_real_create_device( + call_advance_chain(chain_layer_info( + unsafe { (*ci).p_next }, + LOADER_DEVICE_CREATE_INFO_TYPE, + LAYER_LINK_INFO, + )), + call_loader_data_fn(chain_layer_info( + unsafe { (*ci).p_next }, + LOADER_DEVICE_CREATE_INFO_TYPE, + LAYER_DATA_CALLBACK, + )), + phys, + ci, + alloc, + out, + ) +} + +extern "system" fn vkDestroyDevice(dev: vk::Device, alloc: *const vk::AllocationCallbacks<'_>) { + match call_devs_del(dev.as_raw()) { + Some(d) => unsafe { d.device.destroy_device(alloc.as_ref()) }, + None => (), + } +} + +extern "system" fn vkCreateGraphicsPipelines( + dev: vk::Device, + cache: vk::PipelineCache, + count: u32, + cis: *const vk::GraphicsPipelineCreateInfo<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::Pipeline, +) -> vk::Result { + match devs_get(dev.as_raw()) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some(d) => call_create_graphics_pipelines(&d, cache, count, cis, alloc, out), + } +} + +extern "system" fn vkCreateComputePipelines( + dev: vk::Device, + cache: vk::PipelineCache, + count: u32, + cis: *const vk::ComputePipelineCreateInfo<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::Pipeline, +) -> vk::Result { + match devs_get(dev.as_raw()) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some(d) => call_create_compute_pipelines(&d, cache, count, cis, alloc, out), + } +} + +extern "system" fn vkCreateShadersEXT( + dev: vk::Device, + count: u32, + cis: *const VkShaderCreateInfoEXT, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut VkHandle, +) -> vk::Result { + match devs_get(dev.as_raw()).and_then(|d| d.shaders_fp.map(|fp| (d, fp))) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some((d, fp)) => call_create_shaders(&d, fp, dev, count, cis, alloc, out), + } +} + +extern "system" fn vkCreateRayTracingPipelinesKHR( + dev: vk::Device, + deferred: VkHandle, + cache: vk::PipelineCache, + count: u32, + cis: *const VkRayTracingPipelineCreateInfoKHR, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::Pipeline, +) -> vk::Result { + match devs_get(dev.as_raw()).and_then(|d| d.ray_khr_fp.map(|fp| (d, fp))) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some((d, fp)) => { + call_create_ray_tracing_khr(&d, fp, dev, deferred, cache, count, cis, alloc, out) + } + } +} + +extern "system" fn vkCreateRayTracingPipelinesNV( + dev: vk::Device, + cache: vk::PipelineCache, + count: u32, + cis: *const VkRayTracingPipelineCreateInfoNV, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::Pipeline, +) -> vk::Result { + match devs_get(dev.as_raw()).and_then(|d| d.ray_nv_fp.map(|fp| (d, fp))) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some((d, fp)) => call_create_ray_tracing_nv(&d, fp, dev, cache, count, cis, alloc, out), + } +} + +extern "system" fn vkGetPipelineIndirectMemoryRequirementsNV( + dev: vk::Device, + ci: *const vk::ComputePipelineCreateInfo<'_>, + out: *mut c_void, +) { + match devs_get(dev.as_raw()).and_then(|d| d.indirect_memory_fp.map(|fp| (d, fp))) { + None => (), + Some((d, fp)) => call_pipeline_indirect_memory(&d, fp, dev, ci, out), + } +} + +extern "system" fn vkGetPipelineKeyKHR( + dev: vk::Device, + ci: *const VkPipelineCreateInfoKHR, + out: *mut c_void, +) -> vk::Result { + match devs_get(dev.as_raw()).and_then(|d| d.pipeline_key_fp.map(|fp| (d, fp))) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some((d, fp)) => call_get_pipeline_key(&d, fp, dev, ci, out), + } +} + +extern "system" fn vkCreatePipelineBinariesKHR( + dev: vk::Device, + ci: *const VkPipelineBinaryCreateInfoKHR, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut c_void, +) -> vk::Result { + match devs_get(dev.as_raw()).and_then(|d| d.pipeline_binaries_fp.map(|fp| (d, fp))) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some((d, fp)) => call_create_pipeline_binaries(&d, fp, dev, ci, alloc, out), + } +} + +extern "system" fn vkCmdSetAlphaToCoverageEnableEXT( + buffer: vk::CommandBuffer, + enable: vk::Bool32, +) { + call_set_alpha_coverage(cmdbuf_owner(buffer), buffer, enable) +} + +extern "system" fn vkCmdSetAlphaToOneEnableEXT( + buffer: vk::CommandBuffer, + enable: vk::Bool32, +) { + call_set_alpha_one(cmdbuf_owner(buffer), buffer, enable) +} + +extern "system" fn vkCmdSetDepthClampEnableEXT( + buffer: vk::CommandBuffer, + enable: vk::Bool32, +) { + call_set_depth_clamp(cmdbuf_owner(buffer), buffer, enable) +} + +extern "system" fn vkAllocateCommandBuffers( + dev: vk::Device, + info: *const vk::CommandBufferAllocateInfo<'_>, + out: *mut vk::CommandBuffer, +) -> vk::Result { + match devs_get(dev.as_raw()) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some(d) => call_allocate_command_buffers(&d, dev, info, out), + } +} + +extern "system" fn vkFreeCommandBuffers( + dev: vk::Device, + pool: vk::CommandPool, + count: u32, + buffers: *const vk::CommandBuffer, +) { + match devs_get(dev.as_raw()) { + Some(d) => call_free_command_buffers(&d, dev, pool, count, buffers), + None => (), + } +} + +extern "system" fn vkDestroyCommandPool( + dev: vk::Device, + pool: vk::CommandPool, + alloc: *const vk::AllocationCallbacks<'_>, +) { + match devs_get(dev.as_raw()) { + Some(d) => call_destroy_command_pool(&d, dev, pool, alloc), + None => (), + } +} + +extern "system" fn vkCreateSampler( + dev: vk::Device, + ci: *const vk::SamplerCreateInfo<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::Sampler, +) -> vk::Result { + match devs_get(dev.as_raw()) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some(d) => call_create_sampler(&d, ci, alloc, out), + } +} + +extern "system" fn vkWriteSamplerDescriptorsEXT( + dev: vk::Device, + count: u32, + cis: *const vk::SamplerCreateInfo<'_>, + descriptors: *const c_void, +) -> vk::Result { + match devs_get(dev.as_raw()) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some(d) => call_write_sampler_descriptors(&d, dev, count, cis, descriptors), + } +} + +extern "system" fn vkCreateSwapchainKHR( + dev: vk::Device, + ci: *const vk::SwapchainCreateInfoKHR<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::SwapchainKHR, +) -> vk::Result { + match devs_get(dev.as_raw()) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some(d) => call_create_swapchain(&d, dev, ci, alloc, out), + } +} + +extern "system" fn vkCreateSharedSwapchainsKHR( + dev: vk::Device, + count: u32, + cis: *const vk::SwapchainCreateInfoKHR<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::SwapchainKHR, +) -> vk::Result { + match devs_get(dev.as_raw()) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some(d) => call_create_shared_swapchains(&d, dev, count, cis, alloc, out), + } +} + +extern "system" fn vkDestroySwapchainKHR( + dev: vk::Device, + sc: vk::SwapchainKHR, + alloc: *const vk::AllocationCallbacks<'_>, +) { + match devs_get(dev.as_raw()) { + Some(d) => { + call_forget_timeline(dev.as_raw(), sc); + call_forget_forced_mode(dev.as_raw(), sc); + unsafe { (d.swap_fp.destroy_swapchain_khr)(dev, sc, alloc) }; + } + None => (), + } +} + +extern "system" fn vkCreateXcbSurfaceKHR( + inst: vk::Instance, + ci: *const c_void, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::SurfaceKHR, +) -> vk::Result { + call_create_tagged_surface(FN_CREATE_XCB_SURFACE, TAG_XCB, inst, ci, alloc, out) +} + +extern "system" fn vkCreateXlibSurfaceKHR( + inst: vk::Instance, + ci: *const c_void, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::SurfaceKHR, +) -> vk::Result { + call_create_tagged_surface(FN_CREATE_XLIB_SURFACE, TAG_XCB, inst, ci, alloc, out) +} + +extern "system" fn vkCreateWaylandSurfaceKHR( + inst: vk::Instance, + ci: *const c_void, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::SurfaceKHR, +) -> vk::Result { + call_create_tagged_surface(FN_CREATE_WAYLAND_SURFACE, TAG_WAYLAND, inst, ci, alloc, out) +} + +extern "system" fn vkDestroySurfaceKHR( + inst: vk::Instance, + surface: vk::SurfaceKHR, + alloc: *const vk::AllocationCallbacks<'_>, +) { + call_destroy_tagged_surface(inst, surface, alloc) +} + +extern "system" fn vkGetPhysicalDeviceSurfacePresentModesKHR( + phys: vk::PhysicalDevice, + surface: vk::SurfaceKHR, + count: *mut u32, + modes: *mut vk::PresentModeKHR, +) -> vk::Result { + call_surface_present_modes(phys, surface, count, modes) +} + +extern "system" fn vkGetPhysicalDeviceSurfaceCapabilitiesKHR( + phys: vk::PhysicalDevice, + surface: vk::SurfaceKHR, + caps: *mut vk::SurfaceCapabilitiesKHR, +) -> vk::Result { + call_surface_capabilities(phys, surface, caps) +} + +extern "system" fn vkGetPhysicalDeviceSurfaceCapabilities2KHR( + phys: vk::PhysicalDevice, + info: *const VkPhysicalDeviceSurfaceInfo2, + caps: *mut VkSurfaceCapabilities2, +) -> vk::Result { + call_surface_capabilities2(phys, info, caps) +} + +extern "system" fn vkQueuePresentKHR(queue: vk::Queue, info: *const vk::PresentInfoKHR<'_>) -> vk::Result { + call_limited_present(queue_owner(queue), queue, info) +} + +fn call_agreed(iface: *mut VkNegotiateLayerInterface, agreed: u32) -> vk::Result { + unsafe { (*iface).loader_layer_interface_version = agreed }; + unsafe { (*iface).pfn_get_instance_proc_addr = Some(vkGetInstanceProcAddr) }; + unsafe { (*iface).pfn_get_device_proc_addr = Some(vkGetDeviceProcAddr) }; + unsafe { (*iface).pfn_get_physical_device_proc_addr = None }; + vk::Result::SUCCESS +} + +fn call_negotiated(iface: *mut VkNegotiateLayerInterface) -> vk::Result { + let offered = unsafe { (*iface).loader_layer_interface_version }; + match negotiated_version(offered, LAYER_IFACE_VERSION) { + Some(agreed) => call_agreed(iface, agreed), + None => vk::Result::ERROR_INITIALIZATION_FAILED, + } +} + +#[no_mangle] +pub extern "system" fn vkNegotiateLoaderLayerInterfaceVersion(p: *mut c_void) -> vk::Result { + let iface = p as *mut VkNegotiateLayerInterface; + match unsafe { (*iface).s_type } == LAYER_NEGOTIATE_INTERFACE_STRUCT { + true => call_negotiated(iface), + false => vk::Result::ERROR_INITIALIZATION_FAILED, + } +} diff --git a/volt/lib.rs b/src/volt/lib.rs similarity index 76% rename from volt/lib.rs rename to src/volt/lib.rs index da66a8a..8500257 100644 --- a/volt/lib.rs +++ b/src/volt/lib.rs @@ -4,6 +4,7 @@ mod consts; mod logging; mod lists; mod ranks; +mod report; mod env; mod config; mod instance; @@ -19,5 +20,5 @@ mod launcher; #[cfg(test)] mod checks; -pub use env::process_args; -pub use launcher::run_launcher; +pub use env::launch_args; +pub use launcher::call_run_launcher; diff --git a/src/volt/lists.rs b/src/volt/lists.rs new file mode 100644 index 0000000..e4590cd --- /dev/null +++ b/src/volt/lists.rs @@ -0,0 +1,62 @@ +use crate::logging::call_log_at; +use crate::logging::LogLevel; + +pub(crate) struct Narrowed { + pub(crate) items: Vec, + pub(crate) restored: bool, +} + +pub(crate) fn untouched(items: Vec) -> Narrowed { + Narrowed { + items, + restored: false, + } +} + +fn restored(wanted: Vec, rest: Vec) -> Narrowed { + match wanted.is_empty() { + true => Narrowed { + items: rest, + restored: true, + }, + false => untouched(wanted), + } +} + +pub(crate) fn forced(choice: Option, original: T) -> T { + match choice { + Some(value) => value, + None => original, + } +} + +pub(crate) fn kept(items: Vec, keep: F) -> Narrowed +where + F: Fn(&T) -> bool, +{ + let (wanted, rest): (Vec, Vec) = items.into_iter().partition(|item| keep(item)); + restored(wanted, rest) +} + +pub(crate) fn filtered(items: Vec, choice: Option, value_of: F) -> Narrowed +where + V: PartialEq + Copy, + F: Fn(&T) -> Option, +{ + match choice { + Some(value) => kept(items, |item| value_of(item) == Some(value)), + None => untouched(items), + } +} + +fn call_warn_restored(restored: bool, warn: &str) { + match restored { + true => call_log_at(LogLevel::Warn, warn), + false => (), + } +} + +pub(crate) fn call_warned(narrowed: Narrowed, warn: &str) -> Vec { + call_warn_restored(narrowed.restored, warn); + narrowed.items +} diff --git a/volt/logging.rs b/src/volt/logging.rs similarity index 70% rename from volt/logging.rs rename to src/volt/logging.rs index 8594d86..ef3b0df 100644 --- a/volt/logging.rs +++ b/src/volt/logging.rs @@ -4,11 +4,16 @@ use std::sync::atomic::AtomicI32; use std::sync::atomic::Ordering; use crate::consts::DEFAULT_LOG_LEVEL; +use crate::consts::LEVEL_WORD_ERROR; +use crate::consts::LEVEL_WORD_INFO; +use crate::consts::LEVEL_WORD_OFF; +use crate::consts::LINE_END; use crate::consts::LOG_FD; use crate::consts::LOG_LEVEL_ERROR; use crate::consts::LOG_LEVEL_INFO; use crate::consts::LOG_LEVEL_OFF; use crate::consts::LOG_LEVEL_WARN; +use crate::consts::LOG_PREFIX; use crate::env::env_log_level; pub(crate) enum LogLevel { @@ -32,9 +37,9 @@ pub(crate) fn level_num(l: &LogLevel) -> i32 { fn parse_level(s: &str) -> LogLevel { match s { - "off" => LogLevel::Off, - "error" => LogLevel::Error, - "info" => LogLevel::Info, + LEVEL_WORD_OFF => LogLevel::Off, + LEVEL_WORD_ERROR => LogLevel::Error, + LEVEL_WORD_INFO => LogLevel::Info, _ => LogLevel::Warn, } } @@ -47,14 +52,18 @@ fn call_write_log(s: &str) { unsafe { libc::write(LOG_FD, s.as_ptr() as *const c_void, s.len()) }; } -pub(crate) fn log_at(level: LogLevel, msg: &str) { +pub(crate) fn info_wanted() -> bool { + should_emit(level_num(&LogLevel::Info), LEVEL.load(Ordering::Relaxed)) +} + +pub(crate) fn call_log_at(level: LogLevel, msg: &str) { match should_emit(level_num(&level), LEVEL.load(Ordering::Relaxed)) { - true => call_write_log(&format!("[volt] {}\n", msg)), + true => call_write_log(&[LOG_PREFIX, msg, LINE_END].concat()), false => (), } } -pub(crate) fn init_log_level() { +pub(crate) fn call_init_log_level() { match LEVEL_SET.swap(true, Ordering::Relaxed) { true => (), false => LEVEL.store( diff --git a/src/volt/pipeline.rs b/src/volt/pipeline.rs new file mode 100644 index 0000000..b41ae31 --- /dev/null +++ b/src/volt/pipeline.rs @@ -0,0 +1,1004 @@ +use std::ffi::c_void; +use std::sync::Arc; + +use ash::vk; +use ash::vk::Handle; + +use crate::config::ensure_settings; +use crate::config::Settings; +use crate::consts::BINARY_INFO_LOG; +use crate::consts::CHAIN_SAMPLER_LABEL; +use crate::consts::FEATURE_ALPHA_ONE; +use crate::consts::COMPUTE_PIPELINE_CREATE_INFO_TYPE; +use crate::consts::GRAPHICS_PIPELINE_CREATE_INFO_TYPE; +use crate::consts::RAY_TRACING_PIPELINE_CREATE_INFO_KHR_TYPE; +use crate::consts::PIPELINE_BINARY_INFO_TYPE; +use crate::consts::PIPELINE_CREATE_INFO_KHR_TYPE; +use crate::consts::WRAPPED_UNDECLARED_LOG; +use crate::consts::FEATURE_DEPTH_CLAMP; +use crate::consts::FEATURE_SHADING; +use crate::consts::NOTE_MIXED_SAMPLES; +use crate::consts::SETTING_ALPHA_COVERAGE; +use crate::consts::SETTING_ALPHA_ONE; +use crate::consts::SETTING_DEPTH_CLAMP; +use crate::consts::SETTING_SAMPLE_SHADING; +use crate::consts::SHADER_GROUPS_TYPE; +use crate::consts::SHADING_OFF; +use crate::consts::TEXT_OFF; +use crate::consts::UNOWNED_BUFFER_ERROR; +use crate::lists::forced; +use crate::device::DeviceCaps; +use crate::device::VkDevState; +use crate::instance::call_relinked_chain; +use crate::instance::chain_find; +use crate::instance::PfnCreateRayTracingKHR; +use crate::instance::PfnCreateRayTracingNV; +use crate::instance::PfnCreatePipelineBinaries; +use crate::instance::PfnCreateShaders; +use crate::instance::PfnGetPipelineKey; +use crate::instance::PfnPipelineIndirectMemory; +use crate::instance::Relinked; +use crate::instance::VkGraphicsPipelineShaderGroupsCreateInfoNV; +use crate::instance::VkGraphicsShaderGroupCreateInfoNV; +use crate::instance::VkHandle; +use crate::instance::VkPipelineBinaryCreateInfoKHR; +use crate::instance::VkPipelineBinaryInfoKHR; +use crate::instance::VkPipelineCreateInfoKHR; +use crate::instance::VkRayTracingPipelineCreateInfoKHR; +use crate::instance::VkRayTracingPipelineCreateInfoNV; +use crate::instance::VkShaderCreateInfoEXT; +use crate::logging::info_wanted; +use crate::logging::call_log_at; +use crate::logging::LogLevel; +use crate::report::call_report_value; +use crate::report::feature_note; +use crate::report::number_text; +use crate::report::toggle_text; +use crate::sampler::rebuilt_mapping_chain; +use crate::sampler::rebuilt_stage; +use crate::sampler::rebuilt_stages; +use crate::sampler::ChainRebuild; +use crate::sampler::StageRebuild; +use crate::sampler::StagesRebuild; + +fn pick_coverage(choice: Option, original: vk::Bool32) -> vk::Bool32 { + forced(choice, original) +} + +fn alpha_one_allowed(choice: Option, caps: &DeviceCaps) -> Option { + match (choice, caps.alpha_to_one) { + (None, _) => None, + (Some(value), true) => Some(value), + (Some(_), false) => None, + } +} + +fn clamp_allowed(choice: Option, caps: &DeviceCaps) -> Option { + match (choice, caps.depth_clamp) { + (None, _) => None, + (Some(value), true) => Some(value), + (Some(_), false) => None, + } +} + +fn shading_off_only(rate: f32) -> Option { + match rate > SHADING_OFF { + true => None, + false => Some(rate), + } +} + +fn shading_allowed(choice: Option, caps: &DeviceCaps) -> Option { + match (choice, caps.sample_rate_shading, caps.mixed_samples) { + (Some(rate), true, false) => Some(rate), + (Some(rate), _, _) => shading_off_only(rate), + (None, _, _) => None, + } +} + +fn shading_reason(set: bool, caps: &DeviceCaps) -> Option { + match (set, caps.mixed_samples) { + (true, true) => Some(NOTE_MIXED_SAMPLES.into()), + (true, false) => feature_note(true, caps.sample_rate_shading, FEATURE_SHADING), + (false, _) => None, + } +} + +fn shading_note(s: &Settings, caps: &DeviceCaps) -> Option { + match shading_allowed(s.sample_shading, caps) { + Some(_) => None, + None => shading_reason(s.sample_shading.is_some(), caps), + } +} + +fn shading_text(rate: f32) -> String { + match rate > SHADING_OFF { + true => number_text(rate), + false => TEXT_OFF.into(), + } +} + +fn shading_of(enable: vk::Bool32, rate: f32) -> f32 { + match enable == vk::TRUE { + true => rate, + false => SHADING_OFF, + } +} + +fn call_coverage_line(owner: u64, asked: vk::Bool32, held: vk::Bool32) { + call_report_value( + owner, + SETTING_ALPHA_COVERAGE, + asked, + held, + toggle_text, + None, + ); +} + +fn call_alpha_one_line( + owner: u64, + s: &Settings, + caps: &DeviceCaps, + asked: vk::Bool32, + held: vk::Bool32, +) { + call_report_value( + owner, + SETTING_ALPHA_ONE, + asked, + held, + toggle_text, + feature_note(s.alpha_to_one.is_some(), caps.alpha_to_one, FEATURE_ALPHA_ONE), + ); +} + +fn call_clamp_line( + owner: u64, + s: &Settings, + caps: &DeviceCaps, + asked: vk::Bool32, + held: vk::Bool32, +) { + call_report_value( + owner, + SETTING_DEPTH_CLAMP, + asked, + held, + toggle_text, + feature_note( + s.depth_clamp.is_some(), + caps.depth_clamp, + FEATURE_DEPTH_CLAMP, + ), + ); +} + +fn call_shading_line( + owner: u64, + s: &Settings, + caps: &DeviceCaps, + asked: &vk::PipelineMultisampleStateCreateInfo<'_>, + held: &vk::PipelineMultisampleStateCreateInfo<'_>, +) { + call_report_value( + owner, + SETTING_SAMPLE_SHADING, + shading_of(asked.sample_shading_enable, asked.min_sample_shading), + shading_of(held.sample_shading_enable, held.min_sample_shading), + shading_text, + shading_note(s, caps), + ); +} + +fn call_multisample_lines( + owner: u64, + s: &Settings, + caps: &DeviceCaps, + asked: &vk::PipelineMultisampleStateCreateInfo<'_>, + held: &vk::PipelineMultisampleStateCreateInfo<'_>, +) { + call_shading_line(owner, s, caps, asked, held); + call_coverage_line( + owner, + asked.alpha_to_coverage_enable, + held.alpha_to_coverage_enable, + ); + call_alpha_one_line( + owner, + s, + caps, + asked.alpha_to_one_enable, + held.alpha_to_one_enable, + ); +} + +fn call_report_multisample( + owner: u64, + s: &Settings, + caps: &DeviceCaps, + original: *const vk::PipelineMultisampleStateCreateInfo<'_>, + patched: &Option>, +) { + match (unsafe { original.as_ref() }, patched) { + (Some(asked), Some(held)) => call_multisample_lines(owner, s, caps, asked, held), + (_, _) => (), + } +} + +fn call_report_rasterization( + owner: u64, + s: &Settings, + caps: &DeviceCaps, + original: *const vk::PipelineRasterizationStateCreateInfo<'_>, + patched: &Option>, +) { + match (unsafe { original.as_ref() }, patched) { + (Some(asked), Some(held)) => call_clamp_line( + owner, + s, + caps, + asked.depth_clamp_enable, + held.depth_clamp_enable, + ), + (_, _) => (), + } +} + +fn call_report_one( + owner: u64, + s: &Settings, + caps: &DeviceCaps, + original: &vk::GraphicsPipelineCreateInfo<'_>, + multisample: &Option>, + rasterization: &Option>, +) { + call_report_multisample(owner, s, caps, original.p_multisample_state, multisample); + call_report_rasterization( + owner, + s, + caps, + original.p_rasterization_state, + rasterization, + ); +} + +fn call_report_each( + dev: &VkDevState, + s: &Settings, + originals: &[vk::GraphicsPipelineCreateInfo<'_>], + multisamples: &[Option>], + rasterizations: &[Option>], +) { + originals + .iter() + .zip(multisamples.iter()) + .zip(rasterizations.iter()) + .for_each(|((ci, m), r)| { + call_report_one(dev.device.handle().as_raw(), s, &dev.caps, ci, m, r) + }); +} + +fn call_report_pipelines( + dev: &VkDevState, + s: &Settings, + originals: &[vk::GraphicsPipelineCreateInfo<'_>], + multisamples: &[Option>], + rasterizations: &[Option>], +) { + match info_wanted() { + true => call_report_each(dev, s, originals, multisamples, rasterizations), + false => (), + } +} + +fn call_report_coverage(dev: &VkDevState, asked: vk::Bool32, held: vk::Bool32) { + match info_wanted() { + true => call_coverage_line( + dev.device.handle().as_raw(), + asked, + held, + ), + false => (), + } +} + +fn call_report_alpha_one(dev: &VkDevState, asked: vk::Bool32, held: vk::Bool32) { + match info_wanted() { + true => call_alpha_one_line( + dev.device.handle().as_raw(), + ensure_settings(), + &dev.caps, + asked, + held, + ), + false => (), + } +} + +fn call_report_clamp(dev: &VkDevState, asked: vk::Bool32, held: vk::Bool32) { + match info_wanted() { + true => call_clamp_line( + dev.device.handle().as_raw(), + ensure_settings(), + &dev.caps, + asked, + held, + ), + false => (), + } +} + +fn shading_pair(rate: f32) -> (vk::Bool32, f32) { + match rate > SHADING_OFF { + true => (vk::TRUE, rate), + false => (vk::FALSE, SHADING_OFF), + } +} + +fn pick_shading( + choice: Option, + caps: &DeviceCaps, + original: (vk::Bool32, f32), +) -> (vk::Bool32, f32) { + match shading_allowed(choice, caps) { + Some(rate) => shading_pair(rate), + None => original, + } +} + +fn rebuilt_multisample<'a>( + s: &Settings, + caps: &DeviceCaps, + original: &vk::PipelineMultisampleStateCreateInfo<'a>, +) -> vk::PipelineMultisampleStateCreateInfo<'a> { + let (shading_enable, shading_rate) = pick_shading( + s.sample_shading, + caps, + (original.sample_shading_enable, original.min_sample_shading), + ); + vk::PipelineMultisampleStateCreateInfo { + sample_shading_enable: shading_enable, + min_sample_shading: shading_rate, + alpha_to_coverage_enable: pick_coverage(s.alpha_coverage, original.alpha_to_coverage_enable), + alpha_to_one_enable: pick_coverage( + alpha_one_allowed(s.alpha_to_one, caps), + original.alpha_to_one_enable, + ), + ..*original + } +} + +fn rebuilt_rasterization<'a>( + s: &Settings, + caps: &DeviceCaps, + original: &vk::PipelineRasterizationStateCreateInfo<'a>, +) -> vk::PipelineRasterizationStateCreateInfo<'a> { + vk::PipelineRasterizationStateCreateInfo { + depth_clamp_enable: pick_coverage( + clamp_allowed(s.depth_clamp, caps), + original.depth_clamp_enable, + ), + ..*original + } +} + +fn patched_rasterization<'a>( + s: &Settings, + caps: &DeviceCaps, + p: *const vk::PipelineRasterizationStateCreateInfo<'a>, +) -> Option> { + match p.is_null() { + true => None, + false => Some(rebuilt_rasterization(s, caps, unsafe { &*p })), + } +} + +fn patched_multisample<'a>( + s: &Settings, + caps: &DeviceCaps, + p: *const vk::PipelineMultisampleStateCreateInfo<'a>, +) -> Option> { + match p.is_null() { + true => None, + false => Some(rebuilt_multisample(s, caps, unsafe { &*p })), + } +} + +fn state_ptr(owned: &Option, original: *const T) -> *const T { + match owned { + Some(v) => v as *const T, + None => original, + } +} + +pub(crate) struct GroupsRebuild { + #[allow(dead_code)] + each: Vec>, + #[allow(dead_code)] + groups: Vec, + #[allow(dead_code)] + node: Vec, + #[allow(dead_code)] + relink: Relinked, + head: *const c_void, +} + +fn group_list( + node: *const VkGraphicsPipelineShaderGroupsCreateInfoNV, +) -> Vec { + (0..unsafe { (*node).group_count } as usize) + .map(|at| unsafe { *(*node).p_groups.add(at) }) + .collect() +} + +fn rebuilt_group( + group: &VkGraphicsShaderGroupCreateInfoNV, + built: &Option, +) -> VkGraphicsShaderGroupCreateInfoNV { + match built { + Some(one) => VkGraphicsShaderGroupCreateInfoNV { + p_stages: one.stages.as_ptr(), + ..*group + }, + None => *group, + } +} + +fn rebuilt_groups_node( + node: *const VkGraphicsPipelineShaderGroupsCreateInfoNV, + groups: &[VkGraphicsShaderGroupCreateInfoNV], +) -> VkGraphicsPipelineShaderGroupsCreateInfoNV { + VkGraphicsPipelineShaderGroupsCreateInfoNV { + p_groups: groups.as_ptr(), + ..unsafe { *node } + } +} + +fn linked_groups( + head: *const c_void, + node: *const VkGraphicsPipelineShaderGroupsCreateInfoNV, + each: Vec>, +) -> Option { + let groups: Vec = group_list(node) + .iter() + .zip(each.iter()) + .map(|(group, built)| rebuilt_group(group, built)) + .collect(); + let owned = vec![rebuilt_groups_node(node, &groups)]; + let relink = call_relinked_chain( + head, + SHADER_GROUPS_TYPE, + owned.as_ptr() as *const c_void, + CHAIN_SAMPLER_LABEL, + )?; + Some(GroupsRebuild { + head: relink.head, + each, + groups, + node: owned, + relink, + }) +} + +fn built_groups( + dev: &VkDevState, + head: *const c_void, + node: *const VkGraphicsPipelineShaderGroupsCreateInfoNV, +) -> Option { + let each: Vec> = group_list(node) + .iter() + .map(|group| rebuilt_stages(dev, group.p_stages, group.stage_count)) + .collect(); + match each.iter().any(|one| one.is_some()) { + true => linked_groups(head, node, each), + false => None, + } +} + +fn rebuilt_groups(dev: &VkDevState, head: *const c_void) -> Option { + built_groups( + dev, + head, + chain_find(head, SHADER_GROUPS_TYPE)? as *const VkGraphicsPipelineShaderGroupsCreateInfoNV, + ) +} + +fn stages_ptr<'a>( + built: &'a Option, + original: *const vk::PipelineShaderStageCreateInfo<'a>, +) -> *const vk::PipelineShaderStageCreateInfo<'a> { + match built { + Some(one) => one.stages.as_ptr().cast(), + None => original, + } +} + +fn groups_ptr(built: &Option, original: *const c_void) -> *const c_void { + match built { + Some(one) => one.head, + None => original, + } +} + +fn chain_ptr(built: &Option, original: *const c_void) -> *const c_void { + match built { + Some(one) => one.head, + None => original, + } +} + +fn patched_ci<'a>( + original: &vk::GraphicsPipelineCreateInfo<'a>, + multisample: &'a Option>, + rasterization: &'a Option>, + stages: &'a Option, + groups: &'a Option, +) -> vk::GraphicsPipelineCreateInfo<'a> { + vk::GraphicsPipelineCreateInfo { + p_multisample_state: state_ptr(multisample, original.p_multisample_state), + p_rasterization_state: state_ptr(rasterization, original.p_rasterization_state), + p_stages: stages_ptr(stages, original.p_stages), + p_next: groups_ptr(groups, original.p_next), + ..*original + } +} + +fn patched_compute_ci<'a>( + original: &vk::ComputePipelineCreateInfo<'a>, + built: &'a Option, +) -> vk::ComputePipelineCreateInfo<'a> { + match built { + Some(one) => vk::ComputePipelineCreateInfo { + stage: one.stage, + ..*original + }, + None => *original, + } +} + +fn patched_shader_ci( + original: &VkShaderCreateInfoEXT, + built: &Option, +) -> VkShaderCreateInfoEXT { + VkShaderCreateInfoEXT { + p_next: chain_ptr(built, original.p_next), + ..*original + } +} + +fn patched_ray_khr_ci( + original: &VkRayTracingPipelineCreateInfoKHR, + built: &Option, +) -> VkRayTracingPipelineCreateInfoKHR { + match built { + Some(one) => VkRayTracingPipelineCreateInfoKHR { + p_stages: one.stages.as_ptr(), + ..*original + }, + None => *original, + } +} + +fn patched_ray_nv_ci( + original: &VkRayTracingPipelineCreateInfoNV, + built: &Option, +) -> VkRayTracingPipelineCreateInfoNV { + match built { + Some(one) => VkRayTracingPipelineCreateInfoNV { + p_stages: one.stages.as_ptr(), + ..*original + }, + None => *original, + } +} + +fn stage_of<'a>( + ci: &'a vk::ComputePipelineCreateInfo<'a>, +) -> *const vk::PipelineShaderStageCreateInfo<'static> { + (&ci.stage as *const vk::PipelineShaderStageCreateInfo<'a>).cast() +} + +pub(crate) fn call_create_compute_pipelines( + dev: &VkDevState, + cache: vk::PipelineCache, + count: u32, + cis: *const vk::ComputePipelineCreateInfo<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::Pipeline, +) -> vk::Result { + let originals: Vec> = + unsafe { std::slice::from_raw_parts(cis, count as usize) }.to_vec(); + call_binary_line(originals.iter().any(|ci| names_binaries(ci.p_next))); + let stages: Vec> = originals + .iter() + .map(|ci| rebuilt_stage(dev, stage_of(ci))) + .collect(); + let patched: Vec> = originals + .iter() + .zip(stages.iter()) + .map(|(ci, built)| patched_compute_ci(ci, built)) + .collect(); + unsafe { + (dev.device.fp_v1_0().create_compute_pipelines)( + dev.device.handle(), + cache, + count, + patched.as_ptr(), + alloc, + out, + ) + } +} + +pub(crate) fn call_create_shaders( + dev: &VkDevState, + fp: PfnCreateShaders, + handle: vk::Device, + count: u32, + cis: *const VkShaderCreateInfoEXT, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut VkHandle, +) -> vk::Result { + let originals: Vec = + unsafe { std::slice::from_raw_parts(cis, count as usize) }.to_vec(); + let chains: Vec> = originals + .iter() + .map(|ci| rebuilt_mapping_chain(dev, ci.p_next)) + .collect(); + let patched: Vec = originals + .iter() + .zip(chains.iter()) + .map(|(ci, built)| patched_shader_ci(ci, built)) + .collect(); + unsafe { fp(handle, count, patched.as_ptr(), alloc, out) } +} + +pub(crate) fn call_create_ray_tracing_khr( + dev: &VkDevState, + fp: PfnCreateRayTracingKHR, + handle: vk::Device, + deferred: VkHandle, + cache: vk::PipelineCache, + count: u32, + cis: *const VkRayTracingPipelineCreateInfoKHR, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::Pipeline, +) -> vk::Result { + let originals: Vec = + unsafe { std::slice::from_raw_parts(cis, count as usize) }.to_vec(); + call_binary_line(originals.iter().any(|ci| names_binaries(ci.p_next))); + let stages: Vec> = originals + .iter() + .map(|ci| rebuilt_stages(dev, ci.p_stages, ci.stage_count)) + .collect(); + let patched: Vec = originals + .iter() + .zip(stages.iter()) + .map(|(ci, built)| patched_ray_khr_ci(ci, built)) + .collect(); + unsafe { fp(handle, deferred, cache, count, patched.as_ptr(), alloc, out) } +} + +pub(crate) fn call_create_ray_tracing_nv( + dev: &VkDevState, + fp: PfnCreateRayTracingNV, + handle: vk::Device, + cache: vk::PipelineCache, + count: u32, + cis: *const VkRayTracingPipelineCreateInfoNV, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::Pipeline, +) -> vk::Result { + let originals: Vec = + unsafe { std::slice::from_raw_parts(cis, count as usize) }.to_vec(); + let stages: Vec> = originals + .iter() + .map(|ci| rebuilt_stages(dev, ci.p_stages, ci.stage_count)) + .collect(); + let patched: Vec = originals + .iter() + .zip(stages.iter()) + .map(|(ci, built)| patched_ray_nv_ci(ci, built)) + .collect(); + unsafe { fp(handle, cache, count, patched.as_ptr(), alloc, out) } +} + +pub(crate) fn call_pipeline_indirect_memory( + dev: &VkDevState, + fp: PfnPipelineIndirectMemory, + handle: vk::Device, + ci: *const vk::ComputePipelineCreateInfo<'_>, + out: *mut c_void, +) { + let original = unsafe { *ci }; + let built = rebuilt_stage(dev, stage_of(&original)); + unsafe { fp(handle, &patched_compute_ci(&original, &built), out) }; +} + +fn call_forward_coverage(dev: &VkDevState, buffer: vk::CommandBuffer, enable: vk::Bool32) { + let held = pick_coverage(ensure_settings().alpha_coverage, enable); + call_report_coverage(dev, enable, held); + match dev.alpha_fp { + Some(fp) => unsafe { fp(buffer, held) }, + None => (), + } +} + +pub(crate) fn call_set_alpha_coverage( + owner: Option>, + buffer: vk::CommandBuffer, + enable: vk::Bool32, +) { + match owner { + Some(d) => call_forward_coverage(&d, buffer, enable), + None => call_log_at(LogLevel::Error, UNOWNED_BUFFER_ERROR), + } +} + +fn call_forward_alpha_one(dev: &VkDevState, buffer: vk::CommandBuffer, enable: vk::Bool32) { + let held = pick_coverage( + alpha_one_allowed(ensure_settings().alpha_to_one, &dev.caps), + enable, + ); + call_report_alpha_one(dev, enable, held); + match dev.alpha_one_fp { + Some(fp) => unsafe { fp(buffer, held) }, + None => (), + } +} + +pub(crate) fn call_set_alpha_one( + owner: Option>, + buffer: vk::CommandBuffer, + enable: vk::Bool32, +) { + match owner { + Some(d) => call_forward_alpha_one(&d, buffer, enable), + None => call_log_at(LogLevel::Error, UNOWNED_BUFFER_ERROR), + } +} + +fn call_forward_clamp(dev: &VkDevState, buffer: vk::CommandBuffer, enable: vk::Bool32) { + let held = pick_coverage( + clamp_allowed(ensure_settings().depth_clamp, &dev.caps), + enable, + ); + call_report_clamp(dev, enable, held); + match dev.clamp_fp { + Some(fp) => unsafe { fp(buffer, held) }, + None => (), + } +} + +pub(crate) fn call_set_depth_clamp( + owner: Option>, + buffer: vk::CommandBuffer, + enable: vk::Bool32, +) { + match owner { + Some(d) => call_forward_clamp(&d, buffer, enable), + None => call_log_at(LogLevel::Error, UNOWNED_BUFFER_ERROR), + } +} + +pub(crate) struct GraphicsPatch<'a> { + multisamples: Vec>>, + rasterizations: Vec>>, + stages: Vec>, + groups: Vec>, +} + +fn graphics_patch<'a>( + dev: &VkDevState, + s: &Settings, + originals: &[vk::GraphicsPipelineCreateInfo<'a>], +) -> GraphicsPatch<'a> { + let multisamples: Vec>> = originals + .iter() + .map(|ci| patched_multisample(s, &dev.caps, ci.p_multisample_state)) + .collect(); + let rasterizations: Vec>> = originals + .iter() + .map(|ci| patched_rasterization(s, &dev.caps, ci.p_rasterization_state)) + .collect(); + call_report_pipelines(dev, s, originals, &multisamples, &rasterizations); + let stages: Vec> = originals + .iter() + .map(|ci| rebuilt_stages(dev, ci.p_stages.cast(), ci.stage_count)) + .collect(); + let groups: Vec> = originals + .iter() + .map(|ci| rebuilt_groups(dev, ci.p_next)) + .collect(); + GraphicsPatch { + multisamples, + rasterizations, + stages, + groups, + } +} + +fn graphics_patched<'a>( + originals: &[vk::GraphicsPipelineCreateInfo<'a>], + patch: &'a GraphicsPatch<'a>, +) -> Vec> { + originals + .iter() + .zip(patch.multisamples.iter()) + .zip(patch.rasterizations.iter()) + .zip(patch.stages.iter()) + .zip(patch.groups.iter()) + .map(|((((ci, m), r), t), g)| patched_ci(ci, m, r, t, g)) + .collect() +} + +pub(crate) fn call_create_graphics_pipelines( + dev: &VkDevState, + cache: vk::PipelineCache, + count: u32, + cis: *const vk::GraphicsPipelineCreateInfo<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::Pipeline, +) -> vk::Result { + let originals: Vec> = + unsafe { std::slice::from_raw_parts(cis, count as usize) }.to_vec(); + call_binary_line(originals.iter().any(|ci| names_binaries(ci.p_next))); + let patch = graphics_patch(dev, ensure_settings(), &originals); + let patched = graphics_patched(&originals, &patch); + unsafe { + (dev.device.fp_v1_0().create_graphics_pipelines)( + dev.device.handle(), + cache, + count, + patched.as_ptr(), + alloc, + out, + ) + } +} + +fn binary_count(node: *const VkPipelineBinaryInfoKHR) -> u32 { + unsafe { (*node).binary_count } +} + +fn names_binaries(p_next: *const c_void) -> bool { + match chain_find(p_next, PIPELINE_BINARY_INFO_TYPE) { + Some(node) => binary_count(node as *const VkPipelineBinaryInfoKHR) > 0, + None => false, + } +} + +fn call_binary_line(named: bool) { + match named { + true => call_log_at(LogLevel::Info, BINARY_INFO_LOG), + false => (), + } +} + +fn wrapper_of(head: *mut c_void) -> VkPipelineCreateInfoKHR { + VkPipelineCreateInfoKHR { + s_type: vk::StructureType::from_raw(PIPELINE_CREATE_INFO_KHR_TYPE as i32), + p_next: head, + } +} + +fn wrapped_kind(inner: *const c_void) -> u32 { + unsafe { (*(inner as *const vk::StructureType)).as_raw() as u32 } +} + +fn call_undeclared_wrapped() { + call_log_at(LogLevel::Warn, WRAPPED_UNDECLARED_LOG); +} + +fn call_undeclared_run(node: *const VkPipelineCreateInfoKHR, run: F) -> vk::Result +where + F: FnOnce(*const VkPipelineCreateInfoKHR) -> vk::Result, +{ + call_undeclared_wrapped(); + run(node) +} + +fn call_wrapped_graphics( + dev: &VkDevState, + s: &Settings, + inner: *const c_void, + run: F, +) -> vk::Result +where + F: FnOnce(*const VkPipelineCreateInfoKHR) -> vk::Result, +{ + let originals = vec![unsafe { *(inner as *const vk::GraphicsPipelineCreateInfo<'_>) }]; + let patch = graphics_patch(dev, s, &originals); + let patched = graphics_patched(&originals, &patch); + run(&wrapper_of(patched.as_ptr() as *mut c_void)) +} + +fn call_wrapped_compute( + dev: &VkDevState, + inner: *const c_void, + run: F, +) -> vk::Result +where + F: FnOnce(*const VkPipelineCreateInfoKHR) -> vk::Result, +{ + let original = unsafe { *(inner as *const vk::ComputePipelineCreateInfo<'_>) }; + let built = rebuilt_stage(dev, stage_of(&original)); + let patched = [patched_compute_ci(&original, &built)]; + run(&wrapper_of(patched.as_ptr() as *mut c_void)) +} + +fn call_wrapped_ray( + dev: &VkDevState, + inner: *const c_void, + run: F, +) -> vk::Result +where + F: FnOnce(*const VkPipelineCreateInfoKHR) -> vk::Result, +{ + let original = unsafe { *(inner as *const VkRayTracingPipelineCreateInfoKHR) }; + let built = rebuilt_stages(dev, original.p_stages, original.stage_count); + let patched = [patched_ray_khr_ci(&original, &built)]; + run(&wrapper_of(patched.as_ptr() as *mut c_void)) +} + +fn call_with_wrapped( + dev: &VkDevState, + s: &Settings, + node: *const VkPipelineCreateInfoKHR, + run: F, +) -> vk::Result +where + F: FnOnce(*const VkPipelineCreateInfoKHR) -> vk::Result, +{ + let inner = unsafe { (*node).p_next } as *const c_void; + match wrapped_kind(inner) { + GRAPHICS_PIPELINE_CREATE_INFO_TYPE => call_wrapped_graphics(dev, s, inner, run), + COMPUTE_PIPELINE_CREATE_INFO_TYPE => call_wrapped_compute(dev, inner, run), + RAY_TRACING_PIPELINE_CREATE_INFO_KHR_TYPE => call_wrapped_ray(dev, inner, run), + _ => call_undeclared_run(node, run), + } +} + +pub(crate) fn call_get_pipeline_key( + dev: &VkDevState, + fp: PfnGetPipelineKey, + handle: vk::Device, + ci: *const VkPipelineCreateInfoKHR, + out: *mut c_void, +) -> vk::Result { + match ci.is_null() { + true => unsafe { fp(handle, ci, out) }, + false => call_with_wrapped(dev, ensure_settings(), ci, |built| unsafe { + fp(handle, built, out) + }), + } +} + +fn rebuilt_binary_ci( + original: *const VkPipelineBinaryCreateInfoKHR, + wrapper: *const VkPipelineCreateInfoKHR, +) -> VkPipelineBinaryCreateInfoKHR { + VkPipelineBinaryCreateInfoKHR { + p_pipeline_create_info: wrapper, + ..unsafe { *original } + } +} + +pub(crate) fn call_create_pipeline_binaries( + dev: &VkDevState, + fp: PfnCreatePipelineBinaries, + handle: vk::Device, + ci: *const VkPipelineBinaryCreateInfoKHR, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut c_void, +) -> vk::Result { + let wrapped = unsafe { (*ci).p_pipeline_create_info }; + match wrapped.is_null() { + true => unsafe { fp(handle, ci, alloc, out) }, + false => call_with_wrapped(dev, ensure_settings(), wrapped, |built| { + let rebuilt = rebuilt_binary_ci(ci, built); + unsafe { fp(handle, &rebuilt, alloc, out) } + }), + } +} diff --git a/src/volt/present.rs b/src/volt/present.rs new file mode 100644 index 0000000..a278f34 --- /dev/null +++ b/src/volt/present.rs @@ -0,0 +1,355 @@ +use std::collections::HashMap; +use std::sync::Mutex; +use std::sync::OnceLock; +use std::thread; +use std::time::Duration; +use std::time::Instant; + +use ash::vk; +use ash::vk::Handle; + +use crate::config::Settings; +use crate::consts::CADENCE_DYNAMIC; +use crate::consts::CADENCE_FIXED; +use crate::consts::CADENCE_SMOOTH; +use crate::consts::CadenceChoice; +use crate::consts::LimitStage; +use crate::consts::METHOD_EARLY; +use crate::consts::METHOD_LATE; +use crate::consts::METHOD_REACTIVE; +use crate::consts::MethodChoice; +use crate::consts::NS_PER_S; +use crate::consts::PACE_SPIKE_LIMIT; +use crate::consts::PACE_STEPS; +use crate::consts::PACE_WINDOW; +use crate::consts::PACING_PRECISE; +use crate::consts::PACING_SLEEP; +use crate::consts::PACING_SLICED; +use crate::consts::PACING_SPIN; +use crate::consts::PacingChoice; +use crate::consts::SLICE_MARGIN_NS; +use crate::consts::SLICE_STEP_NS; +use crate::consts::SPIN_MARGIN_NS; +use crate::device::VkDevState; +use crate::swapchain::rebuilt_present; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct Timeline { + pub(crate) target: u64, + pub(crate) interval: u64, + pub(crate) last: u64, + pub(crate) peak: u64, +} + +type TimelineMap = HashMap<(u64, u64), Timeline>; + +static EPOCH: OnceLock = OnceLock::new(); +static TIMELINES: Mutex> = Mutex::new(None); + +fn call_now_ns() -> u64 { + EPOCH.get_or_init(Instant::now).elapsed().as_nanos() as u64 +} + +pub(crate) fn target_interval_ns(fps: f32) -> u64 { + (NS_PER_S / fps as f64) as u64 +} + +pub(crate) fn cadence_display(cadence: CadenceChoice) -> String { + match cadence { + CadenceChoice::Fixed => CADENCE_FIXED.into(), + CadenceChoice::Smooth => CADENCE_SMOOTH.into(), + CadenceChoice::Dynamic => CADENCE_DYNAMIC.into(), + } +} + +pub(crate) fn method_display(method: MethodChoice) -> String { + match method { + MethodChoice::Early => METHOD_EARLY.into(), + MethodChoice::Late => METHOD_LATE.into(), + MethodChoice::Reactive => METHOD_REACTIVE.into(), + } +} + +pub(crate) fn pacing_display(pacing: PacingChoice) -> String { + match pacing { + PacingChoice::Sleep => PACING_SLEEP.into(), + PacingChoice::Sliced => PACING_SLICED.into(), + PacingChoice::Precise => PACING_PRECISE.into(), + PacingChoice::Spin => PACING_SPIN.into(), + } +} + +fn next_target_ns(now: u64, previous: u64, interval: u64, fresh: bool) -> u64 { + match fresh { + true => now.saturating_add(interval), + false => previous.saturating_add(interval).max(now), + } +} + +fn wait_deficit_ns(now: u64, target: u64) -> u64 { + target.saturating_sub(now) +} + +fn slice_length_ns(remaining: u64) -> u64 { + remaining.saturating_sub(SLICE_MARGIN_NS).min(SLICE_STEP_NS) +} + +fn restart_wanted(method: Option, interval_changed: bool) -> bool { + match method { + Some(MethodChoice::Reactive) => true, + Some(MethodChoice::Early) | Some(MethodChoice::Late) | None => interval_changed, + } +} + +fn observed_ns(line: Timeline, now: u64, interval: u64) -> u64 { + now.saturating_sub(line.last) + .min(interval.saturating_mul(PACE_SPIKE_LIMIT)) +} + +fn decayed_ns(peak: u64) -> u64 { + peak * (PACE_WINDOW - 1) / PACE_WINDOW +} + +fn peaked_ns(previous: Option, now: u64, interval: u64) -> u64 { + match previous.filter(|line| line.interval == interval) { + Some(line) => observed_ns(line, now, interval).max(decayed_ns(line.peak)), + None => interval, + } +} + +fn steps_needed(interval: u64, peak: u64) -> u64 { + peak.saturating_mul(PACE_STEPS) + .div_ceil(interval) + .max(PACE_STEPS) +} + +fn stepped_ns(interval: u64, peak: u64) -> u64 { + interval.saturating_mul(steps_needed(interval, peak)) / PACE_STEPS +} + +fn paced_ns(cadence: Option, interval: u64, peak: u64) -> u64 { + match cadence { + Some(CadenceChoice::Smooth) => interval.max(peak), + Some(CadenceChoice::Dynamic) => stepped_ns(interval, peak), + Some(CadenceChoice::Fixed) | None => interval, + } +} + +fn previous_target(previous: Option, now: u64) -> u64 { + match previous { + Some(line) => line.target, + None => now, + } +} + +fn interval_changed(previous: Option, interval: u64) -> bool { + match previous { + Some(line) => line.interval != interval, + None => true, + } +} + +pub(crate) fn advanced( + previous: Option, + now: u64, + interval: u64, + method: Option, + cadence: Option, +) -> Timeline { + let peak = peaked_ns(previous, now, interval); + let target = next_target_ns( + now, + previous_target(previous, now), + paced_ns(cadence, interval, peak), + restart_wanted(method, interval_changed(previous, interval)), + ); + Timeline { + target, + interval, + last: now.max(target), + peak, + } +} + +fn call_sleep_ns(ns: u64) { + match ns { + 0 => (), + n => thread::sleep(Duration::from_nanos(n)), + } +} + +fn call_spin_until(target: u64) { + std::iter::repeat(()) + .take_while(|_| call_now_ns() < target) + .for_each(|_| std::hint::spin_loop()); +} + +fn call_sleep_until(target: u64) { + call_sleep_ns(wait_deficit_ns(call_now_ns(), target)); +} + +fn call_precise_until(target: u64) { + call_sleep_ns(wait_deficit_ns(call_now_ns(), target.saturating_sub(SPIN_MARGIN_NS))); + call_spin_until(target); +} + +fn call_slice_until(target: u64) { + std::iter::repeat(()) + .take_while(|_| call_now_ns() + SLICE_MARGIN_NS < target) + .for_each(|_| call_sleep_ns(slice_length_ns(wait_deficit_ns(call_now_ns(), target)))); + call_spin_until(target); +} + +fn call_wait_until(target: u64, pacing: PacingChoice) { + match pacing { + PacingChoice::Sleep => call_sleep_until(target), + PacingChoice::Sliced => call_slice_until(target), + PacingChoice::Precise => call_precise_until(target), + PacingChoice::Spin => call_spin_until(target), + } +} + +fn present_keys(info: *const vk::PresentInfoKHR<'_>) -> Vec { + (0..unsafe { (*info).swapchain_count } as usize) + .map(|at| unsafe { (*(*info).p_swapchains.add(at)).as_raw() }) + .collect() +} + +fn call_stored(map: &mut TimelineMap, key: (u64, u64), next: Timeline) -> u64 { + map.insert(key, next); + next.target +} + +fn call_advanced_in( + map: &mut TimelineMap, + key: (u64, u64), + interval: u64, + method: Option, + cadence: Option, +) -> u64 { + call_stored( + map, + key, + advanced( + map.get(&key).copied(), + call_now_ns(), + interval, + method, + cadence, + ), + ) +} + +fn call_frame_target( + key: (u64, u64), + interval: u64, + method: Option, + cadence: Option, +) -> u64 { + match TIMELINES.lock() { + Ok(mut guard) => call_advanced_in( + guard.get_or_insert_with(HashMap::new), + key, + interval, + method, + cadence, + ), + Err(_) => call_now_ns(), + } +} + +fn call_latest_target( + dev: u64, + keys: Vec, + interval: u64, + method: Option, + cadence: Option, +) -> u64 { + keys.into_iter() + .map(|key| call_frame_target((dev, key), interval, method, cadence)) + .max() + .unwrap_or_else(call_now_ns) +} + +fn call_limit_to( + dev: u64, + keys: Vec, + fps: f32, + pacing: PacingChoice, + method: Option, + cadence: Option, +) { + call_wait_until( + call_latest_target(dev, keys, target_interval_ns(fps), method, cadence), + pacing, + ); +} + +fn pacing_or_default(pacing: Option) -> PacingChoice { + match pacing { + Some(choice) => choice, + None => PacingChoice::Sleep, + } +} + +fn stage_wanted(method: Option) -> LimitStage { + match method { + Some(MethodChoice::Late) => LimitStage::After, + Some(MethodChoice::Early) | Some(MethodChoice::Reactive) | None => LimitStage::Before, + } +} + +fn limit_fps(s: &Settings, stage: LimitStage) -> Option { + s.frame_limit + .filter(|_| stage_wanted(s.limit_method) == stage) +} + +pub(crate) fn call_limit_frame( + stage: LimitStage, + s: &Settings, + dev: u64, + info: *const vk::PresentInfoKHR<'_>, +) { + match limit_fps(s, stage) { + Some(fps) => call_limit_to( + dev, + present_keys(info), + fps, + pacing_or_default(s.pacing), + s.limit_method, + s.cadence, + ), + None => (), + } +} + +pub(crate) fn call_forget_timeline(dev: u64, sc: vk::SwapchainKHR) { + match TIMELINES.lock() { + Ok(mut guard) => { + guard + .get_or_insert_with(HashMap::new) + .remove(&(dev, sc.as_raw())); + } + Err(_) => (), + } +} + +pub(crate) fn call_forget_device_timelines(dev: u64) { + match TIMELINES.lock() { + Ok(mut guard) => guard + .get_or_insert_with(HashMap::new) + .retain(|(owner, _), _| *owner != dev), + Err(_) => (), + } +} + +pub(crate) fn call_present_frame( + dev: &VkDevState, + queue: vk::Queue, + info: *const vk::PresentInfoKHR<'_>, +) -> vk::Result { + match rebuilt_present(dev.device.handle().as_raw(), unsafe { &*info }) { + Some(built) => unsafe { (dev.swap_fp.queue_present_khr)(queue, &built.info) }, + None => unsafe { (dev.swap_fp.queue_present_khr)(queue, info) }, + } +} diff --git a/src/volt/probe.rs b/src/volt/probe.rs new file mode 100644 index 0000000..a1f66b1 --- /dev/null +++ b/src/volt/probe.rs @@ -0,0 +1,289 @@ +use std::ffi::CStr; +use std::fs; +use std::path::PathBuf; +use std::sync::Mutex; + +use ash::vk; + +use crate::config::config_dir; +use crate::consts::LINE_END; +use crate::consts::PROBE_FAIL_WARN; +use crate::consts::PROBE_KEY_ALPHA_TO_ONE; +use crate::consts::PROBE_KEY_COMPOSITE_ALPHAS; +use crate::consts::PROBE_KEY_DEPTH_CLAMP; +use crate::consts::PROBE_KEY_DEVICE_INDEX; +use crate::consts::PROBE_KEY_DEVICE_NAMES; +use crate::consts::PROBE_KEY_MAX_ANISOTROPY; +use crate::consts::PROBE_KEY_MAX_IMAGE_COUNT; +use crate::consts::PROBE_KEY_MAX_LOD_BIAS; +use crate::consts::PROBE_KEY_MAX_LOD_LEVEL; +use crate::consts::PROBE_KEY_MIN_IMAGE_COUNT; +use crate::consts::PROBE_KEY_PRESENT_MODES; +use crate::consts::PROBE_KEY_SAMPLER_ANISOTROPY; +use crate::consts::PROBE_KEY_SAMPLE_RATE_SHADING; +use crate::consts::PROBE_PAIR_CLOSE; +use crate::consts::PROBE_PAIR_OPEN; +use crate::consts::PROBE_FILE; +use crate::consts::PROBE_OFF; +use crate::consts::PROBE_ON; +use crate::consts::PROBE_SECTION; +use crate::consts::PROBE_SECTION_CLOSE; +use crate::consts::PROBE_SECTION_OPEN; +use crate::consts::PROBE_SEP; +use crate::consts::PROBE_TEMP; +use crate::consts::PROBE_WRITE_INFO; +use crate::device::DeviceCaps; +use crate::instance::call_all_devices; +use crate::instance::device_index; +use crate::instance::VkInstState; +use crate::logging::call_log_at; +use crate::logging::LogLevel; +use crate::ranks::alpha_display; +use crate::ranks::alpha_semantic; +use crate::ranks::present_display; +use crate::ranks::present_semantic; + +pub(crate) struct DeviceFacts { + pub(crate) index: u32, + pub(crate) names: Vec, + pub(crate) max_anisotropy: f32, + pub(crate) max_lod_bias: f32, + pub(crate) max_lod_level: f32, + pub(crate) anisotropy: bool, + pub(crate) shading: bool, + pub(crate) alpha_one: bool, + pub(crate) clamp: bool, +} + +pub(crate) struct SurfaceFacts { + pub(crate) present: Vec, + pub(crate) alphas: Vec, + pub(crate) min_images: u32, + pub(crate) max_images: u32, +} + +struct ProbeState { + device: Option, + surfaces: Vec<(&'static str, SurfaceFacts)>, +} + +static STATE: Mutex> = Mutex::new(None); + +fn empty_state() -> ProbeState { + ProbeState { + device: None, + surfaces: Vec::new(), + } +} + +fn unique_sorted(mut values: Vec) -> Vec { + values.sort(); + values.dedup(); + values +} + +fn call_device_features( + inst: &VkInstState, + phys: vk::PhysicalDevice, +) -> vk::PhysicalDeviceFeatures { + unsafe { inst.instance.get_physical_device_features(phys) } +} + +fn feature_held(flag: vk::Bool32) -> bool { + flag == vk::TRUE +} + +fn call_device_name(inst: &VkInstState, phys: vk::PhysicalDevice) -> String { + let props = unsafe { inst.instance.get_physical_device_properties(phys) }; + unsafe { CStr::from_ptr(props.device_name.as_ptr()) } + .to_string_lossy() + .to_lowercase() +} + +fn call_device_names(inst: &VkInstState, all: &[vk::PhysicalDevice]) -> Vec { + all.iter().map(|p| call_device_name(inst, *p)).collect() +} + +fn present_names(supported: &[vk::PresentModeKHR]) -> Vec { + unique_sorted(supported.iter().map(|m| m.as_raw() as u32).collect()) + .into_iter() + .map(|raw| vk::PresentModeKHR::from_raw(raw as i32)) + .filter(|mode| present_semantic(*mode).is_some_and(|facts| facts.floor)) + .map(present_display) + .collect() +} + +fn set_bits(mask: u32) -> Vec { + (0..u32::BITS) + .map(|shift| 1u32 << shift) + .filter(|bit| mask & *bit != 0) + .collect() +} + +fn alpha_names(mask: u32) -> Vec { + set_bits(mask) + .into_iter() + .map(vk::CompositeAlphaFlagsKHR::from_raw) + .filter(|bit| alpha_semantic(*bit).is_some()) + .map(alpha_display) + .collect() +} + +fn joined(items: &[String]) -> String { + items.join(PROBE_SEP) +} + +fn flag_text(value: bool) -> &'static str { + match value { + true => PROBE_ON, + false => PROBE_OFF, + } +} + +fn pair(key: &str, value: &str) -> String { + [key, PROBE_PAIR_OPEN, value, PROBE_PAIR_CLOSE].concat() +} + +fn section_head(tag: &str) -> String { + [PROBE_SECTION_OPEN, tag, PROBE_SECTION_CLOSE, LINE_END].concat() +} + +pub(crate) fn call_build_device( + inst: &VkInstState, + phys: vk::PhysicalDevice, + caps: &DeviceCaps, +) -> DeviceFacts { + let all = call_all_devices(inst); + let features = call_device_features(inst, phys); + DeviceFacts { + index: device_index(&all, phys), + names: call_device_names(inst, &all), + max_anisotropy: caps.max_anisotropy, + max_lod_bias: caps.max_lod_bias, + max_lod_level: caps.max_lod_level, + anisotropy: feature_held(features.sampler_anisotropy), + shading: feature_held(features.sample_rate_shading), + alpha_one: feature_held(features.alpha_to_one), + clamp: feature_held(features.depth_clamp), + } +} + +pub(crate) fn build_surface( + supported: &[vk::PresentModeKHR], + caps: &vk::SurfaceCapabilitiesKHR, +) -> SurfaceFacts { + SurfaceFacts { + present: present_names(supported), + alphas: alpha_names(caps.supported_composite_alpha.as_raw()), + min_images: caps.min_image_count, + max_images: caps.max_image_count, + } +} + +fn render_device(d: &DeviceFacts) -> String { + [ + PROBE_SECTION.to_string(), + LINE_END.to_string(), + pair(PROBE_KEY_DEVICE_INDEX, &d.index.to_string()), + pair(PROBE_KEY_DEVICE_NAMES, &joined(&d.names)), + pair(PROBE_KEY_MAX_ANISOTROPY, &d.max_anisotropy.to_string()), + pair(PROBE_KEY_MAX_LOD_BIAS, &d.max_lod_bias.to_string()), + pair(PROBE_KEY_MAX_LOD_LEVEL, &d.max_lod_level.to_string()), + pair(PROBE_KEY_SAMPLER_ANISOTROPY, flag_text(d.anisotropy)), + pair(PROBE_KEY_SAMPLE_RATE_SHADING, flag_text(d.shading)), + pair(PROBE_KEY_ALPHA_TO_ONE, flag_text(d.alpha_one)), + pair(PROBE_KEY_DEPTH_CLAMP, flag_text(d.clamp)), + ] + .concat() +} + +pub(crate) fn render_surface(tag: &str, s: &SurfaceFacts) -> String { + [ + section_head(tag), + pair(PROBE_KEY_PRESENT_MODES, &joined(&s.present)), + pair(PROBE_KEY_COMPOSITE_ALPHAS, &joined(&s.alphas)), + pair(PROBE_KEY_MIN_IMAGE_COUNT, &s.min_images.to_string()), + pair(PROBE_KEY_MAX_IMAGE_COUNT, &s.max_images.to_string()), + ] + .concat() +} + +fn render_sections(state: &ProbeState) -> Vec { + state + .device + .iter() + .map(render_device) + .chain( + state + .surfaces + .iter() + .map(|(tag, facts)| render_surface(tag, facts)), + ) + .collect() +} + +fn render(state: &ProbeState) -> String { + render_sections(state).join(LINE_END) +} + +fn call_unchanged(path: &PathBuf, text: &str) -> bool { + fs::read_to_string(path) + .map(|old| old == text) + .unwrap_or(false) +} + +fn call_replace_file(text: &str) { + let temp = config_dir().join(PROBE_TEMP); + match fs::write(&temp, text).and_then(|()| fs::rename(&temp, config_dir().join(PROBE_FILE))) { + Ok(()) => call_log_at(LogLevel::Info, PROBE_WRITE_INFO), + Err(_) => call_log_at(LogLevel::Warn, PROBE_FAIL_WARN), + } +} + +fn call_write_file(text: &str) { + let _ = fs::create_dir_all(config_dir()); + match call_unchanged(&config_dir().join(PROBE_FILE), text) { + true => (), + false => call_replace_file(text), + } +} + +fn placed( + surfaces: Vec<(&'static str, SurfaceFacts)>, + tag: &'static str, + facts: SurfaceFacts, +) -> Vec<(&'static str, SurfaceFacts)> { + surfaces + .into_iter() + .filter(|(name, _)| *name != tag) + .chain(std::iter::once((tag, facts))) + .collect() +} + +fn call_stored(update: F) +where + F: FnOnce(ProbeState) -> ProbeState, +{ + match STATE.lock() { + Ok(mut guard) => { + let next = update(guard.take().unwrap_or_else(empty_state)); + call_write_file(&render(&next)); + *guard = Some(next); + } + Err(_) => (), + } +} + +pub(crate) fn call_record_device(facts: DeviceFacts) { + call_stored(|state| ProbeState { + device: Some(facts), + surfaces: state.surfaces, + }); +} + +pub(crate) fn call_record_surface(tag: &'static str, facts: SurfaceFacts) { + call_stored(|state| ProbeState { + device: state.device, + surfaces: placed(state.surfaces, tag, facts), + }); +} diff --git a/src/volt/ranks.rs b/src/volt/ranks.rs new file mode 100644 index 0000000..a688c32 --- /dev/null +++ b/src/volt/ranks.rs @@ -0,0 +1,86 @@ +use ash::vk; + +use crate::consts::ALPHA_UNKNOWN_PREFIX; +use crate::consts::PRESENT_UNKNOWN_PREFIX; + +const PRESENT_MODES: [(i32, &str, bool, bool); 7] = [ + (0, "immediate", true, false), + (1, "mailbox", true, false), + (2, "fifo", true, false), + (3, "fifo_relaxed", true, false), + (1000111000, "shared_demand_refresh", false, true), + (1000111001, "shared_continuous_refresh", false, true), + (1000361000, "fifo_latest_ready", false, false), +]; + +const COMPOSITE_ALPHAS: [(u32, &str, bool); 4] = [ + (1, "opaque", false), + (2, "pre_multiplied", true), + (4, "post_multiplied", true), + (8, "inherit", true), +]; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct PresentFacts { + pub(crate) floor: bool, + pub(crate) shared: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct AlphaFacts { + pub(crate) blends: bool, +} + +fn unknown_display(prefix: &str, value: i64) -> String { + format!("{}{}", prefix, value) +} + +fn present_row(value: i32) -> Option<(i32, &'static str, bool, bool)> { + PRESENT_MODES + .iter() + .copied() + .find(|(known, _, _, _)| *known == value) +} + +fn alpha_row(value: u32) -> Option<(u32, &'static str, bool)> { + COMPOSITE_ALPHAS + .iter() + .copied() + .find(|(known, _, _)| *known == value) +} + +pub(crate) fn present_display(value: vk::PresentModeKHR) -> String { + match present_row(value.as_raw()) { + Some((_, name, _, _)) => name.to_string(), + None => unknown_display(PRESENT_UNKNOWN_PREFIX, value.as_raw() as i64), + } +} + +pub(crate) fn present_parse(text: &str) -> Option { + PRESENT_MODES + .iter() + .find(|(_, name, floor, _)| *name == text && *floor) + .map(|(value, _, _, _)| vk::PresentModeKHR::from_raw(*value)) +} + +pub(crate) fn present_semantic(value: vk::PresentModeKHR) -> Option { + present_row(value.as_raw()).map(|(_, _, floor, shared)| PresentFacts { floor, shared }) +} + +pub(crate) fn alpha_display(value: vk::CompositeAlphaFlagsKHR) -> String { + match alpha_row(value.as_raw()) { + Some((_, name, _)) => name.to_string(), + None => unknown_display(ALPHA_UNKNOWN_PREFIX, value.as_raw() as i64), + } +} + +pub(crate) fn alpha_parse(text: &str) -> Option { + COMPOSITE_ALPHAS + .iter() + .find(|(_, name, _)| *name == text) + .map(|(value, _, _)| vk::CompositeAlphaFlagsKHR::from_raw(*value)) +} + +pub(crate) fn alpha_semantic(value: vk::CompositeAlphaFlagsKHR) -> Option { + alpha_row(value.as_raw()).map(|(_, _, blends)| AlphaFacts { blends }) +} diff --git a/src/volt/report.rs b/src/volt/report.rs new file mode 100644 index 0000000..41ee7b2 --- /dev/null +++ b/src/volt/report.rs @@ -0,0 +1,187 @@ +use std::collections::HashMap; +use std::collections::HashSet; +use std::sync::RwLock; + +use ash::vk; + +use crate::consts::FILTER_LINEAR; +use crate::consts::FILTER_NEAREST; +use crate::consts::FILTER_UNKNOWN_PREFIX; +use crate::consts::MIPMAP_LINEAR; +use crate::consts::MIPMAP_NEAREST; +use crate::consts::MIPMAP_UNKNOWN_PREFIX; +use crate::consts::NOTE_NOT_ENABLED; +use crate::consts::NOTE_NOT_SET; +use crate::consts::REPORT_ASKED; +use crate::consts::REPORT_APPLIED; +use crate::consts::REPORT_MARK; +use crate::consts::REPORT_SEP; +use crate::consts::TEXT_LINEAR; +use crate::consts::TEXT_NEAREST; +use crate::consts::TEXT_OFF; +use crate::consts::TEXT_ON; +use crate::consts::TOGGLE_ON; +use crate::logging::info_wanted; +use crate::logging::call_log_at; +use crate::logging::LogLevel; + +type ReportKey = (&'static str, Option, Option); +type ReportMap = HashMap>; + +static REPORTS: RwLock> = RwLock::new(None); + +fn call_claim_report(owner: u64, key: ReportKey) -> bool { + match REPORTS.write() { + Ok(mut guard) => guard + .get_or_insert_with(HashMap::new) + .entry(owner) + .or_insert_with(HashSet::new) + .insert(key), + Err(_) => false, + } +} + +fn call_forget_owner(owner: u64) { + match REPORTS.write() { + Ok(mut guard) => { + guard.get_or_insert_with(HashMap::new).remove(&owner); + } + Err(_) => (), + } +} + +pub(crate) fn call_forget_reports(owner: u64) { + match info_wanted() { + true => call_forget_owner(owner), + false => (), + } +} + +fn labelled(label: &str, text: Option) -> Option { + text.map(|value| format!("{}{}", label, value)) +} + +fn values(asked: Option, applied: Option) -> String { + [labelled(REPORT_ASKED, asked), labelled(REPORT_APPLIED, applied)] + .into_iter() + .flatten() + .collect::>() + .join(REPORT_SEP) +} + +fn noted(body: String, note: Option) -> String { + match note { + Some(text) => text, + None => body, + } +} + +pub(crate) fn report_line( + name: &str, + asked: Option, + forced: Option, + note: Option, +) -> String { + format!( + "{}{}{}", + name, + REPORT_MARK, + noted(values(asked, forced), note) + ) +} + +pub(crate) fn call_report_setting( + name: &str, + asked: Option, + forced: Option, + note: Option, +) { + call_log_at(LogLevel::Info, &report_line(name, asked, forced, note)); +} + +pub(crate) fn feature_note(set: bool, held: bool, feature: &str) -> Option { + match (set, held) { + (true, false) => Some(format!("{}{}", NOTE_NOT_ENABLED, feature)), + (_, _) => None, + } +} + +fn missing_note(forced: &Option) -> Option { + match forced { + Some(_) => None, + None => Some(NOTE_NOT_SET.into()), + } +} + +pub(crate) fn applied_text(held: T, text: fn(T) -> String) -> String { + text(held) +} + +pub(crate) fn call_report_value( + owner: u64, + name: &'static str, + asked: T, + held: T, + text: fn(T) -> String, + note: Option, +) { + let asked_text = text(asked); + let held_text = applied_text(held, text); + match call_claim_report(owner, (name, Some(asked_text.clone()), Some(held_text.clone()))) { + true => call_report_setting(name, Some(asked_text), Some(held_text), note), + false => (), + } +} + +pub(crate) fn call_report_choice(owner: u64, name: &'static str, forced: Option) { + let note = missing_note(&forced); + match call_claim_report(owner, (name, None, forced.clone())) { + true => call_report_setting(name, None, forced, note), + false => (), + } +} + +pub(crate) fn call_report_reading(owner: u64, name: &'static str, asked: String) { + match call_claim_report(owner, (name, Some(asked.clone()), None)) { + true => call_report_setting(name, Some(asked), None, None), + false => (), + } +} + +pub(crate) fn call_report_reason(name: &str, reason: Option<&str>) { + match reason { + Some(text) => call_report_setting(name, None, None, Some(text.into())), + None => (), + } +} + +pub(crate) fn number_text(value: f32) -> String { + format!("{}", value) +} + +pub(crate) fn count_text(value: u32) -> String { + value.to_string() +} + +pub(crate) fn toggle_text(value: u32) -> String { + match value { + TOGGLE_ON => TEXT_ON.into(), + _ => TEXT_OFF.into(), + } +} + +pub(crate) fn filter_text(value: vk::Filter) -> String { + match value.as_raw() { + FILTER_NEAREST => TEXT_NEAREST.into(), + FILTER_LINEAR => TEXT_LINEAR.into(), + other => format!("{}{}", FILTER_UNKNOWN_PREFIX, other), + } +} + +pub(crate) fn mipmap_text(value: vk::SamplerMipmapMode) -> String { + match value.as_raw() { + MIPMAP_NEAREST => TEXT_NEAREST.into(), + MIPMAP_LINEAR => TEXT_LINEAR.into(), + other => format!("{}{}", MIPMAP_UNKNOWN_PREFIX, other), + } +} diff --git a/src/volt/sampler.rs b/src/volt/sampler.rs new file mode 100644 index 0000000..423a9ef --- /dev/null +++ b/src/volt/sampler.rs @@ -0,0 +1,719 @@ +use std::ffi::c_void; + +use ash::vk; +use ash::vk::Handle; + +use crate::config::ensure_settings; +use crate::config::Settings; +use crate::consts::ANISO_OFF; +use crate::consts::CHAIN_SAMPLER_LABEL; +use crate::consts::CUBIC_REASON; +use crate::consts::FEATURE_ANISOTROPY; +use crate::consts::FILTER_CUBIC; +use crate::consts::LINEAR_REASON; +use crate::consts::MIP_CROSS_REASON; +use crate::consts::PORTABILITY_REASON; +use crate::consts::SAMPLER_IMAGE_PROCESSING_BIT; +use crate::consts::SAMPLER_SHAPE_REASON; +use crate::consts::SAMPLER_SUBSAMPLED_BIT; +use crate::consts::SAMPLER_YCBCR_CONVERSION_INFO_TYPE; +use crate::consts::SETTING_ANISOTROPY; +use crate::consts::SETTING_LOD_BIAS; +use crate::consts::SETTING_MAG_FILTER; +use crate::consts::SETTING_MIN_FILTER; +use crate::consts::SETTING_MIP_CEILING; +use crate::consts::SETTING_MIP_FLOOR; +use crate::consts::SETTING_MIPMAP_MODE; +use crate::consts::SHADER_MAPPING_INFO_TYPE; +use crate::consts::SOURCE_CONSTANT_OFFSET; +use crate::consts::SOURCE_INDIRECT_INDEX; +use crate::consts::SOURCE_INDIRECT_INDEX_ARRAY; +use crate::consts::SOURCE_PUSH_INDEX; +use crate::consts::SOURCE_SHADER_RECORD_INDEX; +use crate::consts::TEXT_OFF; +use crate::device::DeviceCaps; +use crate::device::VkDevState; +use crate::instance::call_relinked_chain; +use crate::instance::chain_find; +use crate::instance::PfnWriteSamplers; +use crate::instance::Relinked; +use crate::instance::VkDescriptorMappingSourceDataEXT; +use crate::instance::VkDescriptorSetAndBindingMappingEXT; +use crate::instance::VkShaderDescriptorSetAndBindingMappingInfoEXT; +use crate::lists::forced; +use crate::logging::info_wanted; +use crate::report::call_report_reason; +use crate::report::call_report_value; +use crate::report::feature_note; +use crate::report::filter_text; +use crate::report::mipmap_text; +use crate::report::number_text; + + +fn uses_cubic(original: &vk::SamplerCreateInfo<'_>) -> bool { + original.mag_filter.as_raw() == FILTER_CUBIC || original.min_filter.as_raw() == FILTER_CUBIC +} + +fn aniso_allowed(choice: Option, caps: &DeviceCaps, cubic: bool) -> Option { + match (choice, caps.sampler_anisotropy, cubic) { + (None, _, _) => None, + (Some(_), _, true) => None, + (Some(level), true, false) => Some(level.min(caps.max_anisotropy)), + (Some(_), false, false) => None, + } +} + +fn aniso_pair(level: f32) -> (vk::Bool32, f32) { + match level > ANISO_OFF { + true => (vk::TRUE, level), + false => (vk::FALSE, ANISO_OFF), + } +} + +fn pick_aniso( + choice: Option, + caps: &DeviceCaps, + original: &vk::SamplerCreateInfo<'_>, +) -> (vk::Bool32, f32) { + match aniso_allowed(choice, caps, uses_cubic(original)) { + Some(level) => aniso_pair(level), + None => (original.anisotropy_enable, original.max_anisotropy), + } +} + +fn pick_lod_bias(choice: Option, caps: &DeviceCaps, original: f32) -> f32 { + match (choice, caps.portability_subset) { + (Some(value), false) => value.clamp(-caps.max_lod_bias, caps.max_lod_bias), + (_, _) => original, + } +} + +fn landed(value: f32, held: bool) -> Option { + match held { + true => Some(value), + false => None, + } +} + +fn landed_floor(choice: Option, against: f32) -> Option { + choice.and_then(|value| landed(value, value <= against)) +} + +fn landed_ceiling(choice: Option, against: f32) -> Option { + choice.and_then(|value| landed(value, value >= against)) +} + +fn pick_lod_range(s: &Settings, original: (f32, f32)) -> (f32, f32) { + let low = forced( + landed_floor(s.mip_floor, forced(s.mip_ceiling, original.1)), + original.0, + ); + let high = forced(landed_ceiling(s.mip_ceiling, low), original.1); + (low, high) +} + +fn already_linear(original: &vk::SamplerCreateInfo<'_>) -> bool { + original.mag_filter == vk::Filter::LINEAR + || original.min_filter == vk::Filter::LINEAR + || original.mipmap_mode == vk::SamplerMipmapMode::LINEAR +} + +fn linear_filter_choice(choice: Option, linear: bool) -> Option { + match choice { + Some(value) if value == vk::Filter::LINEAR && !linear => None, + held => held, + } +} + +fn linear_mipmap_choice( + choice: Option, + linear: bool, +) -> Option { + match choice { + Some(value) if value == vk::SamplerMipmapMode::LINEAR && !linear => None, + held => held, + } +} + +fn restricted_shape(original: &vk::SamplerCreateInfo<'_>) -> bool { + original.flags.as_raw() & SAMPLER_SUBSAMPLED_BIT != 0 + || original.flags.as_raw() & SAMPLER_IMAGE_PROCESSING_BIT != 0 + || original.unnormalized_coordinates == vk::TRUE +} + +fn converted_sampler(original: &vk::SamplerCreateInfo<'_>) -> bool { + chain_find(original.p_next, SAMPLER_YCBCR_CONVERSION_INFO_TYPE).is_some() +} + +fn shape_choice(choice: Option, restricted: bool) -> Option { + match restricted { + true => None, + false => choice, + } +} + +fn shaped_range(s: &Settings, restricted: bool, original: (f32, f32)) -> (f32, f32) { + pick_lod_range( + &Settings { + mip_floor: shape_choice(s.mip_floor, restricted), + mip_ceiling: shape_choice(s.mip_ceiling, restricted), + ..Default::default() + }, + original, + ) +} + +fn patched_ci<'a>( + s: &Settings, + caps: &DeviceCaps, + original: &vk::SamplerCreateInfo<'a>, +) -> vk::SamplerCreateInfo<'a> { + let shape = restricted_shape(original); + let filters = shape || converted_sampler(original); + let linear = already_linear(original); + let (aniso_enable, aniso_max) = pick_aniso( + shape_choice(s.anisotropy, filters), + caps, + original, + ); + let (lod_low, lod_high) = shaped_range(s, shape, (original.min_lod, original.max_lod)); + vk::SamplerCreateInfo { + mag_filter: forced( + linear_filter_choice(shape_choice(s.mag_filter, filters), linear), + original.mag_filter, + ), + min_filter: forced( + linear_filter_choice(shape_choice(s.min_filter, filters), linear), + original.min_filter, + ), + mipmap_mode: forced( + linear_mipmap_choice(shape_choice(s.mipmap, shape), linear), + original.mipmap_mode, + ), + anisotropy_enable: aniso_enable, + max_anisotropy: aniso_max, + mip_lod_bias: pick_lod_bias(s.lod_bias, caps, original.mip_lod_bias), + min_lod: lod_low, + max_lod: lod_high, + ..*original + } +} + +fn aniso_text(level: f32) -> String { + match level > ANISO_OFF { + true => number_text(level), + false => TEXT_OFF.into(), + } +} + +fn aniso_of(enable: vk::Bool32, level: f32) -> f32 { + match enable == vk::TRUE { + true => level, + false => ANISO_OFF, + } +} + +fn call_report_fields( + owner: u64, + s: &Settings, + caps: &DeviceCaps, + asked: &vk::SamplerCreateInfo<'_>, + held: &vk::SamplerCreateInfo<'_>, +) { + call_report_value( + owner, + SETTING_MAG_FILTER, + asked.mag_filter, + held.mag_filter, + filter_text, + None, + ); + call_report_value( + owner, + SETTING_MIN_FILTER, + asked.min_filter, + held.min_filter, + filter_text, + None, + ); + call_report_value( + owner, + SETTING_MIPMAP_MODE, + asked.mipmap_mode, + held.mipmap_mode, + mipmap_text, + None, + ); + call_report_value( + owner, + SETTING_ANISOTROPY, + aniso_of(asked.anisotropy_enable, asked.max_anisotropy), + aniso_of(held.anisotropy_enable, held.max_anisotropy), + aniso_text, + feature_note( + s.anisotropy.is_some(), + caps.sampler_anisotropy, + FEATURE_ANISOTROPY, + ), + ); + call_report_value( + owner, + SETTING_LOD_BIAS, + asked.mip_lod_bias, + held.mip_lod_bias, + number_text, + None, + ); + call_report_value( + owner, + SETTING_MIP_FLOOR, + asked.min_lod, + held.min_lod, + number_text, + None, + ); + call_report_value( + owner, + SETTING_MIP_CEILING, + asked.max_lod, + held.max_lod, + number_text, + None, + ); +} + +fn kept_reason(set: bool, reasons: &[(bool, &'static str)]) -> Option<&'static str> { + match set { + true => reasons.iter().find(|entry| entry.0).map(|entry| entry.1), + false => None, + } +} + +fn filter_dropped(choice: Option, linear: bool) -> bool { + linear_filter_choice(choice, linear) != choice +} + +fn mipmap_dropped(choice: Option, linear: bool) -> bool { + linear_mipmap_choice(choice, linear) != choice +} + +fn floor_crossed(s: &Settings, asked: &vk::SamplerCreateInfo<'_>) -> bool { + landed_floor(s.mip_floor, forced(s.mip_ceiling, asked.max_lod)).is_none() +} + +fn ceiling_crossed(s: &Settings, asked: &vk::SamplerCreateInfo<'_>) -> bool { + landed_ceiling( + s.mip_ceiling, + forced( + landed_floor(s.mip_floor, forced(s.mip_ceiling, asked.max_lod)), + asked.min_lod, + ), + ) + .is_none() +} + +fn filter_reason( + choice: Option, + filters: bool, + linear: bool, +) -> Option<&'static str> { + kept_reason( + choice.is_some(), + &[ + (filters, SAMPLER_SHAPE_REASON), + (filter_dropped(choice, linear), LINEAR_REASON), + ], + ) +} + +fn call_report_filter_reasons(s: &Settings, asked: &vk::SamplerCreateInfo<'_>) { + let shape = restricted_shape(asked); + let filters = shape || converted_sampler(asked); + let linear = already_linear(asked); + call_report_reason(SETTING_MAG_FILTER, filter_reason(s.mag_filter, filters, linear)); + call_report_reason(SETTING_MIN_FILTER, filter_reason(s.min_filter, filters, linear)); + call_report_reason( + SETTING_MIPMAP_MODE, + kept_reason( + s.mipmap.is_some(), + &[ + (shape, SAMPLER_SHAPE_REASON), + (mipmap_dropped(s.mipmap, linear), LINEAR_REASON), + ], + ), + ); +} + +fn call_report_level_reasons( + s: &Settings, + caps: &DeviceCaps, + asked: &vk::SamplerCreateInfo<'_>, +) { + let shape = restricted_shape(asked); + call_report_reason( + SETTING_ANISOTROPY, + kept_reason( + s.anisotropy.is_some(), + &[ + (shape || converted_sampler(asked), SAMPLER_SHAPE_REASON), + (uses_cubic(asked), CUBIC_REASON), + ], + ), + ); + call_report_reason( + SETTING_LOD_BIAS, + kept_reason( + s.lod_bias.is_some(), + &[(caps.portability_subset, PORTABILITY_REASON)], + ), + ); + call_report_reason( + SETTING_MIP_FLOOR, + kept_reason( + s.mip_floor.is_some(), + &[ + (shape, SAMPLER_SHAPE_REASON), + (floor_crossed(s, asked), MIP_CROSS_REASON), + ], + ), + ); + call_report_reason( + SETTING_MIP_CEILING, + kept_reason( + s.mip_ceiling.is_some(), + &[ + (shape, SAMPLER_SHAPE_REASON), + (ceiling_crossed(s, asked), MIP_CROSS_REASON), + ], + ), + ); +} + +fn call_report_one( + dev: &VkDevState, + asked: &vk::SamplerCreateInfo<'_>, + held: &vk::SamplerCreateInfo<'_>, +) { + call_report_filter_reasons(ensure_settings(), asked); + call_report_level_reasons(ensure_settings(), &dev.caps, asked); + call_report_fields( + dev.device.handle().as_raw(), + ensure_settings(), + &dev.caps, + asked, + held, + ); +} + +fn call_report_sampler( + dev: &VkDevState, + asked: &vk::SamplerCreateInfo<'_>, + held: &vk::SamplerCreateInfo<'_>, +) { + match info_wanted() { + true => call_report_one(dev, asked, held), + false => (), + } +} + +fn call_report_each( + dev: &VkDevState, + cis: *const vk::SamplerCreateInfo<'_>, + count: u32, + held: &[vk::SamplerCreateInfo<'_>], +) { + unsafe { std::slice::from_raw_parts(cis, count as usize) } + .iter() + .zip(held.iter()) + .for_each(|(asked, one)| call_report_one(dev, asked, one)); +} + +fn call_report_samplers( + dev: &VkDevState, + cis: *const vk::SamplerCreateInfo<'_>, + count: u32, + held: &[vk::SamplerCreateInfo<'_>], +) { + match info_wanted() { + true => call_report_each(dev, cis, count, held), + false => (), + } +} + +fn patched_list<'a>( + s: &Settings, + caps: &DeviceCaps, + cis: *const vk::SamplerCreateInfo<'a>, + count: u32, +) -> Vec> { + unsafe { std::slice::from_raw_parts(cis, count as usize) } + .iter() + .map(|original| patched_ci(s, caps, original)) + .collect() +} + +pub(crate) fn call_create_sampler( + dev: &VkDevState, + ci: *const vk::SamplerCreateInfo<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::Sampler, +) -> vk::Result { + let patched = patched_ci(ensure_settings(), &dev.caps, unsafe { &*ci }); + call_report_sampler(dev, unsafe { &*ci }, &patched); + match unsafe { dev.device.create_sampler(&patched, alloc.as_ref()) } { + Ok(sampler) => { + unsafe { *out = sampler }; + vk::Result::SUCCESS + } + Err(e) => e, + } +} + +pub(crate) struct ChainRebuild { + #[allow(dead_code)] + samplers: Vec>>, + #[allow(dead_code)] + mappings: Vec, + #[allow(dead_code)] + info: Vec, + #[allow(dead_code)] + relink: Relinked, + pub(crate) head: *const c_void, +} + +pub(crate) struct StageRebuild { + #[allow(dead_code)] + chain: ChainRebuild, + pub(crate) stage: vk::PipelineShaderStageCreateInfo<'static>, +} + +pub(crate) struct StagesRebuild { + #[allow(dead_code)] + each: Vec>, + pub(crate) stages: Vec>, +} + +fn named_sampler( + mapping: &VkDescriptorSetAndBindingMappingEXT, +) -> Option<*const vk::SamplerCreateInfo<'static>> { + match mapping.source { + SOURCE_CONSTANT_OFFSET => { + Some(unsafe { mapping.source_data.constant_offset.p_embedded_sampler }) + } + SOURCE_PUSH_INDEX => Some(unsafe { mapping.source_data.push_index.p_embedded_sampler }), + SOURCE_INDIRECT_INDEX => { + Some(unsafe { mapping.source_data.indirect_index.p_embedded_sampler }) + } + SOURCE_INDIRECT_INDEX_ARRAY => { + Some(unsafe { mapping.source_data.indirect_index_array.p_embedded_sampler }) + } + SOURCE_SHADER_RECORD_INDEX => { + Some(unsafe { mapping.source_data.shader_record_index.p_embedded_sampler }) + } + _ => None, + } +} + +pub(crate) fn embedded_sampler( + mapping: &VkDescriptorSetAndBindingMappingEXT, +) -> Option<*const vk::SamplerCreateInfo<'static>> { + named_sampler(mapping).filter(|held| !held.is_null()) +} + +fn rebuilt_data( + mapping: &VkDescriptorSetAndBindingMappingEXT, + sampler: *const vk::SamplerCreateInfo<'static>, +) -> VkDescriptorMappingSourceDataEXT { + let mut data = mapping.source_data; + match mapping.source { + SOURCE_CONSTANT_OFFSET => data.constant_offset.p_embedded_sampler = sampler, + SOURCE_PUSH_INDEX => data.push_index.p_embedded_sampler = sampler, + SOURCE_INDIRECT_INDEX => data.indirect_index.p_embedded_sampler = sampler, + SOURCE_INDIRECT_INDEX_ARRAY => data.indirect_index_array.p_embedded_sampler = sampler, + SOURCE_SHADER_RECORD_INDEX => data.shader_record_index.p_embedded_sampler = sampler, + _ => (), + }; + data +} + +fn mapping_list( + info: *const VkShaderDescriptorSetAndBindingMappingInfoEXT, +) -> Vec { + (0..unsafe { (*info).mapping_count } as usize) + .map(|at| unsafe { *(*info).p_mappings.add(at) }) + .collect() +} + +fn call_patched_embedded( + dev: &VkDevState, + original: *const vk::SamplerCreateInfo<'static>, +) -> vk::SamplerCreateInfo<'static> { + let held = patched_ci(ensure_settings(), &dev.caps, unsafe { &*original }); + call_report_sampler(dev, unsafe { &*original }, &held); + held +} + +fn patched_embedded( + dev: &VkDevState, + mapping: &VkDescriptorSetAndBindingMappingEXT, +) -> Option> { + embedded_sampler(mapping).map(|original| call_patched_embedded(dev, original)) +} + +fn patched_samplers( + dev: &VkDevState, + info: *const VkShaderDescriptorSetAndBindingMappingInfoEXT, +) -> Vec>> { + mapping_list(info) + .iter() + .map(|mapping| patched_embedded(dev, mapping)) + .collect() +} + +fn rebuilt_mapping( + mapping: &VkDescriptorSetAndBindingMappingEXT, + sampler: &Option>, +) -> VkDescriptorSetAndBindingMappingEXT { + match sampler { + Some(held) => VkDescriptorSetAndBindingMappingEXT { + source_data: rebuilt_data(mapping, held as *const vk::SamplerCreateInfo<'static>), + ..*mapping + }, + None => *mapping, + } +} + +fn rebuilt_mappings( + info: *const VkShaderDescriptorSetAndBindingMappingInfoEXT, + samplers: &[Option>], +) -> Vec { + mapping_list(info) + .iter() + .zip(samplers.iter()) + .map(|(mapping, sampler)| rebuilt_mapping(mapping, sampler)) + .collect() +} + +fn rebuilt_info( + info: *const VkShaderDescriptorSetAndBindingMappingInfoEXT, + mappings: &[VkDescriptorSetAndBindingMappingEXT], +) -> VkShaderDescriptorSetAndBindingMappingInfoEXT { + VkShaderDescriptorSetAndBindingMappingInfoEXT { + p_mappings: mappings.as_ptr(), + ..unsafe { *info } + } +} + +fn built_chain( + head: *const c_void, + info: *const VkShaderDescriptorSetAndBindingMappingInfoEXT, + samplers: Vec>>, +) -> Option { + let mappings = rebuilt_mappings(info, &samplers); + let node = vec![rebuilt_info(info, &mappings)]; + let relink = call_relinked_chain( + head, + SHADER_MAPPING_INFO_TYPE, + node.as_ptr() as *const c_void, + CHAIN_SAMPLER_LABEL, + )?; + Some(ChainRebuild { + head: relink.head, + samplers, + mappings, + info: node, + relink, + }) +} + +pub(crate) fn rebuilt_mapping_chain(dev: &VkDevState, head: *const c_void) -> Option { + let info = chain_find(head, SHADER_MAPPING_INFO_TYPE)? + as *const VkShaderDescriptorSetAndBindingMappingInfoEXT; + let samplers = patched_samplers(dev, info); + match samplers.iter().any(|one| one.is_some()) { + true => built_chain(head, info, samplers), + false => None, + } +} + +pub(crate) fn rebuilt_stage( + dev: &VkDevState, + p: *const vk::PipelineShaderStageCreateInfo<'static>, +) -> Option { + let chain = rebuilt_mapping_chain(dev, unsafe { (*p).p_next })?; + Some(StageRebuild { + stage: vk::PipelineShaderStageCreateInfo { + p_next: chain.head, + ..unsafe { *p } + }, + chain, + }) +} + +fn stage_values( + each: &[Option], + p: *const vk::PipelineShaderStageCreateInfo<'static>, +) -> Vec> { + each.iter() + .enumerate() + .map(|(at, built)| match built { + Some(one) => one.stage, + None => unsafe { *p.add(at) }, + }) + .collect() +} + +fn built_stages( + each: Vec>, + p: *const vk::PipelineShaderStageCreateInfo<'static>, +) -> Option { + match each.iter().any(|one| one.is_some()) { + true => Some(StagesRebuild { + stages: stage_values(&each, p), + each, + }), + false => None, + } +} + +pub(crate) fn rebuilt_stages( + dev: &VkDevState, + p: *const vk::PipelineShaderStageCreateInfo<'static>, + count: u32, +) -> Option { + match p.is_null() { + true => None, + false => built_stages( + (0..count as usize) + .map(|at| rebuilt_stage(dev, unsafe { p.add(at) })) + .collect(), + p, + ), + } +} + +fn call_samplers_through( + dev: &VkDevState, + fp: PfnWriteSamplers, + handle: vk::Device, + count: u32, + cis: *const vk::SamplerCreateInfo<'_>, + descriptors: *const c_void, +) -> vk::Result { + let patched = patched_list(ensure_settings(), &dev.caps, cis, count); + call_report_samplers(dev, cis, count, &patched); + unsafe { fp(handle, count, patched.as_ptr(), descriptors) } +} + +pub(crate) fn call_write_sampler_descriptors( + dev: &VkDevState, + handle: vk::Device, + count: u32, + cis: *const vk::SamplerCreateInfo<'_>, + descriptors: *const c_void, +) -> vk::Result { + match dev.samplers_fp { + Some(fp) => call_samplers_through(dev, fp, handle, count, cis, descriptors), + None => vk::Result::ERROR_INITIALIZATION_FAILED, + } +} diff --git a/src/volt/swapchain.rs b/src/volt/swapchain.rs new file mode 100644 index 0000000..26f3145 --- /dev/null +++ b/src/volt/swapchain.rs @@ -0,0 +1,1069 @@ +use std::collections::HashMap; +use std::collections::HashSet; +use std::ffi::c_void; +use std::ptr; +use std::sync::Mutex; + +use ash::vk; +use ash::vk::Handle; + +use crate::config::ensure_settings; +use crate::config::Settings; +use crate::consts::ALPHA_MISS_WARN; +use crate::consts::ALPHA_OPAQUE_INFO; +use crate::consts::COUNT_SHARED_REASON; +use crate::consts::EXT_GET_SURFACE_CAPS_2; +use crate::consts::EXT_SURFACE_MAINTENANCE_1; +use crate::consts::EXT_SURFACE_MAINTENANCE_1_EXT; +use crate::consts::LOG_SWAPCHAIN_CREATED; +use crate::consts::MODE_COMPATIBILITY_TYPE; +use crate::consts::MODE_LIST_TYPES; +use crate::consts::NO_FLAGS; +use crate::consts::NO_IMAGE_LIMIT; +use crate::consts::PRESENT_EMPTY_WARN; +use crate::consts::PRESENT_ABOVE_FLOOR_REASON; +use crate::consts::PRESENT_LIST_REASON; +use crate::consts::PRESENT_MISS_WARN; +use crate::consts::PRESENT_TIE_REASON; +use crate::consts::SETTING_CLIPPED; +use crate::consts::SETTING_COMPOSITE_ALPHA; +use crate::consts::SETTING_FRAME_LIMIT; +use crate::consts::SETTING_FRAME_LIMIT_CADENCE; +use crate::consts::SETTING_FRAME_LIMIT_METHOD; +use crate::consts::SETTING_FRAME_PACING; +use crate::consts::SETTING_IMAGE_COUNT; +use crate::consts::SETTING_PRESENT_MODE; +use crate::consts::SURFACE_CAPABILITIES_2_TYPE; +use crate::consts::SURFACE_INFO_2_TYPE; +use crate::consts::SURFACE_PRESENT_MODE_TYPE; +use crate::consts::SWAPCHAIN_MODE_LIST_TYPE; +use crate::consts::SWAPCHAIN_PRESENT_MODE_INFO_TYPE; +use crate::consts::SWAPCHAIN_PRESENT_SCALING_TYPE; +use crate::device::VkDevState; +use crate::env::env_probe_active; +use crate::instance::call_relinked_chain; +use crate::instance::call_write_list; +use crate::instance::chain_find; +use crate::instance::filled_nodes; +use crate::instance::insts_get; +use crate::instance::owning_instance; +use crate::instance::surface_tag; +use crate::instance::PfnCreateSharedSwapchains; +use crate::instance::PfnSurfaceCaps2; +use crate::instance::VkChainNode; +use crate::instance::VkInstState; +use crate::instance::VkPhysicalDeviceSurfaceInfo2; +use crate::instance::Relinked; +use crate::instance::VkPresentModeList; +use crate::instance::VkSurfaceCapabilities2; +use crate::instance::VkSurfacePresentModeKHR; +use crate::instance::VkSwapchainPresentModeInfoKHR; +use crate::instance::VkSwapchainPresentModesCreateInfoKHR; +use crate::instance::VkSwapchainPresentScalingCreateInfoKHR; +use crate::lists::call_warned; +use crate::lists::forced; +use crate::lists::untouched; +use crate::lists::Narrowed; +use crate::logging::info_wanted; +use crate::logging::call_log_at; +use crate::logging::LogLevel; +use crate::present::cadence_display; +use crate::present::method_display; +use crate::present::pacing_display; +use crate::probe::build_surface; +use crate::probe::call_record_surface; +use crate::ranks::alpha_display; +use crate::ranks::alpha_semantic; +use crate::ranks::present_display; +use crate::ranks::present_semantic; +use crate::report::call_report_choice; +use crate::report::call_report_reason; +use crate::report::call_report_value; +use crate::report::count_text; +use crate::report::number_text; +use crate::report::toggle_text; + +fn on_floor(mode: vk::PresentModeKHR) -> bool { + present_semantic(mode).is_some_and(|facts| facts.floor) +} + +fn present_kept(mode: vk::PresentModeKHR, choice: vk::PresentModeKHR) -> bool { + match on_floor(mode) { + true => mode == choice, + false => true, + } +} + +fn floor_survived(modes: &[vk::PresentModeKHR]) -> bool { + modes.iter().copied().any(on_floor) +} + +fn restored_modes( + narrowed: Vec, + modes: Vec, +) -> Narrowed { + match floor_survived(&narrowed) { + true => untouched(narrowed), + false => Narrowed { + items: modes, + restored: true, + }, + } +} + +pub(crate) fn present_narrowed( + modes: Vec, + choice: Option, +) -> Narrowed { + match choice { + None => untouched(modes), + Some(value) => restored_modes( + modes + .iter() + .copied() + .filter(|mode| present_kept(*mode, value)) + .collect(), + modes, + ), + } +} + +fn call_present_filtered( + modes: Vec, + choice: Option, +) -> Vec { + call_warned(present_narrowed(modes, choice), PRESENT_EMPTY_WARN) +} + +fn supported_mode( + supported: &[vk::PresentModeKHR], + value: vk::PresentModeKHR, +) -> Option { + supported.iter().copied().find(|m| *m == value) +} + +fn logged_mode_miss(original: vk::PresentModeKHR) -> vk::PresentModeKHR { + call_log_at(LogLevel::Warn, PRESENT_MISS_WARN); + original +} + +fn chosen_mode( + supported: &[vk::PresentModeKHR], + value: vk::PresentModeKHR, + original: vk::PresentModeKHR, +) -> vk::PresentModeKHR { + match supported_mode(supported, value) { + Some(mode) => mode, + None => logged_mode_miss(original), + } +} + +fn scaling_set(node: *const VkSwapchainPresentScalingCreateInfoKHR) -> bool { + unsafe { + (*node).scaling_behavior != NO_FLAGS + || (*node).present_gravity_x != NO_FLAGS + || (*node).present_gravity_y != NO_FLAGS + } +} + +fn scaling_chained(p_next: *const c_void) -> bool { + match chain_find(p_next, SWAPCHAIN_PRESENT_SCALING_TYPE) { + Some(node) => scaling_set(node as *const VkSwapchainPresentScalingCreateInfoKHR), + None => false, + } +} + +pub(crate) fn mode_tie_holds( + flags: vk::SwapchainCreateFlagsKHR, + p_next: *const c_void, +) -> bool { + flags.as_raw() == NO_FLAGS && !scaling_chained(p_next) +} + +fn pick_present_mode( + choice: Option, + supported: &[vk::PresentModeKHR], + tie_holds: bool, + original: vk::PresentModeKHR, +) -> vk::PresentModeKHR { + match (choice, tie_holds, on_floor(original)) { + (Some(value), true, true) => chosen_mode(supported, value, original), + (_, _, _) => original, + } +} + +fn logged_alpha_miss(original: vk::CompositeAlphaFlagsKHR) -> vk::CompositeAlphaFlagsKHR { + call_log_at(LogLevel::Warn, ALPHA_MISS_WARN); + original +} + +fn chosen_alpha( + mask: vk::CompositeAlphaFlagsKHR, + value: vk::CompositeAlphaFlagsKHR, + original: vk::CompositeAlphaFlagsKHR, +) -> vk::CompositeAlphaFlagsKHR { + match value.as_raw() != NO_FLAGS && mask.as_raw() & value.as_raw() == value.as_raw() { + true => value, + false => logged_alpha_miss(original), + } +} + +fn narrowed_alpha_mask( + mask: vk::CompositeAlphaFlagsKHR, + choice: Option, +) -> vk::CompositeAlphaFlagsKHR { + match choice { + Some(value) if mask.as_raw() & value.as_raw() == value.as_raw() => value, + _ => mask, + } +} + +fn narrowed_alpha( + caps: vk::SurfaceCapabilitiesKHR, + choice: Option, +) -> vk::SurfaceCapabilitiesKHR { + vk::SurfaceCapabilitiesKHR { + supported_composite_alpha: narrowed_alpha_mask(caps.supported_composite_alpha, choice), + ..caps + } +} + +fn pick_alpha( + choice: Option, + mask: Option, + original: vk::CompositeAlphaFlagsKHR, +) -> vk::CompositeAlphaFlagsKHR { + match (choice, mask) { + (Some(value), Some(held)) => chosen_alpha(held, value, original), + (_, _) => original, + } +} + +fn pick_clipped(choice: Option, original: vk::Bool32) -> vk::Bool32 { + forced(choice, original) +} + +fn caps_upper(caps_max: u32) -> u32 { + match caps_max { + NO_IMAGE_LIMIT => u32::MAX, + n => n, + } +} + +fn clamped_count(choice: Option, caps: &vk::SurfaceCapabilitiesKHR, original: u32) -> u32 { + match choice { + Some(value) => value.clamp(caps.min_image_count, caps_upper(caps.max_image_count)), + None => original, + } +} + +fn pick_image_count( + choice: Option, + caps: Option<&vk::SurfaceCapabilitiesKHR>, + mode: vk::PresentModeKHR, + original: u32, +) -> u32 { + match (present_semantic(mode).is_some_and(|facts| facts.shared), caps) { + (false, Some(held)) => clamped_count(choice, held, original), + (_, _) => original, + } +} + +fn reported_high(high: u32) -> u32 { + match high { + u32::MAX => NO_IMAGE_LIMIT, + n => n, + } +} + +fn narrowed_caps(caps: vk::SurfaceCapabilitiesKHR, value: u32) -> vk::SurfaceCapabilitiesKHR { + let held = value.clamp(caps.min_image_count, caps_upper(caps.max_image_count)); + vk::SurfaceCapabilitiesKHR { + min_image_count: held, + max_image_count: reported_high(held), + ..caps + } +} + +fn clamped_caps( + caps: vk::SurfaceCapabilitiesKHR, + choice: Option, +) -> vk::SurfaceCapabilitiesKHR { + match choice { + Some(value) => narrowed_caps(caps, value), + None => caps, + } +} + +fn call_log_blending(blends: bool) { + match blends { + false => call_log_at(LogLevel::Info, ALPHA_OPAQUE_INFO), + true => (), + } +} + +fn call_log_alpha(choice: Option) { + match choice.and_then(alpha_semantic) { + Some(facts) => call_log_blending(facts.blends), + None => (), + } +} + +fn present_note( + choice: Option, + tie_holds: bool, + original: vk::PresentModeKHR, +) -> Option<&'static str> { + match (choice, on_floor(original), tie_holds) { + (None, _, _) => None, + (Some(_), false, _) => Some(PRESENT_ABOVE_FLOOR_REASON), + (Some(_), true, false) => Some(PRESENT_TIE_REASON), + (Some(_), true, true) => None, + } +} + +fn count_note(choice: Option, mode: vk::PresentModeKHR) -> Option<&'static str> { + match (choice, present_semantic(mode).is_some_and(|facts| facts.shared)) { + (Some(_), true) => Some(COUNT_SHARED_REASON), + (_, _) => None, + } +} + +fn call_report_display( + owner: u64, + asked: &vk::SwapchainCreateInfoKHR<'_>, + held: &vk::SwapchainCreateInfoKHR<'_>, +) { + call_report_value( + owner, + SETTING_PRESENT_MODE, + asked.present_mode, + held.present_mode, + present_display, + None, + ); + call_report_value( + owner, + SETTING_IMAGE_COUNT, + asked.min_image_count, + held.min_image_count, + count_text, + None, + ); + call_report_value( + owner, + SETTING_COMPOSITE_ALPHA, + asked.composite_alpha, + held.composite_alpha, + alpha_display, + None, + ); + call_report_value( + owner, + SETTING_CLIPPED, + asked.clipped, + held.clipped, + toggle_text, + None, + ); +} + +fn call_report_framerate(owner: u64, s: &Settings) { + call_report_choice(owner, SETTING_FRAME_LIMIT, s.frame_limit.map(number_text)); + call_report_choice( + owner, + SETTING_FRAME_LIMIT_CADENCE, + s.cadence.map(cadence_display), + ); + call_report_choice( + owner, + SETTING_FRAME_LIMIT_METHOD, + s.limit_method.map(method_display), + ); + call_report_choice(owner, SETTING_FRAME_PACING, s.pacing.map(pacing_display)); +} + +fn call_report_fields( + owner: u64, + s: &Settings, + asked: &vk::SwapchainCreateInfoKHR<'_>, + held: &vk::SwapchainCreateInfoKHR<'_>, +) { + call_report_display(owner, asked, held); + call_report_framerate(owner, s); +} + +fn call_report_swapchain( + dev: &VkDevState, + s: &Settings, + asked: &vk::SwapchainCreateInfoKHR<'_>, + held: &vk::SwapchainCreateInfoKHR<'_>, +) { + match info_wanted() { + true => call_report_fields(dev.device.handle().as_raw(), s, asked, held), + false => (), + } +} + +fn patched_swapchain_ci<'a>( + original: &vk::SwapchainCreateInfoKHR<'a>, + chosen: vk::PresentModeKHR, + caps: Option<&vk::SurfaceCapabilitiesKHR>, + s: &Settings, +) -> vk::SwapchainCreateInfoKHR<'a> { + vk::SwapchainCreateInfoKHR { + present_mode: chosen, + min_image_count: pick_image_count(s.image_count, caps, chosen, original.min_image_count), + composite_alpha: pick_alpha( + s.composite_alpha, + caps.map(|held| held.supported_composite_alpha), + original.composite_alpha, + ), + clipped: pick_clipped(s.clipped, original.clipped), + ..*original + } +} + +pub(crate) fn call_query_present_modes( + inst: &VkInstState, + phys: vk::PhysicalDevice, + surface: vk::SurfaceKHR, +) -> Vec { + let mut n: u32 = 0; + let r1 = unsafe { + (inst.surface_fp.get_physical_device_surface_present_modes_khr)(phys, surface, &mut n, ptr::null_mut()) + }; + let mut v = vec![vk::PresentModeKHR::FIFO; n as usize]; + let r2 = unsafe { + (inst.surface_fp.get_physical_device_surface_present_modes_khr)(phys, surface, &mut n, v.as_mut_ptr()) + }; + match (r1, r2) { + (vk::Result::SUCCESS, vk::Result::SUCCESS) => v, + (_, _) => Vec::new(), + } +} + +fn call_query_surface_caps( + inst: &VkInstState, + phys: vk::PhysicalDevice, + surface: vk::SurfaceKHR, +) -> Option { + let mut caps = vk::SurfaceCapabilitiesKHR::default(); + match unsafe { + (inst.surface_fp.get_physical_device_surface_capabilities_khr)(phys, surface, &mut caps) + } { + vk::Result::SUCCESS => Some(caps), + _ => None, + } +} + +fn call_filtered_modes( + inst: &VkInstState, + phys: vk::PhysicalDevice, + surface: vk::SurfaceKHR, + choice: Option, +) -> Vec { + call_present_filtered(call_query_present_modes(inst, phys, surface), choice) +} + +pub(crate) fn call_surface_present_modes( + phys: vk::PhysicalDevice, + surface: vk::SurfaceKHR, + count: *mut u32, + modes: *mut vk::PresentModeKHR, +) -> vk::Result { + match owning_instance(phys) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some((_, inst)) => call_write_list( + &call_filtered_modes(&inst, phys, surface, ensure_settings().present_mode), + count, + modes, + ), + } +} + +fn call_narrowed_result( + queried: vk::Result, + out: *mut vk::SurfaceCapabilitiesKHR, + s: &Settings, +) -> vk::Result { + match queried { + vk::Result::SUCCESS => { + unsafe { *out = narrowed_alpha(clamped_caps(*out, s.image_count), s.composite_alpha) }; + vk::Result::SUCCESS + } + e => e, + } +} + +pub(crate) fn call_surface_capabilities( + phys: vk::PhysicalDevice, + surface: vk::SurfaceKHR, + out: *mut vk::SurfaceCapabilitiesKHR, +) -> vk::Result { + match owning_instance(phys) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some((_, inst)) => call_narrowed_result( + unsafe { + (inst.surface_fp.get_physical_device_surface_capabilities_khr)(phys, surface, out) + }, + out, + ensure_settings(), + ), + } +} + +fn node_type(node: *mut VkChainNode) -> u32 { + unsafe { (*node).s_type.as_raw() as u32 } +} + +fn mode_lists(head: *mut c_void) -> Vec<*mut VkPresentModeList> { + filled_nodes(head) + .into_iter() + .filter(|node| MODE_LIST_TYPES.contains(&node_type(*node))) + .map(|node| node as *mut VkPresentModeList) + .collect() +} + +fn read_modes(list: *mut VkPresentModeList) -> Vec { + (0..unsafe { (*list).present_mode_count } as usize) + .map(|at| unsafe { *(*list).p_present_modes.add(at) }) + .collect() +} + +fn call_write_modes(list: *mut VkPresentModeList, kept_modes: &[vk::PresentModeKHR]) { + kept_modes + .iter() + .enumerate() + .for_each(|(at, mode)| unsafe { *(*list).p_present_modes.add(at) = *mode }); + unsafe { (*list).present_mode_count = kept_modes.len() as u32 }; +} + +fn call_filtered_mode_list(list: *mut VkPresentModeList, choice: Option) { + match unsafe { (*list).p_present_modes.is_null() } { + true => (), + false => call_write_modes(list, &call_present_filtered(read_modes(list), choice)), + } +} + +fn call_filtered_chain(head: *mut c_void, choice: Option) { + mode_lists(head) + .into_iter() + .for_each(|list| call_filtered_mode_list(list, choice)); +} + +fn call_caps2_through( + fp: PfnSurfaceCaps2, + phys: vk::PhysicalDevice, + info: *const VkPhysicalDeviceSurfaceInfo2, + out: *mut VkSurfaceCapabilities2, + s: &Settings, +) -> vk::Result { + match unsafe { fp(phys, info, out) } { + vk::Result::SUCCESS => { + unsafe { + (*out).surface_capabilities = narrowed_alpha( + clamped_caps((*out).surface_capabilities, s.image_count), + s.composite_alpha, + ) + }; + call_filtered_chain(unsafe { (*out).p_next }, s.present_mode); + vk::Result::SUCCESS + } + e => e, + } +} + +pub(crate) fn call_surface_capabilities2( + phys: vk::PhysicalDevice, + info: *const VkPhysicalDeviceSurfaceInfo2, + out: *mut VkSurfaceCapabilities2, +) -> vk::Result { + match owning_instance(phys).and_then(|(_, inst)| inst.caps2_fp) { + None => vk::Result::ERROR_INITIALIZATION_FAILED, + Some(fp) => call_caps2_through(fp, phys, info, out, ensure_settings()), + } +} + +fn call_probe_surface( + tag: Option<&'static str>, + supported: &[vk::PresentModeKHR], + caps: Option<&vk::SurfaceCapabilitiesKHR>, +) { + match (env_probe_active(), tag, caps) { + (true, Some(name), Some(held)) => { + call_record_surface(name, build_surface(supported, held)) + } + (_, _, _) => (), + } +} + +static FORCED_MODES: Mutex>> = Mutex::new(None); + +fn call_remember_forced(dev: u64, swapchain: u64, mode: vk::PresentModeKHR) { + match FORCED_MODES.lock() { + Ok(mut guard) => { + guard + .get_or_insert_with(HashMap::new) + .insert((dev, swapchain), mode); + } + Err(_) => (), + } +} + +pub(crate) fn call_forget_forced_mode(dev: u64, swapchain: vk::SwapchainKHR) { + match FORCED_MODES.lock() { + Ok(mut guard) => { + guard + .get_or_insert_with(HashMap::new) + .remove(&(dev, swapchain.as_raw())); + } + Err(_) => (), + } +} + +pub(crate) fn call_forget_device_forced_modes(dev: u64) { + match FORCED_MODES.lock() { + Ok(mut guard) => guard + .get_or_insert_with(HashMap::new) + .retain(|(owner, _), _| *owner != dev), + Err(_) => (), + } +} + +fn forced_mode_for(dev: u64, swapchain: u64) -> Option { + FORCED_MODES + .lock() + .ok() + .and_then(|guard| guard.as_ref().and_then(|m| m.get(&(dev, swapchain)).copied())) +} + +fn compatibility_allowed(extensions: &HashSet) -> bool { + extensions.contains(EXT_GET_SURFACE_CAPS_2) + && (extensions.contains(EXT_SURFACE_MAINTENANCE_1) + || extensions.contains(EXT_SURFACE_MAINTENANCE_1_EXT)) +} + +fn surface_mode_node(forced: vk::PresentModeKHR) -> VkSurfacePresentModeKHR { + VkSurfacePresentModeKHR { + s_type: vk::StructureType::from_raw(SURFACE_PRESENT_MODE_TYPE as i32), + p_next: ptr::null_mut(), + present_mode: forced, + } +} + +fn compatibility_node(modes: *mut vk::PresentModeKHR, count: u32) -> VkPresentModeList { + VkPresentModeList { + s_type: vk::StructureType::from_raw(MODE_COMPATIBILITY_TYPE as i32), + p_next: ptr::null_mut(), + present_mode_count: count, + p_present_modes: modes, + } +} + +fn capabilities_2(chained: *mut c_void) -> VkSurfaceCapabilities2 { + VkSurfaceCapabilities2 { + s_type: vk::StructureType::from_raw(SURFACE_CAPABILITIES_2_TYPE as i32), + p_next: chained, + surface_capabilities: vk::SurfaceCapabilitiesKHR::default(), + } +} + +fn call_compatibility_list( + inst: &VkInstState, + phys: vk::PhysicalDevice, + surface: vk::SurfaceKHR, + forced: vk::PresentModeKHR, +) -> Option> { + let fp = inst.caps2_fp?; + let mode_info = surface_mode_node(forced); + let info = VkPhysicalDeviceSurfaceInfo2 { + s_type: vk::StructureType::from_raw(SURFACE_INFO_2_TYPE as i32), + p_next: &mode_info as *const VkSurfacePresentModeKHR as *const c_void, + surface, + }; + let mut counting = compatibility_node(ptr::null_mut(), 0); + let mut caps = capabilities_2(&mut counting as *mut VkPresentModeList as *mut c_void); + let counted = unsafe { fp(phys, &info, &mut caps) }; + let mut modes = vec![vk::PresentModeKHR::FIFO; counting.present_mode_count as usize]; + let mut filling = compatibility_node(modes.as_mut_ptr(), counting.present_mode_count); + let mut out = capabilities_2(&mut filling as *mut VkPresentModeList as *mut c_void); + let filled = unsafe { fp(phys, &info, &mut out) }; + match (counted, filled) { + (vk::Result::SUCCESS, vk::Result::SUCCESS) => { + Some(modes[..filling.present_mode_count as usize].to_vec()) + } + (_, _) => None, + } +} + +fn call_compatible( + inst: &VkInstState, + dev: &VkDevState, + surface: vk::SurfaceKHR, + forced: vk::PresentModeKHR, +) -> bool { + compatibility_allowed(&inst.extensions) + && call_compatibility_list(inst, dev.phys, surface, forced) + .map(|list| list.contains(&forced)) + .unwrap_or(false) +} + +fn call_created_swapchain(created: vk::Result) -> vk::Result { + match created { + vk::Result::SUCCESS => { + call_log_at(LogLevel::Info, LOG_SWAPCHAIN_CREATED); + vk::Result::SUCCESS + } + e => e, + } +} + +pub(crate) struct SwapchainRebuild<'a> { + #[allow(dead_code)] + modes: Vec, + #[allow(dead_code)] + node: Vec, + #[allow(dead_code)] + relink: Option, + pub(crate) forced: Option, + pub(crate) ci: vk::SwapchainCreateInfoKHR<'a>, +} + +pub(crate) struct PresentRebuild<'a> { + #[allow(dead_code)] + modes: Vec, + #[allow(dead_code)] + node: Vec, + #[allow(dead_code)] + relink: Relinked, + pub(crate) info: vk::PresentInfoKHR<'a>, +} + +fn spent_mode(dev: u64, swapchain: vk::SwapchainKHR, asked: vk::PresentModeKHR) -> vk::PresentModeKHR { + match forced_mode_for(dev, swapchain.as_raw()) { + Some(mode) => mode, + None => asked, + } +} + +fn spent_modes( + dev: u64, + info: &vk::PresentInfoKHR<'_>, + node: *const VkSwapchainPresentModeInfoKHR, +) -> Vec { + (0..unsafe { (*node).swapchain_count } as usize) + .map(|at| { + spent_mode( + dev, + unsafe { *info.p_swapchains.add(at) }, + unsafe { *(*node).p_present_modes.add(at) }, + ) + }) + .collect() +} + +fn rebuilt_mode_info( + node: *const VkSwapchainPresentModeInfoKHR, + modes: &[vk::PresentModeKHR], +) -> VkSwapchainPresentModeInfoKHR { + VkSwapchainPresentModeInfoKHR { + p_present_modes: modes.as_ptr(), + ..unsafe { *node } + } +} + +fn asked_modes(node: *const VkSwapchainPresentModeInfoKHR) -> Vec { + (0..unsafe { (*node).swapchain_count } as usize) + .map(|at| unsafe { *(*node).p_present_modes.add(at) }) + .collect() +} + +fn built_present<'a>( + dev: u64, + info: &vk::PresentInfoKHR<'a>, + node: *const VkSwapchainPresentModeInfoKHR, +) -> Option> { + let modes = spent_modes(dev, info, node); + match modes == asked_modes(node) { + true => None, + false => call_linked_present(info, node, modes), + } +} + +fn call_linked_present<'a>( + info: &vk::PresentInfoKHR<'a>, + node: *const VkSwapchainPresentModeInfoKHR, + modes: Vec, +) -> Option> { + let owned = vec![rebuilt_mode_info(node, &modes)]; + let relink = call_relinked_chain( + info.p_next, + SWAPCHAIN_PRESENT_MODE_INFO_TYPE, + owned.as_ptr() as *const c_void, + SETTING_PRESENT_MODE, + )?; + Some(PresentRebuild { + info: vk::PresentInfoKHR { + p_next: relink.head, + ..*info + }, + modes, + node: owned, + relink, + }) +} + +pub(crate) fn rebuilt_present<'a>( + dev: u64, + info: &vk::PresentInfoKHR<'a>, +) -> Option> { + built_present( + dev, + info, + chain_find(info.p_next, SWAPCHAIN_PRESENT_MODE_INFO_TYPE)? + as *const VkSwapchainPresentModeInfoKHR, + ) +} + +fn rebuilt_mode_node( + node: *const VkSwapchainPresentModesCreateInfoKHR, + modes: &[vk::PresentModeKHR], +) -> VkSwapchainPresentModesCreateInfoKHR { + VkSwapchainPresentModesCreateInfoKHR { + present_mode_count: modes.len() as u32, + p_present_modes: modes.as_ptr(), + ..unsafe { *node } + } +} + +fn plain_swapchain(patched: vk::SwapchainCreateInfoKHR<'_>) -> SwapchainRebuild<'_> { + SwapchainRebuild { + modes: Vec::new(), + node: Vec::new(), + relink: None, + forced: None, + ci: patched, + } +} + +fn kept_swapchain<'a>( + original: &vk::SwapchainCreateInfoKHR<'a>, + patched: vk::SwapchainCreateInfoKHR<'a>, +) -> SwapchainRebuild<'a> { + plain_swapchain(vk::SwapchainCreateInfoKHR { + present_mode: original.present_mode, + ..patched + }) +} + +fn call_rebuilt_swapchain<'a>( + original: &vk::SwapchainCreateInfoKHR<'a>, + patched: vk::SwapchainCreateInfoKHR<'a>, + node: *const VkSwapchainPresentModesCreateInfoKHR, +) -> SwapchainRebuild<'a> { + let modes = vec![patched.present_mode]; + let owned = vec![rebuilt_mode_node(node, &modes)]; + match call_relinked_chain( + patched.p_next, + SWAPCHAIN_MODE_LIST_TYPE, + owned.as_ptr() as *const c_void, + SETTING_PRESENT_MODE, + ) { + Some(relink) => SwapchainRebuild { + ci: vk::SwapchainCreateInfoKHR { + p_next: relink.head, + ..patched + }, + forced: Some(patched.present_mode), + modes, + node: owned, + relink: Some(relink), + }, + None => kept_swapchain(original, patched), + } +} + +fn call_refused_swapchain<'a>( + original: &vk::SwapchainCreateInfoKHR<'a>, + patched: vk::SwapchainCreateInfoKHR<'a>, +) -> SwapchainRebuild<'a> { + call_report_reason(SETTING_PRESENT_MODE, Some(PRESENT_LIST_REASON)); + kept_swapchain(original, patched) +} + +fn call_listed_swapchain<'a>( + inst: &VkInstState, + dev: &VkDevState, + original: &vk::SwapchainCreateInfoKHR<'a>, + patched: vk::SwapchainCreateInfoKHR<'a>, + node: *const VkSwapchainPresentModesCreateInfoKHR, +) -> SwapchainRebuild<'a> { + match call_compatible(inst, dev, original.surface, patched.present_mode) { + true => call_rebuilt_swapchain(original, patched, node), + false => call_refused_swapchain(original, patched), + } +} + +fn call_narrowed_swapchain<'a>( + inst: &VkInstState, + dev: &VkDevState, + original: &vk::SwapchainCreateInfoKHR<'a>, + patched: vk::SwapchainCreateInfoKHR<'a>, + landed: bool, +) -> SwapchainRebuild<'a> { + match (landed, chain_find(patched.p_next, SWAPCHAIN_MODE_LIST_TYPE)) { + (true, Some(node)) => call_listed_swapchain( + inst, + dev, + original, + patched, + node as *const VkSwapchainPresentModesCreateInfoKHR, + ), + (_, _) => plain_swapchain(patched), + } +} + +fn call_prepared_ci<'a>( + inst: &VkInstState, + dev: &VkDevState, + original: &vk::SwapchainCreateInfoKHR<'a>, + s: &Settings, +) -> SwapchainRebuild<'a> { + let supported = call_query_present_modes(inst, dev.phys, original.surface); + let caps = call_query_surface_caps(inst, dev.phys, original.surface); + call_probe_surface( + surface_tag(vk::Instance::from_raw(dev.instance_handle), original.surface), + &supported, + caps.as_ref(), + ); + call_log_alpha(s.composite_alpha); + let tie_holds = mode_tie_holds(original.flags, original.p_next); + let patched = patched_swapchain_ci( + original, + pick_present_mode(s.present_mode, &supported, tie_holds, original.present_mode), + caps.as_ref(), + s, + ); + let built = call_narrowed_swapchain( + inst, + dev, + original, + patched, + tie_holds && s.present_mode == Some(patched.present_mode), + ); + call_report_reason( + SETTING_PRESENT_MODE, + present_note(s.present_mode, tie_holds, original.present_mode), + ); + call_report_reason( + SETTING_IMAGE_COUNT, + count_note(s.image_count, original.present_mode), + ); + call_report_swapchain(dev, s, original, &built.ci); + built +} + +fn call_create_registered( + dev: &VkDevState, + handle: vk::Device, + inst: &VkInstState, + original: &vk::SwapchainCreateInfoKHR<'_>, + s: &Settings, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::SwapchainKHR, +) -> vk::Result { + let built = call_prepared_ci(inst, dev, original, s); + call_recorded_swapchain( + call_created_swapchain(unsafe { + (dev.swap_fp.create_swapchain_khr)(handle, &built.ci, alloc, out) + }), + handle.as_raw(), + &[built.forced], + out, + ) +} + +fn call_recorded_swapchain( + created: vk::Result, + dev: u64, + forced: &[Option], + out: *mut vk::SwapchainKHR, +) -> vk::Result { + match created { + vk::Result::SUCCESS => { + forced.iter().enumerate().for_each(|(at, mode)| match mode { + Some(held) => { + call_remember_forced(dev, unsafe { (*out.add(at)).as_raw() }, *held) + } + None => (), + }); + vk::Result::SUCCESS + } + e => e, + } +} + +pub(crate) fn call_create_swapchain( + dev: &VkDevState, + handle: vk::Device, + ci: *const vk::SwapchainCreateInfoKHR<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::SwapchainKHR, +) -> vk::Result { + let s = ensure_settings(); + match insts_get(dev.instance_handle) { + Some(inst) => call_create_registered(dev, handle, &inst, unsafe { &*ci }, s, alloc, out), + None => unsafe { (dev.swap_fp.create_swapchain_khr)(handle, ci, alloc, out) }, + } +} + +fn call_shared_patched<'a>( + dev: &VkDevState, + inst: &VkInstState, + cis: *const vk::SwapchainCreateInfoKHR<'a>, + count: u32, + s: &Settings, +) -> Vec> { + unsafe { std::slice::from_raw_parts(cis, count as usize) } + .iter() + .map(|original| call_prepared_ci(inst, dev, original, s)) + .collect() +} + +fn call_shared_through( + dev: &VkDevState, + inst: &VkInstState, + fp: PfnCreateSharedSwapchains, + handle: vk::Device, + count: u32, + cis: *const vk::SwapchainCreateInfoKHR<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::SwapchainKHR, +) -> vk::Result { + let built = call_shared_patched(dev, inst, cis, count, ensure_settings()); + let patched: Vec> = built.iter().map(|one| one.ci).collect(); + let forced: Vec> = built.iter().map(|one| one.forced).collect(); + call_recorded_swapchain( + call_created_swapchain(unsafe { fp(handle, count, patched.as_ptr(), alloc, out) }), + handle.as_raw(), + &forced, + out, + ) +} + +pub(crate) fn call_create_shared_swapchains( + dev: &VkDevState, + handle: vk::Device, + count: u32, + cis: *const vk::SwapchainCreateInfoKHR<'_>, + alloc: *const vk::AllocationCallbacks<'_>, + out: *mut vk::SwapchainKHR, +) -> vk::Result { + match (dev.shared_fp, insts_get(dev.instance_handle)) { + (Some(fp), Some(inst)) => { + call_shared_through(dev, &inst, fp, handle, count, cis, alloc, out) + } + (_, _) => vk::Result::ERROR_INITIALIZATION_FAILED, + } +} diff --git a/src/volt/volt.rs b/src/volt/volt.rs new file mode 100644 index 0000000..e9eb0ff --- /dev/null +++ b/src/volt/volt.rs @@ -0,0 +1,3 @@ +fn main() { + std::process::exit(volt::call_run_launcher(volt::launch_args())); +} diff --git a/volt-gui/probe.py b/volt-gui/probe.py deleted file mode 100644 index 265f5de..0000000 --- a/volt-gui/probe.py +++ /dev/null @@ -1,200 +0,0 @@ -import os - -from pathlib import Path -from typing import Final - -PROBE_FILE: Final[str] = "probe.toml" -PROBE_SEP: Final[str] = ";" -PROBE_ON: Final[str] = "on" - -MS_PER_S: Final[float] = 1000.0 -FRAMETIME_DIGITS: Final[int] = 1 -WHOLE_STEP: Final[int] = 2 -FRACTION_STEP: Final[float] = 0.20 -FRACTION_DIGITS: Final[int] = 2 -COUNT_SPAN: Final[int] = 6 -BIAS_CEILING: Final[float] = 4.0 -SHADING_CEILING: Final[float] = 1.0 -OFF_VALUE: Final[str] = "off" - -PRESENT_FALLBACK: Final[tuple] = ("immediate", "mailbox", "fifo", "fifo_relaxed") -DEPTH_FALLBACK: Final[tuple] = ("8bit", "10bit") -SPACE_FALLBACK: Final[tuple] = ("srgb_nonlinear",) -TRANSFER_FALLBACK: Final[tuple] = ("unorm", "srgb") -ALPHA_FALLBACK: Final[tuple] = ("opaque", "inherit") -DEVICE_FALLBACK: Final[tuple] = ("device 1", "device 2", "device 3", "device 4") -ANISO_FALLBACK: Final[float] = 16.0 -BIAS_FALLBACK: Final[float] = 4.0 -LEVEL_FALLBACK: Final[float] = 14.0 -MIN_COUNT_FALLBACK: Final[float] = 2.0 -MAX_COUNT_FALLBACK: Final[float] = 0.0 - - -def build_probe_path() -> Path: - return Path(os.path.expanduser("~/.config/volt-gui")) / PROBE_FILE - - -def _pair_of(line: str) -> tuple: - return (line.split("=", 1)[0].strip(), line.split("=", 1)[1].strip().strip('"')) - - -def parse_probe_text(text: str) -> dict: - return dict(_pair_of(line) for line in text.splitlines() if "=" in line) - - -def call_read_probe() -> dict: - match build_probe_path().exists(): - case False: - return {} - case True: - return parse_probe_text(build_probe_path().read_text(encoding="utf-8")) - - -def call_probe_stamp() -> float: - match build_probe_path().exists(): - case False: - return 0.0 - case True: - return build_probe_path().stat().st_mtime - - -def probe_text(data: dict, key: str) -> str: - return data.get(key, "") - - -def _is_number(text: str) -> bool: - return text.replace("-", "", 1).replace(".", "", 1).isdigit() - - -def probe_number(data: dict, key: str, fallback: float) -> float: - match _is_number(probe_text(data, key)): - case True: - return float(probe_text(data, key)) - case False: - return fallback - - -def probe_flag(data: dict, key: str) -> bool: - match probe_text(data, key): - case "": - return True - case text: - return text == PROBE_ON - - -def _filled(values: tuple, fallback: tuple) -> tuple: - match len(values): - case 0: - return fallback - case _: - return values - - -def probe_list(data: dict, key: str, fallback: tuple) -> tuple: - return _filled( - tuple(v.lower() for v in probe_text(data, key).split(PROBE_SEP) if v != ""), - fallback) - - -def plain_pairs(values: tuple) -> tuple: - return tuple((v, v) for v in values) - - -def _frametime_label(fps: str) -> str: - return fps + " (" + str(round(MS_PER_S / float(fps), FRAMETIME_DIGITS)) + "ms)" - - -def frametime_pairs(values: tuple) -> tuple: - return tuple((v, _frametime_label(v)) for v in values) - - -def _first_step(low: int) -> int: - return low + (low % WHOLE_STEP) - - -def _whole_values(low: int, high: int) -> tuple: - return tuple(str(v) for v in range(_first_step(low), high + 1, WHOLE_STEP)) - - -def _fraction_values(low: int, high: int) -> tuple: - return tuple( - str(round(v * FRACTION_STEP, FRACTION_DIGITS)) - for v in range(low, high + 1)) - - -def _span_of(limit: float) -> int: - return int(limit / FRACTION_STEP) - - -def present_options(data: dict) -> tuple: - return plain_pairs(probe_list(data, "present_modes", PRESENT_FALLBACK)) - - -def depth_options(data: dict) -> tuple: - return plain_pairs(probe_list(data, "color_depths", DEPTH_FALLBACK)) - - -def space_options(data: dict) -> tuple: - return plain_pairs(probe_list(data, "color_spaces", SPACE_FALLBACK)) - - -def transfer_options(data: dict) -> tuple: - return plain_pairs(probe_list(data, "transfer_functions", TRANSFER_FALLBACK)) - - -def alpha_options(data: dict) -> tuple: - return plain_pairs(probe_list(data, "composite_alphas", ALPHA_FALLBACK)) - - -def gpu_options(data: dict) -> tuple: - return tuple( - (str(at + 1), name) - for at, name in enumerate(probe_list(data, "device_names", DEVICE_FALLBACK))) - - -def _aniso_ladder(data: dict) -> tuple: - return plain_pairs(_whole_values( - WHOLE_STEP, - int(probe_number(data, "max_anisotropy", ANISO_FALLBACK)))) - - -def aniso_options(data: dict) -> tuple: - match probe_flag(data, "sampler_anisotropy"): - case False: - return () - case True: - return ((OFF_VALUE, OFF_VALUE),) + _aniso_ladder(data) - - -def shading_options(data: dict) -> tuple: - match probe_flag(data, "sample_rate_shading"): - case False: - return () - case True: - return ((OFF_VALUE, OFF_VALUE),) + plain_pairs( - _fraction_values(1, _span_of(SHADING_CEILING))) - - -def _count_ceiling(low: int, high: int) -> int: - match high: - case 0: - return low + COUNT_SPAN - case _: - return high - - -def image_count_options(data: dict) -> tuple: - low = int(probe_number(data, "min_image_count", MIN_COUNT_FALLBACK)) - high = int(probe_number(data, "max_image_count", MAX_COUNT_FALLBACK)) - return plain_pairs(_whole_values(low, _count_ceiling(low, high))) - - -def mip_options(data: dict) -> tuple: - return plain_pairs(_whole_values( - 0, - int(probe_number(data, "max_lod_level", LEVEL_FALLBACK)))) - - -def lod_bias_options(data: dict) -> tuple: - span = _span_of(min(probe_number(data, "max_lod_bias", BIAS_FALLBACK), BIAS_CEILING)) - return plain_pairs(_fraction_values(-span, span)) diff --git a/volt-gui/welcome.py b/volt-gui/welcome.py deleted file mode 100644 index 92fed2d..0000000 --- a/volt-gui/welcome.py +++ /dev/null @@ -1,128 +0,0 @@ -from PySide6.QtCore import Qt -from PySide6.QtWidgets import QHBoxLayout -from PySide6.QtWidgets import QMainWindow -from PySide6.QtWidgets import QPushButton -from PySide6.QtWidgets import QStackedWidget -from PySide6.QtWidgets import QVBoxLayout -from PySide6.QtWidgets import QWidget - -from themes import get_standard_button_height -from themes import get_standard_button_width -from ui import create_simple_sidebar_widget -from ui import create_tab_content_widget - - -def get_welcome_settings() -> dict: - return { - "Welcome": { - "Welcome to volt gui": ( - ("text", "volt-gui is my AMD Adrenaline / NVIDIA Settings Linux Alternative.\n\nSettings are applied by volt, a Vulkan implicit layer, so they work on every Vulkan driver: RADV, ANV, NVK, AMDVLK, the NVIDIA proprietary driver, and anything else that supports Vulkan 1.0."), - ("text", "The layer sticks to core Vulkan 1.0 and the swapchain extension, so nothing here behaves differently from one driver to the next."), - ) - }, - "How it Works": { - "The volt Layer": ( - ("text", "Every setting in this application is written to a profile file at ~/.config/volt-gui/. The volt Vulkan layer reads that profile when a game starts and rewrites the Vulkan calls the game makes: samplers for texture filtering and mip selection, the swapchain for vsync, image count and compositing, the surface format list for color depth, color space and transfer function, device enumeration for GPU selection, presents for the frame limiter, and pipelines for the rendering toggles."), - ("text", "Settings are read once when a game starts and never change while it runs. Press Apply, then start the game again. The preview window restarts on Apply so the lists here stay in step."), - ), - "What it Will Not Do": ( - ("text", "volt only changes what the game asks Vulkan for. It never draws anything itself, so sharpening, upscaling, frame generation, forced MSAA and overlays are all out of scope. Use MangoHud for an overlay and CoreCtrl for clocks and fan curves."), - ("text", "It also never turns anything on that the game left off. volt enables no device feature and no extension. Where a setting needs a feature, volt reads what the game asked for and applies the setting only if the game enabled it: that is how Anisotropic Filtering and Sample Shading work, and where the game left the feature clear those cards hold nothing but default. A setting that cannot be reached that way at all stays out, which keeps wireframe, line width, depth clamp and cubic filtering off the table. Where a game moves state onto an extension path, volt follows it there: a hook for an extension the game never enabled is simply unreachable."), - ) - }, - "Settings": { - "One Value Per Setting": ( - ("text", "Every setting is a single choice: the value volt forces, or default, which means volt does not touch what the game asked for. There is no range, no ordering between values, and nothing to get backwards."), - ("text", "A value volt has no name for still appears in the list, still saves to a profile, and still applies, exactly like a named one."), - ("text", "Where the specification admits only what a query returned, a value your device did not report is not forced. volt keeps the game's own value and logs a warning, so a profile written on another machine never makes a call invalid."), - ("text", "Where the specification bounds a value, LOD bias against your device limit and image count against what the surface allows, volt clamps what it passes down. That clamp is correctness rather than a choice, so it is not shown here."), - ), - "Where the Lists Come From": ( - ("text", "Most of the boxes are filled in from your own hardware rather than from a list built into volt-gui. Present modes, colour depths, colour spaces, transfer functions and alpha modes come from what the surface reports, the GPU list comes from what the driver enumerates, and mip levels and LOD bias run up to the limits your device gives. A card without the feature behind it holds nothing but default."), - ("text", "That means a mode or a format volt has never heard of shows up as soon as your driver supports it. It also means a profile written on another machine can name something this one cannot do, in which case that setting resets to default and volt-gui tells you which ones."), - ("text", "The three Framerate settings are the exception. A game never tells Vulkan what frame rate it wants, so there is nothing to read from the device and their lists are volt's own."), - ), - "The Frame Limiter": ( - ("text", "Frame Limit caps the rate at present time. Method sets when the limiter waits and Pacing sets how, and neither does anything until Limit is set."), - ("text", "Pacing runs from cheapest to tightest. sleep hands the whole wait to the kernel and costs nothing. sliced sleeps in short steps and rechecks the clock, which corrects for the kernel waking late. precise sleeps most of the interval then busy waits half a millisecond. spin busy waits the whole interval, the steadiest of the four and the only one that keeps a core awake."), - ), - "Settings That Hide a List": ( - ("text", "Color Depth, Color Space, Transfer Function, Present Mode and Physical Device work differently. Instead of changing a value they hide entries from the list the game is shown, so a game that takes the first surface format or the first device gets the one you picked. The three format settings filter one list one after the other, each restoring it on its own if nothing survives."), - ("text", "If your choice leaves nothing behind, the layer puts the whole list back and logs a warning. The game always has at least one format and one device to work with."), - ) - }, - "Usage": { - "Launching Games": ( - ("text", "Prepend the volt launcher to your game command. It activates the layer for that process only and selects the profile:"), - ("code", "volt -- %command%", "Steam (Launch Options, default profile):"), - ("code", "volt myprofile -- %command%", "Steam (named profile):"), - ("code", "volt -- ./game", "Terminal:"), - ("code", "volt -- flatpak run com.example.Game", "Flatpak:"), - ), - "Default Behavior": ( - ("text", "Every setting defaults to \"default\", which means the layer does not touch that value and the application keeps its own choice. A profile with everything on default is a true passthrough."), - ), - "Seeing What Applied": ( - ("text", "Run the game from a terminal with VOLT_LOG=info and the layer prints what it applied, what the surface or the device turned down, and when it picked up a changed profile."), - ("code", "VOLT_LOG=info volt -- ./game", ""), - ), - "The Preview Window": ( - ("text", "volt-gui keeps a small vkgears window running under the profile you are editing. It is what fills the setting lists with your hardware, and it doubles as a look at the profile: pressing Apply restarts it under the values you just saved. Switching profiles restarts it too."), - ("text", "Close it whenever you like. volt-gui carries on with what it learned the last time it ran, and starts a fresh one when you change profile. If vkgears is not installed the lists fall back to sensible defaults, so nothing breaks."), - ("code", "volt --probe myprofile -- vkgears", "Run it yourself:"), - ) - }, - "Profiles": { - "Profiles": ( - ("text", "Create profiles to switch between configurations per game.\n\n1. Open the profile selector and choose New Profile.\n2. Configure and Apply settings.\n3. Launch the game with that profile name, or switch profiles from the System Tray."), - ("text", "The launch command shown next to the Apply button always matches the selected profile and can be copied directly into Steam."), - ) - }, - "Presets": { - "Presets": ( - ("text", "Presets fill the profile you have open with a starting point, arranged as a ladder from best looking to fastest:\n\n- Quality: trilinear filtering, blended mips, a slight sharpening bias, every mip level allowed, smoothed cutout edges, classic vsync, a 4 image swapchain, 10 bit colour where the surface has it, and precise pacing on an early wait.\n- Balanced: trilinear and blended mips still, mailbox present for vsync without the latency, sliced pacing.\n- Performance FPS: bilinear, a blurring bias, mailbox present, the swapchain held to 4 images, colour held to 8 bit and the cheaper sleep pacing.\n- Performance Low Latency: the same, aimed at input lag instead, with immediate present, a 2 image swapchain, a late wait and spin pacing, the steadiest of the four.\n- Potato FPS: bilinear, hard mip cuts, a full step of blurring bias, the top two mips off the table, cutout smoothing off.\n- Potato Low Latency: the same again with immediate present, a 2 image swapchain and a late wait.\n\nNo preset touches Colour Space, Transfer Function, Composite Alpha or Clipped Presentation: those depend on your display and your compositor, so they stay yours."), - ("text", "Applying a preset replaces every value in the profile after a confirmation, so anything the preset does not set goes back to default. That includes the frame limit: the right cap depends on your display, so that choice stays yours."), - ("text", "A preset can name something your hardware does not offer, mailbox on a surface without it for instance. That setting resets to default and volt-gui says which ones, so the rest of the preset still lands."), - ) - }, - "Options": { - "Options": ( - ("text", "Changes to Options are saved automatically but only take effect after restarting volt-gui. This includes the theme, scaling, tray behavior, and all other preferences."), - ) - }, - } - - -def create_welcome_window_widget() -> QMainWindow: - window = QMainWindow() - window.setWindowTitle("volt-gui Welcome") - window.setMinimumSize(620, 380) - central_widget = QWidget() - main_layout = QVBoxLayout(central_widget) - main_layout.setContentsMargins(8, 8, 8, 8) - main_layout.setSpacing(8) - content_layout = QHBoxLayout() - content_layout.setContentsMargins(0, 0, 0, 0) - content_layout.setSpacing(0) - welcome_settings = get_welcome_settings() - stacked_widget = QStackedWidget() - for section_data in welcome_settings.values(): - stacked_widget.addWidget(create_tab_content_widget("", section_data)["tab"]) - content_layout.addWidget(create_simple_sidebar_widget(tuple(welcome_settings.keys()), stacked_widget)) - content_layout.addWidget(stacked_widget, 1) - main_layout.addLayout(content_layout, 1) - button_container = QWidget() - button_container.setProperty("buttonContainer", True) - button_layout = QHBoxLayout(button_container) - button_layout.setContentsMargins(8, 8, 8, 8) - button_layout.setSpacing(8) - button_layout.setAlignment(Qt.AlignVCenter) - close_button = QPushButton("Close") - close_button.setFixedSize(get_standard_button_width(), get_standard_button_height()) - close_button.clicked.connect(window.close) - button_layout.addStretch(1) - button_layout.addWidget(close_button, 0, Qt.AlignVCenter) - button_layout.addStretch(1) - main_layout.addWidget(button_container) - window.setCentralWidget(central_widget) - return window diff --git a/volt/checks.rs b/volt/checks.rs deleted file mode 100644 index 638ba72..0000000 --- a/volt/checks.rs +++ /dev/null @@ -1,214 +0,0 @@ -use crate::config::parse_settings; -use crate::consts::MethodChoice; -use crate::lists::filtered; -use crate::lists::forced; -use crate::lists::kept; -use crate::present::advanced; -use crate::present::target_interval_ns; -use crate::present::Timeline; -use crate::ranks::alpha_display; -use crate::ranks::alpha_parse; -use crate::ranks::alpha_semantic; -use crate::ranks::depth_label; -use crate::ranks::format_display; -use crate::ranks::format_semantic; -use crate::ranks::numeric_display; -use crate::ranks::numeric_parse; -use crate::ranks::numeric_semantic; -use crate::ranks::parse_depth_label; -use crate::ranks::present_display; -use crate::ranks::present_parse; -use crate::ranks::present_semantic; -use crate::ranks::space_display; -use crate::ranks::space_parse; -use crate::ranks::space_semantic; -use crate::ranks::Numeric; - -const UNKNOWN_MODE: u32 = 4242; -const UNKNOWN_SPACE: u32 = 4243; -const UNKNOWN_FORMAT: u32 = 4244; -const UNKNOWN_ALPHA: u32 = 16; -const FIFO_MODE: u32 = 2; -const SHARED_MODE: u32 = 1000111000; -const HDR10_SPACE: u32 = 1000104008; -const SRGB_SPACE: u32 = 0; -const OPAQUE_ALPHA: u32 = 1; -const INHERIT_ALPHA: u32 = 8; -const SRGB_FORMAT: u32 = 43; -const UFLOAT_FORMAT: u32 = 122; -const EIGHT_BIT: u32 = 8; -const ELEVEN_BIT: u32 = 11; -const TEN_BIT: u32 = 10; -const UNNAMED_MODE_PROFILE: &str = "[display]\npresent_mode = \"present mode 4242\"\n"; -const NAMED_MODE_PROFILE: &str = "[display]\npresent_mode = \"fifo\"\n"; -const TRANSFER_PROFILE: &str = "[display]\ntransfer_function = \"ufloat\"\n"; -const LIST_EMPTY_WARN: &str = "list emptied by the choice, restoring"; -const LIMIT_START_NS: u64 = 10_000; -const LIMIT_INTERVAL_NS: u64 = 1_000; -const OTHER_INTERVAL_NS: u64 = 2_000; -const LATE_NS: u64 = 100; -const TWO_HUNDRED_FPS: f32 = 200.0; -const TWO_HUNDRED_FPS_NS: u64 = 5_000_000; - -#[test] -fn keeps_the_application_value_when_nothing_is_forced() { - assert_eq!(forced(None, 3), 3); - assert_eq!(forced(Some(2), 3), 2); -} - -#[test] -fn restores_a_list_the_choice_emptied() { - assert_eq!(kept(vec![1, 2, 3], |value: &i32| *value > 3, LIST_EMPTY_WARN), vec![1, 2, 3]); - assert_eq!(kept(vec![1, 2, 3], |value: &i32| *value > 1, LIST_EMPTY_WARN), vec![2, 3]); -} - -#[test] -fn keeps_only_what_the_choice_names() { - assert_eq!(filtered(vec![1, 2, 3], Some(2), |v: &i32| Some(*v), LIST_EMPTY_WARN), vec![2]); - assert_eq!(filtered(vec![1, 2, 3], None, |v: &i32| Some(*v), LIST_EMPTY_WARN), vec![1, 2, 3]); - assert_eq!(filtered(vec![1, 2, 3], Some(9), |v: &i32| Some(*v), LIST_EMPTY_WARN), vec![1, 2, 3]); -} - -#[test] -fn round_trips_a_present_mode_through_its_name() { - assert_eq!(present_parse(&present_display(FIFO_MODE)), Some(FIFO_MODE)); - assert_eq!(present_parse(&present_display(UNKNOWN_MODE)), Some(UNKNOWN_MODE)); -} - -#[test] -fn round_trips_a_color_space_through_its_name() { - assert_eq!(space_parse(&space_display(HDR10_SPACE)), Some(HDR10_SPACE)); - assert_eq!(space_parse(&space_display(UNKNOWN_SPACE)), Some(UNKNOWN_SPACE)); -} - -#[test] -fn round_trips_a_composite_alpha_through_its_name() { - assert_eq!(alpha_parse(&alpha_display(OPAQUE_ALPHA)), Some(OPAQUE_ALPHA)); - assert_eq!(alpha_parse(&alpha_display(UNKNOWN_ALPHA)), Some(UNKNOWN_ALPHA)); -} - - -#[test] -fn round_trips_a_transfer_function_through_its_name() { - assert_eq!(numeric_parse(&numeric_display(Numeric::Srgb)), Some(Numeric::Srgb)); - assert_eq!(numeric_parse(&numeric_display(Numeric::Ufloat)), Some(Numeric::Ufloat)); -} - -#[test] -fn round_trips_a_color_depth_through_its_label() { - assert_eq!(parse_depth_label(&depth_label(TEN_BIT)), Some(TEN_BIT)); -} - -#[test] -fn gives_each_enum_its_own_unknown_prefix() { - assert_ne!(present_display(UNKNOWN_MODE), space_display(UNKNOWN_MODE)); - assert_ne!(space_display(UNKNOWN_MODE), format_display(UNKNOWN_MODE)); - assert_ne!(format_display(UNKNOWN_MODE), alpha_display(UNKNOWN_MODE)); -} - -#[test] -fn groups_an_unnamed_value_under_nothing() { - assert!(present_semantic(UNKNOWN_MODE).is_none()); - assert!(space_semantic(UNKNOWN_SPACE).is_none()); - assert!(format_semantic(UNKNOWN_FORMAT).is_none()); - assert!(alpha_semantic(UNKNOWN_ALPHA).is_none()); -} - -#[test] -fn reads_the_facts_a_setting_branches_on() { - assert_eq!(format_semantic(SRGB_FORMAT).map(|facts| facts.depth), Some(EIGHT_BIT)); - assert_eq!(format_semantic(UFLOAT_FORMAT).map(|facts| facts.depth), Some(ELEVEN_BIT)); - assert_eq!( - format_semantic(UFLOAT_FORMAT).map(|facts| facts.numeric), - Some(Numeric::Ufloat) - ); - assert_eq!(alpha_semantic(OPAQUE_ALPHA).map(|facts| facts.blends), Some(false)); - assert_eq!(alpha_semantic(INHERIT_ALPHA).map(|facts| facts.blends), Some(true)); - assert_eq!(space_semantic(SRGB_SPACE).map(|facts| facts.extended), Some(false)); - assert_eq!(space_semantic(HDR10_SPACE).map(|facts| facts.extended), Some(true)); - assert_eq!(present_semantic(FIFO_MODE).map(|facts| facts.extended), Some(false)); - assert_eq!(present_semantic(SHARED_MODE).map(|facts| facts.extended), Some(true)); - assert_eq!(numeric_semantic(Numeric::Srgb).map(|facts| facts.encoded), Some(true)); - assert_eq!(numeric_semantic(Numeric::Unorm).map(|facts| facts.encoded), Some(false)); -} - -#[test] -fn forces_a_value_volt_has_no_name_for() { - assert_eq!(parse_settings(UNNAMED_MODE_PROFILE).present_mode, Some(UNKNOWN_MODE)); - assert_eq!(parse_settings(NAMED_MODE_PROFILE).present_mode, Some(FIFO_MODE)); - assert_eq!(parse_settings(TRANSFER_PROFILE).transfer, Some(Numeric::Ufloat)); -} - -#[test] -fn turns_a_frame_rate_into_an_interval() { - assert_eq!(target_interval_ns(TWO_HUNDRED_FPS), TWO_HUNDRED_FPS_NS); -} - -#[test] -fn starts_the_timeline_one_interval_after_the_first_frame() { - assert_eq!( - advanced(None, LIMIT_START_NS, LIMIT_INTERVAL_NS, Some(MethodChoice::Early)), - Timeline { - target: LIMIT_START_NS + LIMIT_INTERVAL_NS, - interval: LIMIT_INTERVAL_NS, - } - ); -} - -#[test] -fn holds_the_cadence_when_a_frame_runs_a_little_late() { - assert_eq!( - advanced( - Some(Timeline { target: LIMIT_START_NS, interval: LIMIT_INTERVAL_NS }), - LIMIT_START_NS + LATE_NS, - LIMIT_INTERVAL_NS, - Some(MethodChoice::Early), - ) - .target, - LIMIT_START_NS + LIMIT_INTERVAL_NS - ); -} - -#[test] -fn never_chases_a_slow_frame_with_a_fast_one() { - assert_eq!( - advanced( - Some(Timeline { target: LIMIT_START_NS, interval: LIMIT_INTERVAL_NS }), - LIMIT_START_NS + LIMIT_INTERVAL_NS + LIMIT_INTERVAL_NS, - LIMIT_INTERVAL_NS, - Some(MethodChoice::Early), - ) - .target, - LIMIT_START_NS + LIMIT_INTERVAL_NS + LIMIT_INTERVAL_NS + LIMIT_INTERVAL_NS - ); -} - -#[test] -fn measures_a_reactive_interval_from_the_frame_just_shown() { - assert_eq!( - advanced( - Some(Timeline { target: LIMIT_START_NS, interval: LIMIT_INTERVAL_NS }), - LIMIT_START_NS + LATE_NS, - LIMIT_INTERVAL_NS, - Some(MethodChoice::Reactive), - ) - .target, - LIMIT_START_NS + LATE_NS + LIMIT_INTERVAL_NS - ); -} - -#[test] -fn restarts_the_timeline_when_the_frame_limit_changes() { - assert_eq!( - advanced( - Some(Timeline { target: LIMIT_START_NS, interval: LIMIT_INTERVAL_NS }), - LIMIT_START_NS + LATE_NS, - OTHER_INTERVAL_NS, - Some(MethodChoice::Late), - ), - Timeline { - target: LIMIT_START_NS + LATE_NS + OTHER_INTERVAL_NS, - interval: OTHER_INTERVAL_NS, - } - ); -} diff --git a/volt/config.rs b/volt/config.rs deleted file mode 100644 index 5aae822..0000000 --- a/volt/config.rs +++ /dev/null @@ -1,279 +0,0 @@ -use std::fs; -use std::path::PathBuf; -use std::sync::OnceLock; - -use crate::consts::ANISO_OFF; -use crate::consts::DEFAULT_PROFILE; -use crate::consts::FILTER_BILINEAR; -use crate::consts::FILTER_RETRO; -use crate::consts::FILTER_TRILINEAR; -use crate::consts::FRAME_LIMIT_MIN; -use crate::consts::HOME_FALLBACK; -use crate::consts::HOME_UNSET_WARN; -use crate::consts::MethodChoice; -use crate::consts::MIPMAP_LINEAR; -use crate::consts::MIPMAP_NEAREST; -use crate::consts::PacingChoice; -use crate::consts::SECTION_DISPLAY; -use crate::consts::SECTION_FRAMERATE; -use crate::consts::SECTION_GPU; -use crate::consts::SECTION_RENDERING; -use crate::consts::SECTION_TEXTURES; -use crate::consts::SETTINGS_FROZEN_INFO; -use crate::consts::SHADING_MAX; -use crate::consts::SHADING_OFF; -use crate::consts::TOGGLE_OFF; -use crate::consts::TOGGLE_ON; -use crate::env::env_config_name; -use crate::env::env_home; -use crate::logging::init_log_level; -use crate::logging::log_at; -use crate::logging::LogLevel; -use crate::ranks::alpha_parse; -use crate::ranks::numeric_parse; -use crate::ranks::parse_depth_label; -use crate::ranks::present_parse; -use crate::ranks::space_parse; -use crate::ranks::Numeric; - -#[derive(Default)] -pub(crate) struct Settings { - pub(crate) gpu: Option, - pub(crate) present_mode: Option, - pub(crate) image_count: Option, - pub(crate) depth: Option, - pub(crate) color_space: Option, - pub(crate) transfer: Option, - pub(crate) composite_alpha: Option, - pub(crate) clipped: Option, - pub(crate) filtering: Option, - pub(crate) mipmap: Option, - pub(crate) anisotropy: Option, - pub(crate) lod_bias: Option, - pub(crate) mip_floor: Option, - pub(crate) mip_ceiling: Option, - pub(crate) sample_shading: Option, - pub(crate) alpha_coverage: Option, - pub(crate) frame_limit: Option, - pub(crate) limit_method: Option, - pub(crate) pacing: Option, -} - -static SETTINGS: OnceLock = OnceLock::new(); - -fn table_value<'a>(doc: &'a toml::Value, section: &str, key: &str) -> Option<&'a str> { - doc.as_table() - .and_then(|t| t.get(section)) - .and_then(|v| v.as_table()) - .and_then(|t| t.get(key)) - .and_then(|v| v.as_str()) -} - -fn non_default(text: &str) -> Option<&str> { - match text { - "default" | "" => None, - other => Some(other), - } -} - -fn parse_float(text: &str) -> Option { - text.parse::().ok() -} - -fn parse_uint(text: &str) -> Option { - text.parse::().ok() -} - -fn parse_filter(text: &str) -> Option { - match text { - "retro" => Some(FILTER_RETRO), - "bilinear" => Some(FILTER_BILINEAR), - "trilinear" => Some(FILTER_TRILINEAR), - _ => None, - } -} - -fn parse_mipmap(text: &str) -> Option { - match text { - "nearest" => Some(MIPMAP_NEAREST), - "linear" => Some(MIPMAP_LINEAR), - _ => None, - } -} - -fn parse_toggle(text: &str) -> Option { - match text { - "off" => Some(TOGGLE_OFF), - "on" => Some(TOGGLE_ON), - _ => None, - } -} - -fn parse_aniso(text: &str) -> Option { - match text { - "off" => Some(ANISO_OFF), - other => parse_float(other).filter(|v| *v >= ANISO_OFF), - } -} - -fn parse_shading(text: &str) -> Option { - match text { - "off" => Some(SHADING_OFF), - other => parse_float(other).filter(|v| (SHADING_OFF..=SHADING_MAX).contains(v)), - } -} - -fn parse_limit(text: &str) -> Option { - parse_float(text).filter(|v| *v >= FRAME_LIMIT_MIN) -} - -fn parse_gpu(text: &str) -> Option { - parse_uint(text).filter(|v| *v >= 1) -} - -fn parse_method(text: &str) -> Option { - match text { - "early" => Some(MethodChoice::Early), - "late" => Some(MethodChoice::Late), - "reactive" => Some(MethodChoice::Reactive), - _ => None, - } -} - -fn parse_pacing(text: &str) -> Option { - match text { - "sleep" => Some(PacingChoice::Sleep), - "sliced" => Some(PacingChoice::Sliced), - "precise" => Some(PacingChoice::Precise), - "spin" => Some(PacingChoice::Spin), - _ => None, - } -} - -fn checked(section: &str, key: &str, text: &str, value: Option) -> Option { - match value { - Some(v) => Some(v), - None => { - log_at( - LogLevel::Warn, - &format!( - "{}.{} names \"{}\", which is not a value this build can read: that setting was left alone", - section, key, text - ), - ); - None - } - } -} - -fn field(doc: &toml::Value, section: &str, key: &str, parse: F) -> Option -where - F: Fn(&str) -> Option, -{ - match table_value(doc, section, key).and_then(non_default) { - None => None, - Some(text) => checked(section, key, text, parse(text)), - } -} - -fn parse_doc(text: &str) -> toml::Value { - match text.parse::() { - Ok(d) => d, - Err(e) => { - log_at( - LogLevel::Warn, - &format!("config parse failed: {}, using defaults", e), - ); - toml::Value::Table(toml::map::Map::new()) - } - } -} - -pub(crate) fn parse_settings(text: &str) -> Settings { - let doc = parse_doc(text); - Settings { - gpu: field(&doc, SECTION_GPU, "device", parse_gpu), - present_mode: field(&doc, SECTION_DISPLAY, "present_mode", present_parse), - image_count: field(&doc, SECTION_DISPLAY, "image_count", parse_uint), - depth: field(&doc, SECTION_DISPLAY, "color_depth", parse_depth_label), - color_space: field(&doc, SECTION_DISPLAY, "color_space", space_parse), - transfer: field(&doc, SECTION_DISPLAY, "transfer_function", numeric_parse), - composite_alpha: field(&doc, SECTION_DISPLAY, "composite_alpha", alpha_parse), - clipped: field(&doc, SECTION_DISPLAY, "clipped", parse_toggle), - filtering: field(&doc, SECTION_TEXTURES, "filtering", parse_filter), - mipmap: field(&doc, SECTION_TEXTURES, "mipmap_mode", parse_mipmap), - anisotropy: field(&doc, SECTION_TEXTURES, "anisotropy", parse_aniso), - lod_bias: field(&doc, SECTION_TEXTURES, "lod_bias", parse_float), - mip_floor: field(&doc, SECTION_TEXTURES, "mip_floor", parse_float), - mip_ceiling: field(&doc, SECTION_TEXTURES, "mip_ceiling", parse_float), - sample_shading: field(&doc, SECTION_RENDERING, "sample_shading", parse_shading), - alpha_coverage: field(&doc, SECTION_RENDERING, "alpha_to_coverage", parse_toggle), - frame_limit: field(&doc, SECTION_FRAMERATE, "frame_limit", parse_limit), - limit_method: field(&doc, SECTION_FRAMERATE, "frame_limit_method", parse_method), - pacing: field(&doc, SECTION_FRAMERATE, "frame_pacing", parse_pacing), - } -} - -fn name_is_valid(raw: &str) -> bool { - !raw.is_empty() - && !raw.contains('/') - && !raw.contains('\\') - && !raw.contains("..") - && !raw.contains('\0') - && raw.chars().all(|ch| ch.is_ascii_graphic()) -} - -pub(crate) fn sanitize_name(raw: &str) -> String { - match name_is_valid(raw) { - true => raw.into(), - false => { - log_at(LogLevel::Warn, "invalid profile name, using default profile"); - DEFAULT_PROFILE.into() - } - } -} - -fn fallback_home() -> String { - log_at(LogLevel::Warn, HOME_UNSET_WARN); - HOME_FALLBACK.into() -} - -fn home_text() -> String { - match env_home() { - Some(path) => path, - None => fallback_home(), - } -} - -pub(crate) fn home_dir() -> PathBuf { - PathBuf::from(home_text()) -} - -pub(crate) fn config_dir() -> PathBuf { - home_dir().join(".config").join("volt-gui") -} - -pub(crate) fn profile_name() -> String { - sanitize_name(&env_config_name()) -} - -pub(crate) fn config_path(name: &str) -> PathBuf { - config_dir().join(format!("{}.toml", name)) -} - -fn read_config(path: &PathBuf) -> Settings { - fs::read_to_string(path) - .map(|t| parse_settings(&t)) - .unwrap_or_default() -} - -fn load_settings() -> Settings { - init_log_level(); - let loaded = read_config(&config_path(&profile_name())); - log_at(LogLevel::Info, SETTINGS_FROZEN_INFO); - loaded -} - -pub(crate) fn ensure_settings() -> &'static Settings { - SETTINGS.get_or_init(load_settings) -} diff --git a/volt/consts.rs b/volt/consts.rs deleted file mode 100644 index 94634be..0000000 --- a/volt/consts.rs +++ /dev/null @@ -1,191 +0,0 @@ -pub(crate) const LAYER_NAME: &str = "VK_LAYER_VOLT_settings"; -pub(crate) const LAYER_DESC: &str = "Driver style Vulkan settings layer for Linux."; - -pub(crate) const ENABLE_VALUE: &str = "1"; -pub(crate) const DEFAULT_PROFILE: &str = "default"; -pub(crate) const EXIT_EXEC_FAILED: i32 = 127; -pub(crate) const EXIT_USAGE: i32 = 1; -pub(crate) const EXIT_OK: i32 = 0; - -pub(crate) const ENV_CONFIG_NAME: &str = "VOLT_CONFIG_NAME"; -pub(crate) const ENV_LOG: &str = "VOLT_LOG"; -pub(crate) const ENV_ENABLE: &str = "VOLT_ENABLE"; -pub(crate) const ENV_PROBE: &str = "VOLT_PROBE"; -pub(crate) const ENV_HOME: &str = "HOME"; - -pub(crate) const HOME_FALLBACK: &str = "/tmp"; -pub(crate) const HOME_UNSET_WARN: &str = "HOME is unset, reading profiles from /tmp instead"; - -pub(crate) const PROBE_FLAG: &str = "--probe"; -pub(crate) const PROBE_FILE: &str = "probe.toml"; -pub(crate) const PROBE_SECTION: &str = "[probe]"; -pub(crate) const PROBE_SEP: &str = ";"; -pub(crate) const PROBE_ON: &str = "on"; -pub(crate) const PROBE_OFF: &str = "off"; -pub(crate) const PROBE_UNSET: &str = ""; -pub(crate) const PROBE_WRITE_INFO: &str = "probe written to the config directory"; -pub(crate) const PROBE_FAIL_WARN: &str = "probe write failed, the interface keeps its built in lists"; - -pub(crate) const FLATPAK_CMD: &str = "flatpak"; -pub(crate) const FLATPAK_RUN: &str = "run"; -pub(crate) const FLATPAK_INJECT: &str = "/usr/lib/extensions/vulkan/volt/bin/volt-flatpak"; - -pub(crate) const LOG_FD: i32 = 2; -pub(crate) const LOG_LEVEL_OFF: i32 = 0; -pub(crate) const LOG_LEVEL_ERROR: i32 = 1; -pub(crate) const LOG_LEVEL_WARN: i32 = 2; -pub(crate) const LOG_LEVEL_INFO: i32 = 3; -pub(crate) const DEFAULT_LOG_LEVEL: i32 = 2; - -pub(crate) const NS_PER_S: f64 = 1_000_000_000.0; -pub(crate) const SPIN_MARGIN_NS: u64 = 500_000; -pub(crate) const SLICE_MARGIN_NS: u64 = 150_000; -pub(crate) const SLICE_STEP_NS: u64 = 1_000_000; -pub(crate) const FRAME_LIMIT_MIN: f32 = 1.0; -pub(crate) const ANISO_OFF: f32 = 1.0; -pub(crate) const SHADING_OFF: f32 = 0.0; -pub(crate) const SHADING_MAX: f32 = 1.0; -pub(crate) const LAYER_IFACE_VERSION: u32 = 2; -pub(crate) const LAYER_LINK_INFO: i32 = 0; -pub(crate) const LAYER_DATA_CALLBACK: i32 = 1; - -pub(crate) const MODE_COMPATIBILITY_TYPE: u32 = 1000274002; -pub(crate) const LATENCY_CAPABILITIES_TYPE: u32 = 1000505008; -pub(crate) const MODE_LIST_TYPES: [u32; 2] = [ - MODE_COMPATIBILITY_TYPE, - LATENCY_CAPABILITIES_TYPE, -]; - -pub(crate) const DEPTH_SUFFIX: &str = "bit"; -pub(crate) const PRESENT_UNKNOWN_PREFIX: &str = "present mode "; -pub(crate) const SPACE_UNKNOWN_PREFIX: &str = "color space "; -pub(crate) const ALPHA_UNKNOWN_PREFIX: &str = "composite alpha "; -pub(crate) const FORMAT_UNKNOWN_PREFIX: &str = "format "; - -pub(crate) const FILTER_RETRO: u32 = 0; -pub(crate) const FILTER_BILINEAR: u32 = 1; -pub(crate) const FILTER_TRILINEAR: u32 = 2; - -pub(crate) const MIPMAP_NEAREST: u32 = 0; -pub(crate) const MIPMAP_LINEAR: u32 = 1; - -pub(crate) const TOGGLE_OFF: u32 = 0; -pub(crate) const TOGGLE_ON: u32 = 1; - -pub(crate) const SETTINGS_FROZEN_INFO: &str = "settings loaded and frozen for the life of the process"; -pub(crate) const ANISO_ABSENT_INFO: &str = "the application did not enable samplerAnisotropy, leaving anisotropic filtering alone"; -pub(crate) const SHADING_ABSENT_INFO: &str = "the application did not enable sampleRateShading, leaving sample shading alone"; -pub(crate) const PRESENT_MISS_WARN: &str = "the surface does not support the present mode setting, keeping application choice"; -pub(crate) const PRESENT_EMPTY_WARN: &str = "present mode selection matched no supported mode, keeping every mode"; - -pub(crate) const PRESENT_EXTENDED_INFO: &str = "this present mode comes from an extension and only exists where the application enabled it"; -pub(crate) const ALPHA_MISS_WARN: &str = "the surface does not support the composite alpha setting, keeping application choice"; -pub(crate) const UNOWNED_QUEUE_ERROR: &str = "present on a queue with no registered device"; -pub(crate) const UNOWNED_BUFFER_ERROR: &str = "alpha to coverage on a command buffer with no registered device"; - -pub(crate) const GPU_EMPTY_WARN: &str = "gpu selection matched no device, keeping every device"; -pub(crate) const GROUP_EMPTY_WARN: &str = "gpu selection matched no device group, keeping every group"; -pub(crate) const DEPTH_EMPTY_WARN: &str = "color depth selection matched no surface format, keeping every format"; -pub(crate) const SPACE_EMPTY_WARN: &str = "color space selection matched no surface format, keeping every format"; -pub(crate) const TRANSFER_EMPTY_WARN: &str = "transfer function selection matched no surface format, keeping every format"; -pub(crate) const SPACE_EXTENDED_INFO: &str = "this color space comes from a swapchain colorspace extension and only exists where the stack enabled it"; -pub(crate) const ALPHA_OPAQUE_INFO: &str = "opaque composite alpha skips compositor blending"; -pub(crate) const TRANSFER_ENCODED_INFO: &str = "this transfer function leaves the encoding curve to the display hardware"; -pub(crate) const TRANSFER_SHADER_INFO: &str = "this transfer function leaves the encoding curve to whatever the application does in its own shaders"; -pub(crate) const FORMAT_FORCED_WARN: &str = "the application asked for a color depth the settings exclude, leaving it alone: it may have allocated resources to match"; -pub(crate) const SPACE_FORCED_WARN: &str = "the application asked for a color space the settings exclude, leaving it alone: it may have allocated resources to match"; -pub(crate) const TRANSFER_FORCED_WARN: &str = "the application asked for a transfer function the settings exclude, leaving it alone: it may have allocated resources to match"; - -pub(crate) const NULL_OK: [&str; 4] = [ - "vkCreateInstance", - "vkEnumerateInstanceVersion", - "vkEnumerateInstanceExtensionProperties", - "vkEnumerateInstanceLayerProperties", -]; - -pub(crate) const USAGE: &str = "usage: volt [--probe] [PROFILE] -- COMMAND [ARGS...]\n volt -- CMD run CMD with the default profile (~/.config/volt-gui/default.toml)\n volt NAME -- CMD run CMD with profile ~/.config/volt-gui/NAME.toml\n volt --probe NAME -- CMD the same, and record what this device supports\n\nsettings are read once when the application starts and never change while it\nruns: edit the profile, then start the application again\n"; - -pub(crate) const FN_PRESENT_RECTANGLES: &str = "vkGetPhysicalDevicePresentRectanglesKHR"; - -pub(crate) const FN_SURFACE_CAPS_2: &str = "vkGetPhysicalDeviceSurfaceCapabilities2KHR"; -pub(crate) const FN_SURFACE_FORMATS_2: &str = "vkGetPhysicalDeviceSurfaceFormats2KHR"; -pub(crate) const FN_SURFACE_MODES_2: &str = "vkGetPhysicalDeviceSurfacePresentModes2EXT"; -pub(crate) const FN_DEVICE_GROUPS: &str = "vkEnumeratePhysicalDeviceGroups"; -pub(crate) const FN_DEVICE_GROUPS_KHR: &str = "vkEnumeratePhysicalDeviceGroupsKHR"; -pub(crate) const FN_SHARED_SWAPCHAINS: &str = "vkCreateSharedSwapchainsKHR"; -pub(crate) const FN_WRITE_SAMPLERS: &str = "vkWriteSamplerDescriptorsEXT"; -pub(crate) const FN_SET_ALPHA_COVERAGE: &str = "vkCmdSetAlphaToCoverageEnableEXT"; - -pub(crate) const SECTION_GPU: &str = "gpu"; -pub(crate) const SECTION_DISPLAY: &str = "display"; -pub(crate) const SECTION_TEXTURES: &str = "textures"; -pub(crate) const SECTION_RENDERING: &str = "rendering"; -pub(crate) const SECTION_FRAMERATE: &str = "framerate"; - -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum PacingChoice { - Sleep, - Sliced, - Precise, - Spin, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum MethodChoice { - Early, - Late, - Reactive, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum LimitStage { - Before, - After, -} - -pub(crate) const DEFAULT_CONFIG: &str = r#"# volt profile -# every setting is one value: the value volt forces, or "default", which -# keeps whatever the application asked for. there is no range and no order -# -# a value volt has no name for is written the way the interface shows it, -# and forces exactly like a named one -# -# anisotropy and sample_shading need a device feature the application itself -# enabled. volt never enables one: where the application left the feature -# clear the setting is ignored and a line is logged -# -# a forced value the device did not report is not forced: volt keeps the -# application's own value and logs a warning -# -# settings are read once when the application starts. changing this file has -# no effect on an application that is already running: start it again - -[gpu] -device = "default" - -[display] -present_mode = "default" -image_count = "default" -color_depth = "default" -color_space = "default" -transfer_function = "default" -composite_alpha = "default" -clipped = "default" - -[textures] -filtering = "default" -mipmap_mode = "default" -anisotropy = "default" -lod_bias = "default" -mip_floor = "default" -mip_ceiling = "default" - -[rendering] -sample_shading = "default" -alpha_to_coverage = "default" - -[framerate] -frame_limit = "default" -frame_limit_method = "default" -frame_pacing = "default" -"#; diff --git a/volt/device.rs b/volt/device.rs deleted file mode 100644 index 23551eb..0000000 --- a/volt/device.rs +++ /dev/null @@ -1,378 +0,0 @@ -use std::collections::HashMap; -use std::ffi::c_void; -use std::mem; -use std::ptr; -use std::sync::Arc; -use std::sync::RwLock; - -use ash::vk; -use ash::vk::Handle; - -use crate::consts::FN_PRESENT_RECTANGLES; -use crate::consts::FN_SET_ALPHA_COVERAGE; -use crate::consts::FN_SHARED_SWAPCHAINS; -use crate::consts::FN_WRITE_SAMPLERS; -use crate::instance::call_next_gdpa; -use crate::instance::call_next_gipa; -use crate::instance::owning_instance; -use crate::instance::PfnCmdSetAlphaToCoverage; -use crate::instance::PfnCreateSharedSwapchains; -use crate::instance::PfnSetDeviceLoaderData; -use crate::instance::PfnWriteSamplers; -use crate::instance::VkChainNode; -use crate::instance::VkInstState; -use crate::instance::VkLayerLinkInfo; -use crate::logging::log_at; -use crate::logging::LogLevel; - -#[derive(Clone, Copy, Default)] -pub(crate) struct DeviceCaps { - pub(crate) sampler_anisotropy: bool, - pub(crate) sample_rate_shading: bool, - pub(crate) max_anisotropy: f32, - pub(crate) max_lod_bias: f32, - pub(crate) max_lod_level: f32, -} - -pub(crate) struct VkDevState { - pub(crate) device: ash::Device, - pub(crate) phys: vk::PhysicalDevice, - pub(crate) gdpa: vk::PFN_vkGetDeviceProcAddr, - pub(crate) loader_data: Option, - pub(crate) swap_fp: vk::KhrSwapchainFn, - pub(crate) shared_fp: Option, - pub(crate) samplers_fp: Option, - pub(crate) alpha_fp: Option, - pub(crate) caps: DeviceCaps, - pub(crate) instance_handle: u64, -} - -static DEVS: RwLock>>> = RwLock::new(None); -static QUEUE_TO_DEV: RwLock>> = RwLock::new(None); -static CMDBUF_TO_DEV: RwLock>> = RwLock::new(None); - -pub(crate) fn devs_get(h: u64) -> Option> { - DEVS.read() - .ok() - .and_then(|g| g.as_ref().and_then(|m| m.get(&h).cloned())) -} - -pub(crate) fn devs_gdpa(h: u64) -> Option { - DEVS.read() - .ok() - .and_then(|g| g.as_ref().and_then(|m| m.get(&h).map(|d| d.gdpa))) -} - -pub(crate) fn devs_put(h: u64, v: VkDevState) { - match DEVS.write() { - Ok(mut g) => { - g.get_or_insert_with(HashMap::new).insert(h, Arc::new(v)); - } - Err(_) => (), - } -} - -fn queue_dev_forget(dev: u64) { - match QUEUE_TO_DEV.write() { - Ok(mut g) => g - .iter_mut() - .for_each(|m| m.retain(|_, owner| *owner != dev)), - Err(_) => (), - } -} - -fn cmdbuf_dev_forget(dev: u64) { - match CMDBUF_TO_DEV.write() { - Ok(mut g) => g - .iter_mut() - .for_each(|m| m.retain(|_, owner| owner.0 != dev)), - Err(_) => (), - } -} - -pub(crate) fn devs_del(h: u64) -> Option> { - queue_dev_forget(h); - cmdbuf_dev_forget(h); - DEVS.write() - .ok() - .and_then(|mut g| g.as_mut().and_then(|m| m.remove(&h))) -} - -fn queue_dev_get(q: u64) -> Option { - QUEUE_TO_DEV - .read() - .ok() - .and_then(|g| g.as_ref().and_then(|m| m.get(&q).copied())) -} - -pub(crate) fn queue_dev_put(q: u64, d: u64) { - match QUEUE_TO_DEV.write() { - Ok(mut g) => { - g.get_or_insert_with(HashMap::new).insert(q, d); - } - Err(_) => (), - } -} - -pub(crate) fn queue_owner(queue: vk::Queue) -> Option> { - queue_dev_get(queue.as_raw()).and_then(devs_get) -} - -fn cmdbuf_dev_get(c: u64) -> Option { - CMDBUF_TO_DEV - .read() - .ok() - .and_then(|g| g.as_ref().and_then(|m| m.get(&c).map(|owner| owner.0))) -} - -fn cmdbuf_dev_put(c: u64, owner: (u64, u64)) { - match CMDBUF_TO_DEV.write() { - Ok(mut g) => { - g.get_or_insert_with(HashMap::new).insert(c, owner); - } - Err(_) => (), - } -} - -fn cmdbuf_dev_del(c: u64) { - match CMDBUF_TO_DEV.write() { - Ok(mut g) => { - g.get_or_insert_with(HashMap::new).remove(&c); - } - Err(_) => (), - } -} - -fn cmdbuf_pool_forget(pool: u64) { - match CMDBUF_TO_DEV.write() { - Ok(mut g) => g - .iter_mut() - .for_each(|m| m.retain(|_, owner| owner.1 != pool)), - Err(_) => (), - } -} - -pub(crate) fn cmdbuf_owner(buffer: vk::CommandBuffer) -> Option> { - cmdbuf_dev_get(buffer.as_raw()).and_then(devs_get) -} - -fn lod_levels_for(max_dimension: u32) -> f32 { - (max_dimension.max(1) as f32).log2().floor() -} - -fn build_caps( - props: &vk::PhysicalDeviceProperties, - asked: &vk::PhysicalDeviceFeatures, -) -> DeviceCaps { - DeviceCaps { - sampler_anisotropy: asked.sampler_anisotropy == vk::TRUE, - sample_rate_shading: asked.sample_rate_shading == vk::TRUE, - max_anisotropy: props.limits.max_sampler_anisotropy, - max_lod_bias: props.limits.max_sampler_lod_bias, - max_lod_level: lod_levels_for(props.limits.max_image_dimension2_d), - } -} - -fn non_null_node(p: *const c_void) -> Option<*const VkChainNode> { - match p.is_null() { - true => None, - false => Some(p as *const VkChainNode), - } -} - -fn chained_features(p_next: *const c_void) -> Option { - std::iter::successors(non_null_node(p_next), |node| { - non_null_node(unsafe { (**node).p_next as *const c_void }) - }) - .find(|node| unsafe { (**node).s_type } == vk::StructureType::PHYSICAL_DEVICE_FEATURES_2) - .map(|node| unsafe { (*(node as *const vk::PhysicalDeviceFeatures2)).features }) -} - -fn plain_features(ci: &vk::DeviceCreateInfo) -> Option { - match ci.p_enabled_features.is_null() { - true => None, - false => Some(unsafe { *ci.p_enabled_features }), - } -} - -fn asked_features(ci: *const vk::DeviceCreateInfo) -> vk::PhysicalDeviceFeatures { - match chained_features(unsafe { (*ci).p_next }) { - Some(features) => features, - None => plain_features(unsafe { &*ci }).unwrap_or_default(), - } -} - -fn name_is_device_level(name: &std::ffi::CStr) -> bool { - name.to_str().map(|s| s != FN_PRESENT_RECTANGLES).unwrap_or(true) -} - -fn load_swap_fp(gdpa: vk::PFN_vkGetDeviceProcAddr, handle: vk::Device) -> vk::KhrSwapchainFn { - vk::KhrSwapchainFn::load(|name| match name_is_device_level(name) { - true => unsafe { mem::transmute(gdpa(handle, name.as_ptr())) }, - false => ptr::null(), - }) -} - -fn call_typed_device_fp( - gdpa: vk::PFN_vkGetDeviceProcAddr, - handle: vk::Device, - name: &str, -) -> Option { - call_next_gdpa(gdpa, handle, name).map(|f| unsafe { mem::transmute_copy(&f) }) -} - -fn call_loader_data(fp: PfnSetDeviceLoaderData, handle: vk::Device, queue: vk::Queue) { - let _ = unsafe { fp(handle, queue.as_raw() as usize as *mut c_void) }; -} - -pub(crate) fn call_register_queue(dev: &VkDevState, handle: vk::Device, queue: vk::Queue) { - match dev.loader_data { - Some(fp) => call_loader_data(fp, handle, queue), - None => (), - } -} - -fn device_caps( - inst: &VkInstState, - phys: vk::PhysicalDevice, - ci: *const vk::DeviceCreateInfo, -) -> DeviceCaps { - build_caps( - unsafe { &inst.instance.get_physical_device_properties(phys) }, - &asked_features(ci), - ) -} - -fn call_record_command_buffers( - dev: vk::Device, - info: *const vk::CommandBufferAllocateInfo, - out: *mut vk::CommandBuffer, -) { - (0..unsafe { (*info).command_buffer_count } as usize).for_each(|at| { - cmdbuf_dev_put( - unsafe { (*out.add(at)).as_raw() }, - (dev.as_raw(), unsafe { (*info).command_pool.as_raw() }), - ) - }); -} - -fn call_forget_command_buffers(count: u32, buffers: *const vk::CommandBuffer) { - (0..count as usize).for_each(|at| cmdbuf_dev_del(unsafe { (*buffers.add(at)).as_raw() })); -} - -pub(crate) fn call_allocate_command_buffers( - d: &VkDevState, - dev: vk::Device, - info: *const vk::CommandBufferAllocateInfo, - out: *mut vk::CommandBuffer, -) -> vk::Result { - match unsafe { (d.device.fp_v1_0().allocate_command_buffers)(dev, info, out) } { - vk::Result::SUCCESS => { - call_record_command_buffers(dev, info, out); - vk::Result::SUCCESS - } - e => e, - } -} - -pub(crate) fn call_free_command_buffers( - d: &VkDevState, - dev: vk::Device, - pool: vk::CommandPool, - count: u32, - buffers: *const vk::CommandBuffer, -) { - call_forget_command_buffers(count, buffers); - unsafe { (d.device.fp_v1_0().free_command_buffers)(dev, pool, count, buffers) }; -} - -pub(crate) fn call_destroy_command_pool( - d: &VkDevState, - dev: vk::Device, - pool: vk::CommandPool, - alloc: *const vk::AllocationCallbacks, -) { - cmdbuf_pool_forget(pool.as_raw()); - unsafe { (d.device.fp_v1_0().destroy_command_pool)(dev, pool, alloc) }; -} - -fn register_device( - gdpa: vk::PFN_vkGetDeviceProcAddr, - loader_data: Option, - handle: vk::Device, - inst: &VkInstState, - inst_handle: u64, - phys: vk::PhysicalDevice, - caps: DeviceCaps, -) { - let mut inst_fp = inst.instance.fp_v1_0().clone(); - inst_fp.get_device_proc_addr = gdpa; - let device = unsafe { ash::Device::load(&inst_fp, handle) }; - devs_put( - handle.as_raw(), - VkDevState { - device, - phys, - gdpa, - loader_data, - swap_fp: load_swap_fp(gdpa, handle), - shared_fp: call_typed_device_fp(gdpa, handle, FN_SHARED_SWAPCHAINS), - samplers_fp: call_typed_device_fp(gdpa, handle, FN_WRITE_SAMPLERS), - alpha_fp: call_typed_device_fp(gdpa, handle, FN_SET_ALPHA_COVERAGE), - caps, - instance_handle: inst_handle, - }, - ); - log_at(LogLevel::Info, "vk device registered"); -} - -fn invoke_create_device( - create_fn: unsafe extern "system" fn(), - link: &VkLayerLinkInfo, - loader_data: Option, - inst: &VkInstState, - inst_handle: u64, - phys: vk::PhysicalDevice, - ci: *const vk::DeviceCreateInfo, - alloc: *const vk::AllocationCallbacks, - out: *mut vk::Device, -) -> vk::Result { - match unsafe { - let cf: vk::PFN_vkCreateDevice = mem::transmute(create_fn); - cf(phys, ci, alloc, out) - } { - vk::Result::SUCCESS => { - register_device( - link.pfn_next_get_device_proc_addr, - loader_data, - unsafe { *out }, - inst, - inst_handle, - phys, - device_caps(inst, phys, ci), - ); - vk::Result::SUCCESS - } - e => e, - } -} - -pub(crate) fn call_real_create_device( - link: Option, - loader_data: Option, - phys: vk::PhysicalDevice, - ci: *const vk::DeviceCreateInfo, - alloc: *const vk::AllocationCallbacks, - out: *mut vk::Device, -) -> vk::Result { - match (link, owning_instance(phys)) { - (Some(l), Some((ih, inst))) => call_next_gipa( - l.pfn_next_get_instance_proc_addr, - vk::Instance::from_raw(ih), - "vkCreateDevice", - ) - .map(|f| invoke_create_device(f, &l, loader_data, &inst, ih, phys, ci, alloc, out)) - .unwrap_or(vk::Result::ERROR_INITIALIZATION_FAILED), - (_, _) => vk::Result::ERROR_INITIALIZATION_FAILED, - } -} diff --git a/volt/instance.rs b/volt/instance.rs deleted file mode 100644 index 4629820..0000000 --- a/volt/instance.rs +++ /dev/null @@ -1,502 +0,0 @@ -use std::collections::HashMap; -use std::ffi::c_void; -use std::ffi::CString; -use std::mem; -use std::ptr; -use std::sync::RwLock; - -use ash::vk; -use ash::vk::Handle; - -use crate::config::ensure_settings; -use crate::consts::FN_DEVICE_GROUPS; -use crate::consts::FN_DEVICE_GROUPS_KHR; -use crate::consts::FN_SURFACE_CAPS_2; -use crate::consts::FN_SURFACE_FORMATS_2; -use crate::consts::FN_SURFACE_MODES_2; -use crate::consts::GPU_EMPTY_WARN; -use crate::consts::GROUP_EMPTY_WARN; -use crate::lists::filtered; -use crate::lists::kept; -use crate::logging::log_at; -use crate::logging::LogLevel; - -pub(crate) const SURFACE_FORMAT_2: u32 = 1000119002; - -pub(crate) type PfnSurfaceCaps2 = unsafe extern "system" fn( - vk::PhysicalDevice, - *const VkPhysicalDeviceSurfaceInfo2, - *mut VkSurfaceCapabilities2, -) -> vk::Result; - -pub(crate) type PfnSurfaceFormats2 = unsafe extern "system" fn( - vk::PhysicalDevice, - *const VkPhysicalDeviceSurfaceInfo2, - *mut u32, - *mut VkSurfaceFormat2, -) -> vk::Result; - -pub(crate) type PfnSurfaceModes2 = unsafe extern "system" fn( - vk::PhysicalDevice, - *const VkPhysicalDeviceSurfaceInfo2, - *mut u32, - *mut vk::PresentModeKHR, -) -> vk::Result; - -pub(crate) type PfnDeviceGroups = unsafe extern "system" fn( - vk::Instance, - *mut u32, - *mut vk::PhysicalDeviceGroupProperties, -) -> vk::Result; - -pub(crate) type PfnCreateSharedSwapchains = unsafe extern "system" fn( - vk::Device, - u32, - *const vk::SwapchainCreateInfoKHR, - *const vk::AllocationCallbacks, - *mut vk::SwapchainKHR, -) -> vk::Result; - -pub(crate) type PfnWriteSamplers = unsafe extern "system" fn( - vk::Device, - u32, - *const vk::SamplerCreateInfo, - *const c_void, -) -> vk::Result; - -pub(crate) type PfnCmdSetAlphaToCoverage = - unsafe extern "system" fn(vk::CommandBuffer, vk::Bool32); - -pub(crate) type PfnSetDeviceLoaderData = - unsafe extern "system" fn(vk::Device, *mut c_void) -> vk::Result; - -#[repr(C)] -pub(crate) struct VkLayerLink { - pub(crate) p_next: *mut VkLayerLink, - pub(crate) pfn_next_get_instance_proc_addr: vk::PFN_vkGetInstanceProcAddr, - pub(crate) pfn_next_get_device_proc_addr: vk::PFN_vkGetDeviceProcAddr, -} - -pub(crate) struct VkLayerLinkInfo { - pub(crate) pfn_next_get_instance_proc_addr: vk::PFN_vkGetInstanceProcAddr, - pub(crate) pfn_next_get_device_proc_addr: vk::PFN_vkGetDeviceProcAddr, -} - -#[repr(C)] -pub(crate) struct VkLayerCreateInfo { - pub(crate) s_type: vk::StructureType, - pub(crate) p_next: *const c_void, - pub(crate) function: i32, - pub(crate) u_layer_info: *mut VkLayerLink, -} - -#[repr(C)] -#[derive(Clone, Copy)] -pub(crate) struct VkPhysicalDeviceSurfaceInfo2 { - pub(crate) s_type: vk::StructureType, - pub(crate) p_next: *const c_void, - pub(crate) surface: vk::SurfaceKHR, -} - -#[repr(C)] -pub(crate) struct VkSurfaceCapabilities2 { - pub(crate) s_type: vk::StructureType, - pub(crate) p_next: *mut c_void, - pub(crate) surface_capabilities: vk::SurfaceCapabilitiesKHR, -} - -#[repr(C)] -#[derive(Clone, Copy)] -pub(crate) struct VkSurfaceFormat2 { - pub(crate) s_type: vk::StructureType, - pub(crate) p_next: *mut c_void, - pub(crate) surface_format: vk::SurfaceFormatKHR, -} - -#[repr(C)] -#[derive(Clone, Copy)] -pub(crate) struct VkChainNode { - pub(crate) s_type: vk::StructureType, - pub(crate) p_next: *mut c_void, -} - -#[repr(C)] -#[derive(Clone, Copy)] -pub(crate) struct VkPresentModeList { - pub(crate) s_type: vk::StructureType, - pub(crate) p_next: *mut c_void, - pub(crate) present_mode_count: u32, - pub(crate) p_present_modes: *mut vk::PresentModeKHR, -} - -#[derive(Clone)] -pub(crate) struct VkInstState { - pub(crate) instance: ash::Instance, - pub(crate) gipa: vk::PFN_vkGetInstanceProcAddr, - pub(crate) surface_fp: vk::KhrSurfaceFn, - pub(crate) caps2_fp: Option, - pub(crate) formats2_fp: Option, - pub(crate) modes2_fp: Option, - pub(crate) groups_fp: Option, - pub(crate) groups_khr_fp: Option, -} - -static INSTS: RwLock>> = RwLock::new(None); -static PHYS_OWNER: RwLock>> = RwLock::new(None); - -fn phys_owner_get(phys: u64) -> Option { - PHYS_OWNER - .read() - .ok() - .and_then(|g| g.as_ref().and_then(|m| m.get(&phys).copied())) -} - -fn phys_owner_put(phys: u64, inst: u64) { - match PHYS_OWNER.write() { - Ok(mut g) => { - g.get_or_insert_with(HashMap::new).insert(phys, inst); - } - Err(_) => (), - } -} - -fn phys_owner_forget(inst: u64) { - match PHYS_OWNER.write() { - Ok(mut g) => g - .iter_mut() - .for_each(|m| m.retain(|_, owner| *owner != inst)), - Err(_) => (), - } -} - -pub(crate) fn insts_get(h: u64) -> Option { - INSTS.read().ok().and_then(|g| g.as_ref().and_then(|m| m.get(&h).cloned())) -} - -pub(crate) fn insts_put(h: u64, v: VkInstState) { - match INSTS.write() { - Ok(mut g) => { - g.get_or_insert_with(HashMap::new).insert(h, v); - } - Err(_) => (), - } -} - -pub(crate) fn insts_del(h: u64) { - phys_owner_forget(h); - match INSTS.write() { - Ok(mut g) => { - g.get_or_insert_with(HashMap::new).remove(&h); - } - Err(_) => (), - } -} - -pub(crate) fn owning_instance(phys: vk::PhysicalDevice) -> Option<(u64, VkInstState)> { - phys_owner_get(phys.as_raw()).and_then(|h| insts_get(h).map(|st| (h, st))) -} - -fn indexed(devices: Vec) -> Vec<(usize, vk::PhysicalDevice)> { - devices.into_iter().enumerate().collect() -} - -fn plain(pairs: Vec<(usize, vk::PhysicalDevice)>) -> Vec { - pairs.into_iter().map(|(_, device)| device).collect() -} - -fn device_position(pair: &(usize, vk::PhysicalDevice)) -> u32 { - pair.0 as u32 + 1 -} - -fn gpu_filtered( - devices: Vec, - choice: Option, -) -> Vec { - plain(filtered( - indexed(devices), - choice, - |pair| Some(device_position(pair)), - GPU_EMPTY_WARN, - )) -} - -fn group_devices(group: &vk::PhysicalDeviceGroupProperties) -> Vec { - group.physical_devices[..group.physical_device_count as usize].to_vec() -} - -fn group_wanted( - group: &vk::PhysicalDeviceGroupProperties, - allowed: &[vk::PhysicalDevice], -) -> bool { - group_devices(group) - .into_iter() - .any(|device| allowed.contains(&device)) -} - -fn group_filtered( - groups: Vec, - allowed: Vec, - choice: Option, -) -> Vec { - match choice { - Some(_) => kept(groups, |group| group_wanted(group, &allowed), GROUP_EMPTY_WARN), - None => groups, - } -} - -fn copy_count(requested: u32, available: usize) -> usize { - (requested as usize).min(available) -} - -fn completeness(written: usize, available: usize) -> vk::Result { - match written == available { - true => vk::Result::SUCCESS, - false => vk::Result::INCOMPLETE, - } -} - -fn non_null_ci(p: *const VkLayerCreateInfo) -> Option<*const VkLayerCreateInfo> { - match p.is_null() { - true => None, - false => Some(p), - } -} - -pub(crate) fn chain_layer_info( - p_next: *const c_void, - want: vk::StructureType, - function: i32, -) -> *mut VkLayerCreateInfo { - std::iter::successors(non_null_ci(p_next as *const VkLayerCreateInfo), |p| { - non_null_ci(unsafe { (**p).p_next as *const VkLayerCreateInfo }) - }) - .find(|p| unsafe { (**p).s_type == want && (**p).function == function }) - .map(|p| p as *mut VkLayerCreateInfo) - .unwrap_or(ptr::null_mut()) -} - -pub(crate) fn call_loader_data_fn(node: *mut VkLayerCreateInfo) -> Option { - unsafe { node.as_ref() } - .map(|info| info.u_layer_info) - .filter(|link| !link.is_null()) - .map(|link| unsafe { mem::transmute(link) }) -} - -pub(crate) fn call_advance_chain(link: *mut VkLayerCreateInfo) -> Option { - match link.is_null() || unsafe { (*link).u_layer_info.is_null() } { - true => None, - false => unsafe { - let li = (*link).u_layer_info; - let out = VkLayerLinkInfo { - pfn_next_get_instance_proc_addr: (*li).pfn_next_get_instance_proc_addr, - pfn_next_get_device_proc_addr: (*li).pfn_next_get_device_proc_addr, - }; - (*link).u_layer_info = (*li).p_next; - Some(out) - }, - } -} - -pub(crate) fn call_next_gipa(gipa: vk::PFN_vkGetInstanceProcAddr, inst: vk::Instance, name: &str) -> vk::PFN_vkVoidFunction { - let c = CString::new(name).unwrap_or_default(); - unsafe { gipa(inst, c.as_ptr()) } -} - -pub(crate) fn call_next_gdpa(gdpa: vk::PFN_vkGetDeviceProcAddr, dev: vk::Device, name: &str) -> vk::PFN_vkVoidFunction { - let c = CString::new(name).unwrap_or_default(); - unsafe { gdpa(dev, c.as_ptr()) } -} - -fn call_write_count(list: &[T], count: *mut u32) -> vk::Result { - unsafe { *count = list.len() as u32 }; - vk::Result::SUCCESS -} - -fn call_write_items(list: &[T], count: *mut u32, out: *mut T) -> vk::Result { - let n = copy_count(unsafe { *count }, list.len()); - (0..n).for_each(|i| unsafe { *out.add(i) = list[i] }); - unsafe { *count = n as u32 }; - completeness(n, list.len()) -} - -pub(crate) fn call_write_list(list: &[T], count: *mut u32, out: *mut T) -> vk::Result { - match out.is_null() { - true => call_write_count(list, count), - false => call_write_items(list, count, out), - } -} - -fn call_enumerate_through( - st: &VkInstState, - count: *mut u32, - devices: *mut vk::PhysicalDevice, -) -> vk::Result { - match unsafe { st.instance.enumerate_physical_devices() } { - Ok(all) => call_write_list(&gpu_filtered(all, ensure_settings().gpu), count, devices), - Err(e) => e, - } -} - -pub(crate) fn call_filtered_enumerate( - inst: vk::Instance, - count: *mut u32, - devices: *mut vk::PhysicalDevice, -) -> vk::Result { - match insts_get(inst.as_raw()) { - None => vk::Result::ERROR_INITIALIZATION_FAILED, - Some(st) => call_enumerate_through(&st, count, devices), - } -} - -fn call_query_groups( - handle: vk::Instance, - fp: PfnDeviceGroups, -) -> Vec { - let mut n: u32 = 0; - let r1 = unsafe { fp(handle, &mut n, ptr::null_mut()) }; - let mut v: Vec = - (0..n).map(|_| Default::default()).collect(); - let r2 = unsafe { fp(handle, &mut n, v.as_mut_ptr()) }; - match (r1, r2) { - (vk::Result::SUCCESS, vk::Result::SUCCESS) => v, - (_, _) => Vec::new(), - } -} - -fn call_allowed_devices(st: &VkInstState) -> Vec { - gpu_filtered( - unsafe { st.instance.enumerate_physical_devices() }.unwrap_or_default(), - ensure_settings().gpu, - ) -} - -fn call_groups_through( - st: &VkInstState, - handle: vk::Instance, - fp: PfnDeviceGroups, - count: *mut u32, - groups: *mut vk::PhysicalDeviceGroupProperties, -) -> vk::Result { - call_write_list( - &group_filtered( - call_query_groups(handle, fp), - call_allowed_devices(st), - ensure_settings().gpu, - ), - count, - groups, - ) -} - -fn call_groups_with( - inst: vk::Instance, - found: Option<(VkInstState, PfnDeviceGroups)>, - count: *mut u32, - groups: *mut vk::PhysicalDeviceGroupProperties, -) -> vk::Result { - match found { - None => vk::Result::ERROR_INITIALIZATION_FAILED, - Some((st, fp)) => call_groups_through(&st, inst, fp, count, groups), - } -} - -pub(crate) fn call_filtered_groups( - inst: vk::Instance, - count: *mut u32, - groups: *mut vk::PhysicalDeviceGroupProperties, -) -> vk::Result { - call_groups_with( - inst, - insts_get(inst.as_raw()).and_then(|st| st.groups_fp.map(|fp| (st, fp))), - count, - groups, - ) -} - -pub(crate) fn call_filtered_groups_khr( - inst: vk::Instance, - count: *mut u32, - groups: *mut vk::PhysicalDeviceGroupProperties, -) -> vk::Result { - call_groups_with( - inst, - insts_get(inst.as_raw()).and_then(|st| st.groups_khr_fp.map(|fp| (st, fp))), - count, - groups, - ) -} - -fn load_surface_fp(gipa: vk::PFN_vkGetInstanceProcAddr, handle: vk::Instance) -> vk::KhrSurfaceFn { - vk::KhrSurfaceFn::load(|name| unsafe { mem::transmute(gipa(handle, name.as_ptr())) }) -} - -fn call_typed_instance_fp( - gipa: vk::PFN_vkGetInstanceProcAddr, - handle: vk::Instance, - name: &str, -) -> Option { - call_next_gipa(gipa, handle, name).map(|f| unsafe { mem::transmute_copy(&f) }) -} - -fn call_owned_devices(instance: &ash::Instance) -> Vec { - unsafe { instance.enumerate_physical_devices() }.unwrap_or_default() -} - -fn call_remember_owner(handle: vk::Instance, devices: Vec) { - devices - .into_iter() - .for_each(|phys| phys_owner_put(phys.as_raw(), handle.as_raw())); -} - -fn register_instance(gipa: vk::PFN_vkGetInstanceProcAddr, handle: vk::Instance) { - let static_fn = vk::StaticFn { get_instance_proc_addr: gipa }; - let instance = unsafe { ash::Instance::load(&static_fn, handle) }; - call_remember_owner(handle, call_owned_devices(&instance)); - insts_put( - handle.as_raw(), - VkInstState { - instance, - gipa, - surface_fp: load_surface_fp(gipa, handle), - caps2_fp: call_typed_instance_fp(gipa, handle, FN_SURFACE_CAPS_2), - formats2_fp: call_typed_instance_fp(gipa, handle, FN_SURFACE_FORMATS_2), - modes2_fp: call_typed_instance_fp(gipa, handle, FN_SURFACE_MODES_2), - groups_fp: call_typed_instance_fp(gipa, handle, FN_DEVICE_GROUPS), - groups_khr_fp: call_typed_instance_fp(gipa, handle, FN_DEVICE_GROUPS_KHR), - }, - ); - log_at(LogLevel::Info, "vk instance registered"); -} - -fn invoke_create_instance( - create_fn: unsafe extern "system" fn(), - gipa: vk::PFN_vkGetInstanceProcAddr, - ci: *const vk::InstanceCreateInfo, - alloc: *const vk::AllocationCallbacks, - out: *mut vk::Instance, -) -> vk::Result { - match unsafe { - let cf: vk::PFN_vkCreateInstance = mem::transmute(create_fn); - cf(ci, alloc, out) - } { - vk::Result::SUCCESS => { - register_instance(gipa, unsafe { *out }); - vk::Result::SUCCESS - } - e => e, - } -} - -pub(crate) fn call_real_create_instance( - link: Option, - ci: *const vk::InstanceCreateInfo, - alloc: *const vk::AllocationCallbacks, - out: *mut vk::Instance, -) -> vk::Result { - match link { - None => vk::Result::ERROR_INITIALIZATION_FAILED, - Some(l) => call_next_gipa(l.pfn_next_get_instance_proc_addr, vk::Instance::null(), "vkCreateInstance") - .map(|f| invoke_create_instance(f, l.pfn_next_get_instance_proc_addr, ci, alloc, out)) - .unwrap_or(vk::Result::ERROR_INITIALIZATION_FAILED), - } -} diff --git a/volt/layer.rs b/volt/layer.rs deleted file mode 100644 index b6319bf..0000000 --- a/volt/layer.rs +++ /dev/null @@ -1,605 +0,0 @@ -use std::ffi::c_char; -use std::ffi::c_void; -use std::ffi::CStr; -use std::mem; -use std::ptr; -use std::sync::Arc; - -use ash::vk; -use ash::vk::Handle; - -use crate::config::ensure_settings; -use crate::config::Settings; -use crate::consts::FN_DEVICE_GROUPS; -use crate::consts::FN_DEVICE_GROUPS_KHR; -use crate::consts::FN_SET_ALPHA_COVERAGE; -use crate::consts::FN_SHARED_SWAPCHAINS; -use crate::consts::FN_SURFACE_CAPS_2; -use crate::consts::FN_SURFACE_FORMATS_2; -use crate::consts::FN_SURFACE_MODES_2; -use crate::consts::FN_WRITE_SAMPLERS; -use crate::consts::LAYER_DATA_CALLBACK; -use crate::consts::LAYER_DESC; -use crate::consts::LAYER_IFACE_VERSION; -use crate::consts::LAYER_LINK_INFO; -use crate::consts::LAYER_NAME; -use crate::consts::LimitStage; -use crate::consts::NULL_OK; -use crate::consts::UNOWNED_QUEUE_ERROR; -use crate::device::call_allocate_command_buffers; -use crate::device::call_destroy_command_pool; -use crate::device::call_free_command_buffers; -use crate::device::call_real_create_device; -use crate::device::call_register_queue; -use crate::device::cmdbuf_owner; -use crate::device::devs_del; -use crate::device::devs_gdpa; -use crate::device::devs_get; -use crate::device::queue_dev_put; -use crate::device::queue_owner; -use crate::device::VkDevState; -use crate::instance::call_advance_chain; -use crate::instance::call_filtered_enumerate; -use crate::instance::call_filtered_groups; -use crate::instance::call_filtered_groups_khr; -use crate::instance::call_loader_data_fn; -use crate::instance::call_next_gdpa; -use crate::instance::call_next_gipa; -use crate::instance::call_real_create_instance; -use crate::instance::chain_layer_info; -use crate::instance::insts_del; -use crate::instance::insts_get; -use crate::instance::VkPhysicalDeviceSurfaceInfo2; -use crate::instance::VkSurfaceCapabilities2; -use crate::instance::VkSurfaceFormat2; -use crate::logging::init_log_level; -use crate::logging::log_at; -use crate::logging::LogLevel; -use crate::pipeline::call_create_graphics_pipelines; -use crate::pipeline::call_set_alpha_coverage; -use crate::present::call_forget_timeline; -use crate::present::call_present_frame; -use crate::present::maybe_limit_frame; -use crate::sampler::call_create_sampler; -use crate::sampler::call_write_sampler_descriptors; -use crate::swapchain::call_create_shared_swapchains; -use crate::swapchain::call_create_swapchain; -use crate::swapchain::call_surface_capabilities; -use crate::swapchain::call_surface_capabilities2; -use crate::swapchain::call_surface_formats; -use crate::swapchain::call_surface_formats2; -use crate::swapchain::call_surface_present_modes; -use crate::swapchain::call_surface_present_modes2; - -#[repr(C)] -struct VkNegotiateLayerInterface { - s_type: i32, - p_next: *mut c_void, - loader_layer_interface_version: u32, - pfn_get_instance_proc_addr: Option, - pfn_get_device_proc_addr: Option, - pfn_get_physical_device_proc_addr: Option, -} - -fn cstr_to_str<'a>(p: *const c_char) -> &'a str { - match p.is_null() { - true => "", - false => unsafe { CStr::from_ptr(p).to_str().unwrap_or("") }, - } -} - -fn null_ok_name(name: &str) -> bool { - NULL_OK.contains(&name) -} - -fn vk_hooked_symbol(name: &str) -> Option<*mut c_void> { - match name { - "vkGetInstanceProcAddr" => Some(vkGetInstanceProcAddr as *mut c_void), - "vkGetDeviceProcAddr" => Some(vkGetDeviceProcAddr as *mut c_void), - "vkCreateInstance" => Some(vkCreateInstance as *mut c_void), - "vkDestroyInstance" => Some(vkDestroyInstance as *mut c_void), - "vkCreateDevice" => Some(vkCreateDevice as *mut c_void), - "vkDestroyDevice" => Some(vkDestroyDevice as *mut c_void), - "vkEnumeratePhysicalDevices" => Some(vkEnumeratePhysicalDevices as *mut c_void), - "vkCreateGraphicsPipelines" => Some(vkCreateGraphicsPipelines as *mut c_void), - "vkCreateSampler" => Some(vkCreateSampler as *mut c_void), - "vkAllocateCommandBuffers" => Some(vkAllocateCommandBuffers as *mut c_void), - "vkFreeCommandBuffers" => Some(vkFreeCommandBuffers as *mut c_void), - "vkDestroyCommandPool" => Some(vkDestroyCommandPool as *mut c_void), - "vkCreateSwapchainKHR" => Some(vkCreateSwapchainKHR as *mut c_void), - "vkDestroySwapchainKHR" => Some(vkDestroySwapchainKHR as *mut c_void), - "vkGetPhysicalDeviceSurfaceFormatsKHR" => Some(vkGetPhysicalDeviceSurfaceFormatsKHR as *mut c_void), - "vkGetPhysicalDeviceSurfacePresentModesKHR" => Some(vkGetPhysicalDeviceSurfacePresentModesKHR as *mut c_void), - "vkGetPhysicalDeviceSurfaceCapabilitiesKHR" => Some(vkGetPhysicalDeviceSurfaceCapabilitiesKHR as *mut c_void), - "vkQueuePresentKHR" => Some(vkQueuePresentKHR as *mut c_void), - "vkGetDeviceQueue" => Some(volt_GetDeviceQueue as *mut c_void), - "vkGetDeviceQueue2" => Some(volt_GetDeviceQueue2 as *mut c_void), - _ => None, - } -} - -fn instance_extension_hook(name: &str) -> Option<*mut c_void> { - match name { - FN_SURFACE_CAPS_2 => Some(vkGetPhysicalDeviceSurfaceCapabilities2KHR as *mut c_void), - FN_SURFACE_FORMATS_2 => Some(vkGetPhysicalDeviceSurfaceFormats2KHR as *mut c_void), - FN_SURFACE_MODES_2 => Some(vkGetPhysicalDeviceSurfacePresentModes2EXT as *mut c_void), - FN_DEVICE_GROUPS => Some(vkEnumeratePhysicalDeviceGroups as *mut c_void), - FN_DEVICE_GROUPS_KHR => Some(vkEnumeratePhysicalDeviceGroupsKHR as *mut c_void), - _ => None, - } -} - -fn instance_fp_present(inst: vk::Instance, name: &str) -> bool { - match (insts_get(inst.as_raw()), name) { - (Some(st), FN_SURFACE_CAPS_2) => st.caps2_fp.is_some(), - (Some(st), FN_SURFACE_FORMATS_2) => st.formats2_fp.is_some(), - (Some(st), FN_SURFACE_MODES_2) => st.modes2_fp.is_some(), - (Some(st), FN_DEVICE_GROUPS) => st.groups_fp.is_some(), - (Some(st), FN_DEVICE_GROUPS_KHR) => st.groups_khr_fp.is_some(), - (_, _) => false, - } -} - -fn instance_hooked_symbol(inst: vk::Instance, name: &str) -> Option<*mut c_void> { - instance_extension_hook(name).filter(|_| instance_fp_present(inst, name)) -} - -fn device_extension_hook(name: &str) -> Option<*mut c_void> { - match name { - FN_SHARED_SWAPCHAINS => Some(vkCreateSharedSwapchainsKHR as *mut c_void), - FN_WRITE_SAMPLERS => Some(vkWriteSamplerDescriptorsEXT as *mut c_void), - FN_SET_ALPHA_COVERAGE => Some(vkCmdSetAlphaToCoverageEnableEXT as *mut c_void), - _ => None, - } -} - -fn device_fp_present(dev: vk::Device, name: &str) -> bool { - match (devs_get(dev.as_raw()), name) { - (Some(d), FN_SHARED_SWAPCHAINS) => d.shared_fp.is_some(), - (Some(d), FN_WRITE_SAMPLERS) => d.samplers_fp.is_some(), - (Some(d), FN_SET_ALPHA_COVERAGE) => d.alpha_fp.is_some(), - (_, _) => false, - } -} - -fn device_hooked_symbol(dev: vk::Device, name: &str) -> Option<*mut c_void> { - device_extension_hook(name).filter(|_| device_fp_present(dev, name)) -} - -fn null_ok_ptr(name: &str) -> *mut c_void { - match name { - "vkCreateInstance" => vkCreateInstance as *mut c_void, - "vkEnumerateInstanceExtensionProperties" => volt_EnumerateInstanceExtensionProperties as *mut c_void, - "vkEnumerateInstanceLayerProperties" => volt_EnumerateInstanceLayerProperties as *mut c_void, - "vkEnumerateInstanceVersion" => volt_EnumerateInstanceVersion as *mut c_void, - _ => ptr::null_mut(), - } -} - -fn copy_cstr(dst: &mut [c_char], s: &str) { - s.bytes().take(dst.len() - 1).enumerate().for_each(|(i, b)| dst[i] = b as c_char); -} - -fn forward_device_proc(dev: vk::Device, name: &str) -> vk::PFN_vkVoidFunction { - match devs_gdpa(dev.as_raw()) { - Some(gdpa) => call_next_gdpa(gdpa, dev, name), - None => None, - } -} - -fn forward_instance_proc(inst: vk::Instance, name: &str) -> vk::PFN_vkVoidFunction { - match insts_get(inst.as_raw()) { - Some(st) => call_next_gipa(st.gipa, inst, name), - None => None, - } -} - -fn resolve_instance_proc(inst: vk::Instance, name: &str) -> vk::PFN_vkVoidFunction { - match (vk_hooked_symbol(name), instance_hooked_symbol(inst, name)) { - (Some(p), _) => unsafe { mem::transmute(p) }, - (None, Some(p)) => unsafe { mem::transmute(p) }, - (None, None) => forward_instance_proc(inst, name), - } -} - -fn resolve_null_instance_proc(name: &str) -> vk::PFN_vkVoidFunction { - match null_ok_name(name) { - true => unsafe { mem::transmute(null_ok_ptr(name)) }, - false => None, - } -} - -fn call_chain_destroy_instance(gipa: vk::PFN_vkGetInstanceProcAddr, inst: vk::Instance, alloc: *const vk::AllocationCallbacks) { - match call_next_gipa(gipa, inst, "vkDestroyInstance") { - Some(d) => unsafe { - let df: vk::PFN_vkDestroyInstance = mem::transmute(d); - df(inst, alloc); - }, - None => (), - } -} - -fn call_unowned_present() -> vk::Result { - log_at(LogLevel::Error, UNOWNED_QUEUE_ERROR); - vk::Result::ERROR_INITIALIZATION_FAILED -} - -fn call_forward_present( - owner: Option>, - queue: vk::Queue, - info: *const vk::PresentInfoKHR, -) -> vk::Result { - match owner { - Some(d) => call_present_frame(&d, queue, info), - None => call_unowned_present(), - } -} - -fn call_after_present( - presented: vk::Result, - s: &Settings, - info: *const vk::PresentInfoKHR, -) -> vk::Result { - maybe_limit_frame(LimitStage::After, s, info); - presented -} - -fn call_limited_present( - owner: Option>, - queue: vk::Queue, - info: *const vk::PresentInfoKHR, -) -> vk::Result { - let s = ensure_settings(); - maybe_limit_frame(LimitStage::Before, s, info); - call_after_present(call_forward_present(owner, queue, info), s, info) -} - -unsafe extern "system" fn volt_EnumerateInstanceExtensionProperties( - layer: *const c_char, - count: *mut u32, - _props: *mut vk::ExtensionProperties, -) -> vk::Result { - match cstr_to_str(layer) == LAYER_NAME { - true => { - *count = 0; - vk::Result::SUCCESS - } - false => vk::Result::ERROR_LAYER_NOT_PRESENT, - } -} - -unsafe extern "system" fn volt_EnumerateInstanceLayerProperties( - count: *mut u32, - props: *mut vk::LayerProperties, -) -> vk::Result { - match props.is_null() { - true => { - *count = 1; - vk::Result::SUCCESS - } - false => { - let mut p = vk::LayerProperties { - spec_version: vk::make_api_version(0, 1, 0, 0), - implementation_version: 1, - ..Default::default() - }; - copy_cstr(&mut p.layer_name, LAYER_NAME); - copy_cstr(&mut p.description, LAYER_DESC); - *count = 1; - *props = p; - vk::Result::SUCCESS - } - } -} - -unsafe extern "system" fn volt_EnumerateInstanceVersion(v: *mut u32) -> vk::Result { - *v = vk::make_api_version(0, 1, 0, 0); - vk::Result::SUCCESS -} - -unsafe extern "system" fn volt_GetDeviceQueue(dev: vk::Device, qfam: u32, qidx: u32, out: *mut vk::Queue) { - match devs_get(dev.as_raw()) { - Some(d) => { - let q = d.device.get_device_queue(qfam, qidx); - call_register_queue(&d, dev, q); - queue_dev_put(q.as_raw(), dev.as_raw()); - *out = q; - } - None => log_at(LogLevel::Warn, "GetDeviceQueue on unregistered device"), - } -} - -unsafe extern "system" fn volt_GetDeviceQueue2(dev: vk::Device, info: *const vk::DeviceQueueInfo2, out: *mut vk::Queue) { - match devs_get(dev.as_raw()) { - Some(d) => { - let q = d.device.get_device_queue2(&*info); - call_register_queue(&d, dev, q); - queue_dev_put(q.as_raw(), dev.as_raw()); - *out = q; - } - None => log_at(LogLevel::Warn, "GetDeviceQueue2 on unregistered device"), - } -} - -unsafe extern "system" fn vkGetInstanceProcAddr(inst: vk::Instance, name: *const c_char) -> vk::PFN_vkVoidFunction { - let n = cstr_to_str(name); - match inst == vk::Instance::null() { - true => resolve_null_instance_proc(n), - false => resolve_instance_proc(inst, n), - } -} - -unsafe extern "system" fn vkGetDeviceProcAddr(dev: vk::Device, name: *const c_char) -> vk::PFN_vkVoidFunction { - let n = cstr_to_str(name); - match (vk_hooked_symbol(n), device_hooked_symbol(dev, n)) { - (Some(p), _) => mem::transmute(p), - (None, Some(p)) => mem::transmute(p), - (None, None) => forward_device_proc(dev, n), - } -} - -unsafe extern "system" fn vkCreateInstance( - ci: *const vk::InstanceCreateInfo, - alloc: *const vk::AllocationCallbacks, - out: *mut vk::Instance, -) -> vk::Result { - init_log_level(); - call_real_create_instance( - call_advance_chain(chain_layer_info( - (*ci).p_next, - vk::StructureType::LOADER_INSTANCE_CREATE_INFO, - LAYER_LINK_INFO, - )), - ci, - alloc, - out, - ) -} - -unsafe extern "system" fn vkDestroyInstance(inst: vk::Instance, alloc: *const vk::AllocationCallbacks) { - let st = insts_get(inst.as_raw()); - insts_del(inst.as_raw()); - match st { - Some(s) => call_chain_destroy_instance(s.gipa, inst, alloc), - None => (), - } -} - -unsafe extern "system" fn vkEnumeratePhysicalDevices( - inst: vk::Instance, - count: *mut u32, - devices: *mut vk::PhysicalDevice, -) -> vk::Result { - call_filtered_enumerate(inst, count, devices) -} - -unsafe extern "system" fn vkEnumeratePhysicalDeviceGroups( - inst: vk::Instance, - count: *mut u32, - groups: *mut vk::PhysicalDeviceGroupProperties, -) -> vk::Result { - call_filtered_groups(inst, count, groups) -} - -unsafe extern "system" fn vkEnumeratePhysicalDeviceGroupsKHR( - inst: vk::Instance, - count: *mut u32, - groups: *mut vk::PhysicalDeviceGroupProperties, -) -> vk::Result { - call_filtered_groups_khr(inst, count, groups) -} - -unsafe extern "system" fn vkCreateDevice( - phys: vk::PhysicalDevice, - ci: *const vk::DeviceCreateInfo, - alloc: *const vk::AllocationCallbacks, - out: *mut vk::Device, -) -> vk::Result { - call_real_create_device( - call_advance_chain(chain_layer_info( - (*ci).p_next, - vk::StructureType::LOADER_DEVICE_CREATE_INFO, - LAYER_LINK_INFO, - )), - call_loader_data_fn(chain_layer_info( - (*ci).p_next, - vk::StructureType::LOADER_DEVICE_CREATE_INFO, - LAYER_DATA_CALLBACK, - )), - phys, - ci, - alloc, - out, - ) -} - -unsafe extern "system" fn vkDestroyDevice(dev: vk::Device, alloc: *const vk::AllocationCallbacks) { - match devs_del(dev.as_raw()) { - Some(d) => d.device.destroy_device(alloc.as_ref()), - None => (), - } -} - -unsafe extern "system" fn vkCreateGraphicsPipelines( - dev: vk::Device, - cache: vk::PipelineCache, - count: u32, - cis: *const vk::GraphicsPipelineCreateInfo, - alloc: *const vk::AllocationCallbacks, - out: *mut vk::Pipeline, -) -> vk::Result { - match devs_get(dev.as_raw()) { - None => vk::Result::ERROR_INITIALIZATION_FAILED, - Some(d) => call_create_graphics_pipelines(&d, cache, count, cis, alloc, out), - } -} - -unsafe extern "system" fn vkCmdSetAlphaToCoverageEnableEXT( - buffer: vk::CommandBuffer, - enable: vk::Bool32, -) { - call_set_alpha_coverage(cmdbuf_owner(buffer), buffer, enable) -} - -unsafe extern "system" fn vkAllocateCommandBuffers( - dev: vk::Device, - info: *const vk::CommandBufferAllocateInfo, - out: *mut vk::CommandBuffer, -) -> vk::Result { - match devs_get(dev.as_raw()) { - None => vk::Result::ERROR_INITIALIZATION_FAILED, - Some(d) => call_allocate_command_buffers(&d, dev, info, out), - } -} - -unsafe extern "system" fn vkFreeCommandBuffers( - dev: vk::Device, - pool: vk::CommandPool, - count: u32, - buffers: *const vk::CommandBuffer, -) { - match devs_get(dev.as_raw()) { - Some(d) => call_free_command_buffers(&d, dev, pool, count, buffers), - None => (), - } -} - -unsafe extern "system" fn vkDestroyCommandPool( - dev: vk::Device, - pool: vk::CommandPool, - alloc: *const vk::AllocationCallbacks, -) { - match devs_get(dev.as_raw()) { - Some(d) => call_destroy_command_pool(&d, dev, pool, alloc), - None => (), - } -} - -unsafe extern "system" fn vkCreateSampler( - dev: vk::Device, - ci: *const vk::SamplerCreateInfo, - alloc: *const vk::AllocationCallbacks, - out: *mut vk::Sampler, -) -> vk::Result { - match devs_get(dev.as_raw()) { - None => vk::Result::ERROR_INITIALIZATION_FAILED, - Some(d) => call_create_sampler(&d, ci, alloc, out), - } -} - -unsafe extern "system" fn vkWriteSamplerDescriptorsEXT( - dev: vk::Device, - count: u32, - cis: *const vk::SamplerCreateInfo, - descriptors: *const c_void, -) -> vk::Result { - match devs_get(dev.as_raw()) { - None => vk::Result::ERROR_INITIALIZATION_FAILED, - Some(d) => call_write_sampler_descriptors(&d, dev, count, cis, descriptors), - } -} - -unsafe extern "system" fn vkCreateSwapchainKHR( - dev: vk::Device, - ci: *const vk::SwapchainCreateInfoKHR, - alloc: *const vk::AllocationCallbacks, - out: *mut vk::SwapchainKHR, -) -> vk::Result { - match devs_get(dev.as_raw()) { - None => vk::Result::ERROR_INITIALIZATION_FAILED, - Some(d) => call_create_swapchain(&d, dev, ci, alloc, out), - } -} - -unsafe extern "system" fn vkCreateSharedSwapchainsKHR( - dev: vk::Device, - count: u32, - cis: *const vk::SwapchainCreateInfoKHR, - alloc: *const vk::AllocationCallbacks, - out: *mut vk::SwapchainKHR, -) -> vk::Result { - match devs_get(dev.as_raw()) { - None => vk::Result::ERROR_INITIALIZATION_FAILED, - Some(d) => call_create_shared_swapchains(&d, dev, count, cis, alloc, out), - } -} - -unsafe extern "system" fn vkDestroySwapchainKHR( - dev: vk::Device, - sc: vk::SwapchainKHR, - alloc: *const vk::AllocationCallbacks, -) { - match devs_get(dev.as_raw()) { - Some(d) => { - call_forget_timeline(sc); - (d.swap_fp.destroy_swapchain_khr)(dev, sc, alloc); - } - None => (), - } -} - -unsafe extern "system" fn vkGetPhysicalDeviceSurfaceFormatsKHR( - phys: vk::PhysicalDevice, - surface: vk::SurfaceKHR, - count: *mut u32, - formats: *mut vk::SurfaceFormatKHR, -) -> vk::Result { - call_surface_formats(phys, surface, count, formats) -} - -unsafe extern "system" fn vkGetPhysicalDeviceSurfaceFormats2KHR( - phys: vk::PhysicalDevice, - info: *const VkPhysicalDeviceSurfaceInfo2, - count: *mut u32, - formats: *mut VkSurfaceFormat2, -) -> vk::Result { - call_surface_formats2(phys, info, count, formats) -} - -unsafe extern "system" fn vkGetPhysicalDeviceSurfacePresentModesKHR( - phys: vk::PhysicalDevice, - surface: vk::SurfaceKHR, - count: *mut u32, - modes: *mut vk::PresentModeKHR, -) -> vk::Result { - call_surface_present_modes(phys, surface, count, modes) -} - -unsafe extern "system" fn vkGetPhysicalDeviceSurfacePresentModes2EXT( - phys: vk::PhysicalDevice, - info: *const VkPhysicalDeviceSurfaceInfo2, - count: *mut u32, - modes: *mut vk::PresentModeKHR, -) -> vk::Result { - call_surface_present_modes2(phys, info, count, modes) -} - -unsafe extern "system" fn vkGetPhysicalDeviceSurfaceCapabilitiesKHR( - phys: vk::PhysicalDevice, - surface: vk::SurfaceKHR, - caps: *mut vk::SurfaceCapabilitiesKHR, -) -> vk::Result { - call_surface_capabilities(phys, surface, caps) -} - -unsafe extern "system" fn vkGetPhysicalDeviceSurfaceCapabilities2KHR( - phys: vk::PhysicalDevice, - info: *const VkPhysicalDeviceSurfaceInfo2, - caps: *mut VkSurfaceCapabilities2, -) -> vk::Result { - call_surface_capabilities2(phys, info, caps) -} - -unsafe extern "system" fn vkQueuePresentKHR(queue: vk::Queue, info: *const vk::PresentInfoKHR) -> vk::Result { - call_limited_present(queue_owner(queue), queue, info) -} - -#[no_mangle] -pub unsafe extern "system" fn vkNegotiateLoaderLayerInterfaceVersion(p: *mut c_void) -> vk::Result { - let iface = p as *mut VkNegotiateLayerInterface; - (*iface).loader_layer_interface_version = LAYER_IFACE_VERSION; - (*iface).pfn_get_instance_proc_addr = Some(vkGetInstanceProcAddr); - (*iface).pfn_get_device_proc_addr = Some(vkGetDeviceProcAddr); - (*iface).pfn_get_physical_device_proc_addr = None; - vk::Result::SUCCESS -} diff --git a/volt/lists.rs b/volt/lists.rs deleted file mode 100644 index cb00b34..0000000 --- a/volt/lists.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::logging::log_at; -use crate::logging::LogLevel; - -fn restored(wanted: Vec, rest: Vec, warn: &str) -> Vec { - match wanted.is_empty() { - true => { - log_at(LogLevel::Warn, warn); - rest - } - false => wanted, - } -} - -pub(crate) fn forced(choice: Option, original: T) -> T { - match choice { - Some(value) => value, - None => original, - } -} - -pub(crate) fn kept(items: Vec, keep: F, warn: &str) -> Vec -where - F: Fn(&T) -> bool, -{ - let (wanted, rest): (Vec, Vec) = items.into_iter().partition(|item| keep(item)); - restored(wanted, rest, warn) -} - -pub(crate) fn filtered( - items: Vec, - choice: Option, - value_of: F, - warn: &str, -) -> Vec -where - V: PartialEq + Copy, - F: Fn(&T) -> Option, -{ - match choice { - Some(value) => kept(items, |item| value_of(item) == Some(value), warn), - None => items, - } -} diff --git a/volt/main.rs b/volt/main.rs deleted file mode 100644 index 79e4807..0000000 --- a/volt/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - std::process::exit(volt::run_launcher(volt::process_args())); -} diff --git a/volt/pipeline.rs b/volt/pipeline.rs deleted file mode 100644 index 3b23282..0000000 --- a/volt/pipeline.rs +++ /dev/null @@ -1,157 +0,0 @@ -use std::sync::Arc; - -use ash::vk; - -use crate::config::ensure_settings; -use crate::config::Settings; -use crate::consts::SHADING_ABSENT_INFO; -use crate::consts::SHADING_OFF; -use crate::consts::TOGGLE_ON; -use crate::consts::UNOWNED_BUFFER_ERROR; -use crate::device::DeviceCaps; -use crate::device::VkDevState; -use crate::logging::log_at; -use crate::logging::LogLevel; - -fn toggle_vk(value: u32) -> vk::Bool32 { - match value { - TOGGLE_ON => vk::TRUE, - _ => vk::FALSE, - } -} - -fn pick_coverage(choice: Option, original: vk::Bool32) -> vk::Bool32 { - match choice { - Some(value) => toggle_vk(value), - None => original, - } -} - -fn shading_absent() -> Option { - log_at(LogLevel::Info, SHADING_ABSENT_INFO); - None -} - -fn shading_allowed(choice: Option, caps: &DeviceCaps) -> Option { - match (choice, caps.sample_rate_shading) { - (None, _) => None, - (Some(rate), true) => Some(rate), - (Some(_), false) => shading_absent(), - } -} - -fn shading_pair(rate: f32) -> (vk::Bool32, f32) { - match rate > SHADING_OFF { - true => (vk::TRUE, rate), - false => (vk::FALSE, SHADING_OFF), - } -} - -fn pick_shading( - choice: Option, - caps: &DeviceCaps, - original: (vk::Bool32, f32), -) -> (vk::Bool32, f32) { - match shading_allowed(choice, caps) { - Some(rate) => shading_pair(rate), - None => original, - } -} - -fn rebuilt_multisample( - s: &Settings, - caps: &DeviceCaps, - original: &vk::PipelineMultisampleStateCreateInfo, -) -> vk::PipelineMultisampleStateCreateInfo { - let (shading_enable, shading_rate) = pick_shading( - s.sample_shading, - caps, - (original.sample_shading_enable, original.min_sample_shading), - ); - vk::PipelineMultisampleStateCreateInfo { - sample_shading_enable: shading_enable, - min_sample_shading: shading_rate, - alpha_to_coverage_enable: pick_coverage(s.alpha_coverage, original.alpha_to_coverage_enable), - ..*original - } -} - -fn patched_multisample( - s: &Settings, - caps: &DeviceCaps, - p: *const vk::PipelineMultisampleStateCreateInfo, -) -> Option { - match p.is_null() { - true => None, - false => Some(rebuilt_multisample(s, caps, unsafe { &*p })), - } -} - -fn state_ptr(owned: &Option, original: *const T) -> *const T { - match owned { - Some(v) => v as *const T, - None => original, - } -} - -fn patched_ci( - original: &vk::GraphicsPipelineCreateInfo, - multisample: &Option, -) -> vk::GraphicsPipelineCreateInfo { - vk::GraphicsPipelineCreateInfo { - p_multisample_state: state_ptr(multisample, original.p_multisample_state), - ..*original - } -} - -fn call_forward_coverage(dev: &VkDevState, buffer: vk::CommandBuffer, enable: vk::Bool32) { - match dev.alpha_fp { - Some(fp) => unsafe { - fp(buffer, pick_coverage(ensure_settings().alpha_coverage, enable)) - }, - None => (), - } -} - -pub(crate) fn call_set_alpha_coverage( - owner: Option>, - buffer: vk::CommandBuffer, - enable: vk::Bool32, -) { - match owner { - Some(d) => call_forward_coverage(&d, buffer, enable), - None => log_at(LogLevel::Error, UNOWNED_BUFFER_ERROR), - } -} - -pub(crate) fn call_create_graphics_pipelines( - dev: &VkDevState, - cache: vk::PipelineCache, - count: u32, - cis: *const vk::GraphicsPipelineCreateInfo, - alloc: *const vk::AllocationCallbacks, - out: *mut vk::Pipeline, -) -> vk::Result { - let s = ensure_settings(); - let originals: Vec = - unsafe { std::slice::from_raw_parts(cis, count as usize) }.to_vec(); - let multisamples: Vec> = originals - .iter() - .map(|ci| patched_multisample(s, &dev.caps, ci.p_multisample_state)) - .collect(); - let patched: Vec = originals - .iter() - .zip(multisamples.iter()) - .map(|(ci, m)| patched_ci(ci, m)) - .collect(); - unsafe { - (dev.device.fp_v1_0().create_graphics_pipelines)( - dev.device.handle(), - cache, - count, - patched.as_ptr(), - alloc, - out, - ) - } -} diff --git a/volt/present.rs b/volt/present.rs deleted file mode 100644 index 49eb1c6..0000000 --- a/volt/present.rs +++ /dev/null @@ -1,222 +0,0 @@ -use std::collections::HashMap; -use std::sync::Mutex; -use std::sync::OnceLock; -use std::thread; -use std::time::Duration; -use std::time::Instant; - -use ash::vk; -use ash::vk::Handle; - -use crate::config::Settings; -use crate::consts::LimitStage; -use crate::consts::MethodChoice; -use crate::consts::NS_PER_S; -use crate::consts::PacingChoice; -use crate::consts::SLICE_MARGIN_NS; -use crate::consts::SLICE_STEP_NS; -use crate::consts::SPIN_MARGIN_NS; -use crate::device::VkDevState; - -#[derive(Clone, Copy, Debug, PartialEq)] -pub(crate) struct Timeline { - pub(crate) target: u64, - pub(crate) interval: u64, -} - -type TimelineMap = HashMap; - -static EPOCH: OnceLock = OnceLock::new(); -static TIMELINES: Mutex> = Mutex::new(None); - -fn call_now_ns() -> u64 { - EPOCH.get_or_init(Instant::now).elapsed().as_nanos() as u64 -} - -pub(crate) fn target_interval_ns(fps: f32) -> u64 { - (NS_PER_S / fps as f64) as u64 -} - -fn overshoot_ns(now: u64, target: u64) -> u64 { - now.saturating_sub(target) -} - -fn next_target_ns(now: u64, previous: u64, interval: u64, fresh: bool) -> u64 { - match (fresh, overshoot_ns(now, previous) >= interval) { - (false, false) => previous + interval, - (_, _) => now + interval, - } -} - -fn wait_deficit_ns(now: u64, target: u64) -> u64 { - target.saturating_sub(now) -} - -fn slice_length_ns(remaining: u64) -> u64 { - remaining.saturating_sub(SLICE_MARGIN_NS).min(SLICE_STEP_NS) -} - -fn restart_wanted(method: Option, interval_changed: bool) -> bool { - match method { - Some(MethodChoice::Reactive) => true, - Some(MethodChoice::Early) | Some(MethodChoice::Late) | None => interval_changed, - } -} - -fn previous_target(previous: Option, now: u64) -> u64 { - match previous { - Some(line) => line.target, - None => now, - } -} - -fn interval_changed(previous: Option, interval: u64) -> bool { - match previous { - Some(line) => line.interval != interval, - None => true, - } -} - -pub(crate) fn advanced( - previous: Option, - now: u64, - interval: u64, - method: Option, -) -> Timeline { - Timeline { - target: next_target_ns( - now, - previous_target(previous, now), - interval, - restart_wanted(method, interval_changed(previous, interval)), - ), - interval, - } -} - -fn call_sleep_ns(ns: u64) { - match ns { - 0 => (), - n => thread::sleep(Duration::from_nanos(n)), - } -} - -fn call_spin_until(target: u64) { - std::iter::repeat(()) - .take_while(|_| call_now_ns() < target) - .for_each(|_| std::hint::spin_loop()); -} - -fn call_sleep_until(target: u64) { - call_sleep_ns(wait_deficit_ns(call_now_ns(), target)); -} - -fn call_precise_until(target: u64) { - call_sleep_ns(wait_deficit_ns(call_now_ns(), target.saturating_sub(SPIN_MARGIN_NS))); - call_spin_until(target); -} - -fn call_slice_until(target: u64) { - std::iter::repeat(()) - .take_while(|_| call_now_ns() + SLICE_MARGIN_NS < target) - .for_each(|_| call_sleep_ns(slice_length_ns(wait_deficit_ns(call_now_ns(), target)))); - call_spin_until(target); -} - -fn call_wait_until(target: u64, pacing: PacingChoice) { - match pacing { - PacingChoice::Sleep => call_sleep_until(target), - PacingChoice::Sliced => call_slice_until(target), - PacingChoice::Precise => call_precise_until(target), - PacingChoice::Spin => call_spin_until(target), - } -} - -fn call_present_key(info: *const vk::PresentInfoKHR) -> u64 { - unsafe { (*(*info).p_swapchains).as_raw() } -} - -fn call_stored(map: &mut TimelineMap, key: u64, next: Timeline) -> u64 { - map.insert(key, next); - next.target -} - -fn call_advanced_in( - map: &mut TimelineMap, - key: u64, - interval: u64, - method: Option, -) -> u64 { - call_stored( - map, - key, - advanced(map.get(&key).copied(), call_now_ns(), interval, method), - ) -} - -fn call_frame_target(key: u64, interval: u64, method: Option) -> u64 { - match TIMELINES.lock() { - Ok(mut guard) => call_advanced_in( - guard.get_or_insert_with(HashMap::new), - key, - interval, - method, - ), - Err(_) => call_now_ns(), - } -} - -fn call_limit_to(key: u64, fps: f32, pacing: PacingChoice, method: Option) { - call_wait_until(call_frame_target(key, target_interval_ns(fps), method), pacing); -} - -fn pacing_or_default(pacing: Option) -> PacingChoice { - match pacing { - Some(choice) => choice, - None => PacingChoice::Sleep, - } -} - -fn stage_wanted(method: Option) -> LimitStage { - match method { - Some(MethodChoice::Late) => LimitStage::After, - Some(MethodChoice::Early) | Some(MethodChoice::Reactive) | None => LimitStage::Before, - } -} - -fn limit_fps(s: &Settings, stage: LimitStage) -> Option { - s.frame_limit.filter(|_| stage_wanted(s.limit_method) == stage) -} - -pub(crate) fn maybe_limit_frame( - stage: LimitStage, - s: &Settings, - info: *const vk::PresentInfoKHR, -) { - match limit_fps(s, stage) { - Some(fps) => call_limit_to( - call_present_key(info), - fps, - pacing_or_default(s.pacing), - s.limit_method, - ), - None => (), - } -} - -pub(crate) fn call_forget_timeline(sc: vk::SwapchainKHR) { - match TIMELINES.lock() { - Ok(mut guard) => { - guard.get_or_insert_with(HashMap::new).remove(&sc.as_raw()); - } - Err(_) => (), - } -} - -pub(crate) fn call_present_frame( - dev: &VkDevState, - queue: vk::Queue, - info: *const vk::PresentInfoKHR, -) -> vk::Result { - unsafe { (dev.swap_fp.queue_present_khr)(queue, info) } -} diff --git a/volt/probe.rs b/volt/probe.rs deleted file mode 100644 index fcbcace..0000000 --- a/volt/probe.rs +++ /dev/null @@ -1,206 +0,0 @@ -use std::ffi::CStr; -use std::fs; -use std::sync::Once; - -use ash::vk; - -use crate::config::config_dir; -use crate::consts::PROBE_FAIL_WARN; -use crate::consts::PROBE_FILE; -use crate::consts::PROBE_OFF; -use crate::consts::PROBE_ON; -use crate::consts::PROBE_SECTION; -use crate::consts::PROBE_SEP; -use crate::consts::PROBE_WRITE_INFO; -use crate::device::VkDevState; -use crate::instance::VkInstState; -use crate::logging::log_at; -use crate::logging::LogLevel; -use crate::ranks::alpha_display; -use crate::ranks::depth_label; -use crate::ranks::format_semantic; -use crate::ranks::numeric_display; -use crate::ranks::present_display; -use crate::ranks::space_display; - -static WRITTEN: Once = Once::new(); - -pub(crate) struct ProbeData { - pub(crate) index: u32, - pub(crate) names: Vec, - pub(crate) present: Vec, - pub(crate) depths: Vec, - pub(crate) spaces: Vec, - pub(crate) transfers: Vec, - pub(crate) alphas: Vec, - pub(crate) min_images: u32, - pub(crate) max_images: u32, - pub(crate) max_anisotropy: f32, - pub(crate) max_lod_bias: f32, - pub(crate) max_lod_level: f32, - pub(crate) anisotropy: bool, - pub(crate) shading: bool, -} - -fn unique_names(names: Vec) -> Vec { - names.into_iter().fold(Vec::new(), |mut kept, name| { - match kept.contains(&name) { - true => kept, - false => { - kept.push(name); - kept - } - } - }) -} - -fn unique_sorted(mut values: Vec) -> Vec { - values.sort(); - values.dedup(); - values -} - -fn all_devices(inst: &VkInstState) -> Vec { - unsafe { inst.instance.enumerate_physical_devices() }.unwrap_or_default() -} - -fn device_name(inst: &VkInstState, phys: vk::PhysicalDevice) -> String { - let props = unsafe { inst.instance.get_physical_device_properties(phys) }; - unsafe { CStr::from_ptr(props.device_name.as_ptr()) } - .to_string_lossy() - .to_lowercase() -} - -fn device_names(inst: &VkInstState, all: &[vk::PhysicalDevice]) -> Vec { - all.iter().map(|p| device_name(inst, *p)).collect() -} - -fn device_index(all: &[vk::PhysicalDevice], phys: vk::PhysicalDevice) -> u32 { - all.iter() - .position(|p| *p == phys) - .map(|at| at as u32 + 1) - .unwrap_or(1) -} - -fn present_names(supported: &[vk::PresentModeKHR]) -> Vec { - unique_sorted(supported.iter().map(|m| m.as_raw() as u32).collect()) - .into_iter() - .map(present_display) - .collect() -} - -fn space_names(formats: &[vk::SurfaceFormatKHR]) -> Vec { - unique_sorted(formats.iter().map(|f| f.color_space.as_raw() as u32).collect()) - .into_iter() - .map(space_display) - .collect() -} - -fn depth_names(formats: &[vk::SurfaceFormatKHR]) -> Vec { - unique_sorted( - formats - .iter() - .filter_map(|f| format_semantic(f.format.as_raw() as u32)) - .map(|facts| facts.depth) - .collect(), - ) - .into_iter() - .map(depth_label) - .collect() -} - -fn transfer_names(formats: &[vk::SurfaceFormatKHR]) -> Vec { - unique_names( - unique_sorted(formats.iter().map(|f| f.format.as_raw() as u32).collect()) - .into_iter() - .filter_map(format_semantic) - .map(|facts| numeric_display(facts.numeric)) - .collect(), - ) -} - -fn set_bits(mask: u32) -> Vec { - (0..u32::BITS) - .map(|shift| 1u32 << shift) - .filter(|bit| mask & *bit != 0) - .collect() -} - -fn alpha_names(mask: u32) -> Vec { - set_bits(mask).into_iter().map(alpha_display).collect() -} - -fn joined(items: &[String]) -> String { - items.join(PROBE_SEP) -} - -fn flag_text(value: bool) -> &'static str { - match value { - true => PROBE_ON, - false => PROBE_OFF, - } -} - -fn pair(key: &str, value: &str) -> String { - format!("{} = \"{}\"\n", key, value) -} - -pub(crate) fn build_probe( - inst: &VkInstState, - dev: &VkDevState, - supported: &[vk::PresentModeKHR], - caps: &vk::SurfaceCapabilitiesKHR, - formats: &[vk::SurfaceFormatKHR], -) -> ProbeData { - let all = all_devices(inst); - ProbeData { - index: device_index(&all, dev.phys), - names: device_names(inst, &all), - present: present_names(supported), - depths: depth_names(formats), - spaces: space_names(formats), - transfers: transfer_names(formats), - alphas: alpha_names(caps.supported_composite_alpha.as_raw()), - min_images: caps.min_image_count, - max_images: caps.max_image_count, - max_anisotropy: dev.caps.max_anisotropy, - max_lod_bias: dev.caps.max_lod_bias, - max_lod_level: dev.caps.max_lod_level, - anisotropy: dev.caps.sampler_anisotropy, - shading: dev.caps.sample_rate_shading, - } -} - -fn render(d: &ProbeData) -> String { - [ - PROBE_SECTION.to_string(), - "\n".to_string(), - pair("device_index", &d.index.to_string()), - pair("device_names", &joined(&d.names)), - pair("present_modes", &joined(&d.present)), - pair("color_depths", &joined(&d.depths)), - pair("color_spaces", &joined(&d.spaces)), - pair("transfer_functions", &joined(&d.transfers)), - pair("composite_alphas", &joined(&d.alphas)), - pair("min_image_count", &d.min_images.to_string()), - pair("max_image_count", &d.max_images.to_string()), - pair("max_anisotropy", &d.max_anisotropy.to_string()), - pair("max_lod_bias", &d.max_lod_bias.to_string()), - pair("max_lod_level", &d.max_lod_level.to_string()), - pair("sampler_anisotropy", flag_text(d.anisotropy)), - pair("sample_rate_shading", flag_text(d.shading)), - ] - .concat() -} - -fn call_write_file(text: &str) { - let _ = fs::create_dir_all(config_dir()); - match fs::write(config_dir().join(PROBE_FILE), text) { - Ok(()) => log_at(LogLevel::Info, PROBE_WRITE_INFO), - Err(_) => log_at(LogLevel::Warn, PROBE_FAIL_WARN), - } -} - -pub(crate) fn call_write_probe(d: ProbeData) { - WRITTEN.call_once(|| call_write_file(&render(&d))); -} diff --git a/volt/ranks.rs b/volt/ranks.rs deleted file mode 100644 index 706b5b8..0000000 --- a/volt/ranks.rs +++ /dev/null @@ -1,240 +0,0 @@ -use crate::consts::ALPHA_UNKNOWN_PREFIX; -use crate::consts::DEPTH_SUFFIX; -use crate::consts::FORMAT_UNKNOWN_PREFIX; -use crate::consts::PRESENT_UNKNOWN_PREFIX; -use crate::consts::SPACE_UNKNOWN_PREFIX; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum Numeric { - Unorm, - Srgb, - Sfloat, - Ufloat, -} - -const PRESENT_MODES: [(u32, &str, bool); 7] = [ - (0, "immediate", false), - (1, "mailbox", false), - (2, "fifo", false), - (3, "fifo_relaxed", false), - (1000111000, "shared_demand_refresh", true), - (1000111001, "shared_continuous_refresh", true), - (1000361000, "fifo_latest_ready", true), -]; - -const COLOR_SPACES: [(u32, &str, bool); 16] = [ - (0, "srgb_nonlinear", false), - (1000104001, "display_p3_nonlinear", true), - (1000104002, "extended_srgb_linear", true), - (1000104003, "display_p3_linear", true), - (1000104004, "dci_p3_nonlinear", true), - (1000104005, "bt709_linear", true), - (1000104006, "bt709_nonlinear", true), - (1000104007, "bt2020_linear", true), - (1000104008, "hdr10_st2084", true), - (1000104009, "dolbyvision", true), - (1000104010, "hdr10_hlg", true), - (1000104011, "adobergb_linear", true), - (1000104012, "adobergb_nonlinear", true), - (1000104013, "pass_through", true), - (1000104014, "extended_srgb_nonlinear", true), - (1000213000, "display_native_amd", true), -]; - -const COMPOSITE_ALPHAS: [(u32, &str, bool); 4] = [ - (1, "opaque", false), - (2, "pre_multiplied", true), - (4, "post_multiplied", true), - (8, "inherit", true), -]; - -const FORMATS: [(u32, &str, u32, Numeric); 33] = [ - (2, "r4g4b4a4_unorm_pack16", 4, Numeric::Unorm), - (3, "b4g4r4a4_unorm_pack16", 4, Numeric::Unorm), - (4, "r5g6b5_unorm_pack16", 5, Numeric::Unorm), - (5, "b5g6r5_unorm_pack16", 5, Numeric::Unorm), - (6, "r5g5b5a1_unorm_pack16", 5, Numeric::Unorm), - (7, "b5g5r5a1_unorm_pack16", 5, Numeric::Unorm), - (8, "a1r5g5b5_unorm_pack16", 5, Numeric::Unorm), - (23, "r8g8b8_unorm", 8, Numeric::Unorm), - (29, "r8g8b8_srgb", 8, Numeric::Srgb), - (30, "b8g8r8_unorm", 8, Numeric::Unorm), - (36, "b8g8r8_srgb", 8, Numeric::Srgb), - (37, "r8g8b8a8_unorm", 8, Numeric::Unorm), - (43, "r8g8b8a8_srgb", 8, Numeric::Srgb), - (44, "b8g8r8a8_unorm", 8, Numeric::Unorm), - (50, "b8g8r8a8_srgb", 8, Numeric::Srgb), - (51, "a8b8g8r8_unorm_pack32", 8, Numeric::Unorm), - (57, "a8b8g8r8_srgb_pack32", 8, Numeric::Srgb), - (58, "a2r10g10b10_unorm_pack32", 10, Numeric::Unorm), - (64, "a2b10g10r10_unorm_pack32", 10, Numeric::Unorm), - (84, "r16g16b16_unorm", 16, Numeric::Unorm), - (90, "r16g16b16_sfloat", 16, Numeric::Sfloat), - (91, "r16g16b16a16_unorm", 16, Numeric::Unorm), - (97, "r16g16b16a16_sfloat", 16, Numeric::Sfloat), - (106, "r32g32b32_sfloat", 32, Numeric::Sfloat), - (109, "r32g32b32a32_sfloat", 32, Numeric::Sfloat), - (118, "r64g64b64_sfloat", 64, Numeric::Sfloat), - (121, "r64g64b64a64_sfloat", 64, Numeric::Sfloat), - (122, "b10g11r11_ufloat_pack32", 11, Numeric::Ufloat), - (123, "e5b9g9r9_ufloat_pack32", 9, Numeric::Ufloat), - (1000340000, "a4r4g4b4_unorm_pack16", 4, Numeric::Unorm), - (1000340001, "a4b4g4r4_unorm_pack16", 4, Numeric::Unorm), - (1000470000, "a1b5g5r5_unorm_pack16", 5, Numeric::Unorm), - (1000609011, "r14x2g14x2b14x2a14x2_unorm_4pack16_arm", 14, Numeric::Unorm), -]; - -const NUMERICS: [(Numeric, &str, bool); 4] = [ - (Numeric::Unorm, "unorm", false), - (Numeric::Srgb, "srgb", true), - (Numeric::Sfloat, "sfloat", false), - (Numeric::Ufloat, "ufloat", false), -]; - -#[derive(Clone, Copy)] -pub(crate) struct PresentFacts { - pub(crate) extended: bool, -} - -#[derive(Clone, Copy)] -pub(crate) struct SpaceFacts { - pub(crate) extended: bool, -} - -#[derive(Clone, Copy)] -pub(crate) struct AlphaFacts { - pub(crate) blends: bool, -} - -#[derive(Clone, Copy)] -pub(crate) struct FormatFacts { - pub(crate) depth: u32, - pub(crate) numeric: Numeric, -} - -#[derive(Clone, Copy)] -pub(crate) struct NumericFacts { - pub(crate) encoded: bool, -} - -fn unknown_display(prefix: &str, value: u32) -> String { - format!("{}{}", prefix, value) -} - -fn parse_unknown(prefix: &str, text: &str) -> Option { - text.strip_prefix(prefix) - .and_then(|raw| raw.parse::().ok()) -} - -fn present_row(value: u32) -> Option<(u32, &'static str, bool)> { - PRESENT_MODES.iter().copied().find(|(known, _, _)| *known == value) -} - -fn space_row(value: u32) -> Option<(u32, &'static str, bool)> { - COLOR_SPACES.iter().copied().find(|(known, _, _)| *known == value) -} - -fn alpha_row(value: u32) -> Option<(u32, &'static str, bool)> { - COMPOSITE_ALPHAS.iter().copied().find(|(known, _, _)| *known == value) -} - -fn format_row(value: u32) -> Option<(u32, &'static str, u32, Numeric)> { - FORMATS.iter().copied().find(|(known, _, _, _)| *known == value) -} - -fn numeric_row(numeric: Numeric) -> Option<(Numeric, &'static str, bool)> { - NUMERICS.iter().copied().find(|(known, _, _)| *known == numeric) -} - -pub(crate) fn present_display(value: u32) -> String { - match present_row(value) { - Some((_, name, _)) => name.to_string(), - None => unknown_display(PRESENT_UNKNOWN_PREFIX, value), - } -} - -pub(crate) fn present_parse(text: &str) -> Option { - match PRESENT_MODES.iter().find(|(_, name, _)| *name == text) { - Some((value, _, _)) => Some(*value), - None => parse_unknown(PRESENT_UNKNOWN_PREFIX, text), - } -} - -pub(crate) fn present_semantic(value: u32) -> Option { - present_row(value).map(|(_, _, extended)| PresentFacts { extended }) -} - -pub(crate) fn space_display(value: u32) -> String { - match space_row(value) { - Some((_, name, _)) => name.to_string(), - None => unknown_display(SPACE_UNKNOWN_PREFIX, value), - } -} - -pub(crate) fn space_parse(text: &str) -> Option { - match COLOR_SPACES.iter().find(|(_, name, _)| *name == text) { - Some((value, _, _)) => Some(*value), - None => parse_unknown(SPACE_UNKNOWN_PREFIX, text), - } -} - -pub(crate) fn space_semantic(value: u32) -> Option { - space_row(value).map(|(_, _, extended)| SpaceFacts { extended }) -} - -pub(crate) fn alpha_display(value: u32) -> String { - match alpha_row(value) { - Some((_, name, _)) => name.to_string(), - None => unknown_display(ALPHA_UNKNOWN_PREFIX, value), - } -} - -pub(crate) fn alpha_parse(text: &str) -> Option { - match COMPOSITE_ALPHAS.iter().find(|(_, name, _)| *name == text) { - Some((value, _, _)) => Some(*value), - None => parse_unknown(ALPHA_UNKNOWN_PREFIX, text), - } -} - -pub(crate) fn alpha_semantic(value: u32) -> Option { - alpha_row(value).map(|(_, _, blends)| AlphaFacts { blends }) -} - -pub(crate) fn format_display(value: u32) -> String { - match format_row(value) { - Some((_, name, _, _)) => name.to_string(), - None => unknown_display(FORMAT_UNKNOWN_PREFIX, value), - } -} - - -pub(crate) fn format_semantic(value: u32) -> Option { - format_row(value).map(|(_, _, depth, numeric)| FormatFacts { depth, numeric }) -} - -pub(crate) fn numeric_display(numeric: Numeric) -> String { - match numeric_row(numeric) { - Some((_, name, _)) => name.to_string(), - None => String::new(), - } -} - -pub(crate) fn numeric_parse(text: &str) -> Option { - match NUMERICS.iter().find(|(_, name, _)| *name == text) { - Some((numeric, _, _)) => Some(*numeric), - None => None, - } -} - -pub(crate) fn numeric_semantic(numeric: Numeric) -> Option { - numeric_row(numeric).map(|(_, _, encoded)| NumericFacts { encoded }) -} - -pub(crate) fn depth_label(bits: u32) -> String { - format!("{}{}", bits, DEPTH_SUFFIX) -} - -pub(crate) fn parse_depth_label(text: &str) -> Option { - text.strip_suffix(DEPTH_SUFFIX) - .and_then(|bits| bits.parse::().ok()) -} diff --git a/volt/sampler.rs b/volt/sampler.rs deleted file mode 100644 index ef4e55d..0000000 --- a/volt/sampler.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::ffi::c_void; - -use ash::vk; - -use crate::config::ensure_settings; -use crate::config::Settings; -use crate::consts::ANISO_ABSENT_INFO; -use crate::consts::ANISO_OFF; -use crate::consts::FILTER_BILINEAR; -use crate::consts::FILTER_RETRO; -use crate::consts::FILTER_TRILINEAR; -use crate::consts::MIPMAP_LINEAR; -use crate::device::DeviceCaps; -use crate::device::VkDevState; -use crate::instance::PfnWriteSamplers; -use crate::lists::forced; -use crate::logging::log_at; -use crate::logging::LogLevel; - -fn filter_triple(value: u32) -> (vk::Filter, vk::Filter, vk::SamplerMipmapMode) { - match value { - FILTER_RETRO => (vk::Filter::NEAREST, vk::Filter::NEAREST, vk::SamplerMipmapMode::NEAREST), - FILTER_BILINEAR => (vk::Filter::LINEAR, vk::Filter::LINEAR, vk::SamplerMipmapMode::NEAREST), - FILTER_TRILINEAR => (vk::Filter::LINEAR, vk::Filter::LINEAR, vk::SamplerMipmapMode::LINEAR), - _ => (vk::Filter::LINEAR, vk::Filter::LINEAR, vk::SamplerMipmapMode::LINEAR), - } -} - -fn pick_filters( - choice: Option, - original: (vk::Filter, vk::Filter, vk::SamplerMipmapMode), -) -> (vk::Filter, vk::Filter, vk::SamplerMipmapMode) { - match choice { - Some(value) => filter_triple(value), - None => original, - } -} - -fn mipmap_vk(value: u32) -> vk::SamplerMipmapMode { - match value { - MIPMAP_LINEAR => vk::SamplerMipmapMode::LINEAR, - _ => vk::SamplerMipmapMode::NEAREST, - } -} - -fn pick_mipmap(choice: Option, original: vk::SamplerMipmapMode) -> vk::SamplerMipmapMode { - match choice { - Some(value) => mipmap_vk(value), - None => original, - } -} - -fn aniso_absent() -> Option { - log_at(LogLevel::Info, ANISO_ABSENT_INFO); - None -} - -fn aniso_allowed(choice: Option, caps: &DeviceCaps) -> Option { - match (choice, caps.sampler_anisotropy) { - (None, _) => None, - (Some(level), true) => Some(level.min(caps.max_anisotropy)), - (Some(_), false) => aniso_absent(), - } -} - -fn aniso_pair(level: f32) -> (vk::Bool32, f32) { - match level > ANISO_OFF { - true => (vk::TRUE, level), - false => (vk::FALSE, ANISO_OFF), - } -} - -fn pick_aniso( - choice: Option, - caps: &DeviceCaps, - original: (vk::Bool32, f32), -) -> (vk::Bool32, f32) { - match aniso_allowed(choice, caps) { - Some(level) => aniso_pair(level), - None => original, - } -} - -fn pick_lod_bias(choice: Option, caps: &DeviceCaps, original: f32) -> f32 { - forced(choice, original).clamp(-caps.max_lod_bias, caps.max_lod_bias) -} - -fn pick_lod_range(s: &Settings, original: (f32, f32)) -> (f32, f32) { - let low = forced(s.mip_floor, original.0); - let high = forced(s.mip_ceiling, original.1); - (low.min(high), high.max(low)) -} - -fn patched_ci( - s: &Settings, - caps: &DeviceCaps, - original: &vk::SamplerCreateInfo, -) -> vk::SamplerCreateInfo { - let (mag, min, mip) = pick_filters( - s.filtering, - (original.mag_filter, original.min_filter, original.mipmap_mode), - ); - let (aniso_enable, aniso_max) = pick_aniso( - s.anisotropy, - caps, - (original.anisotropy_enable, original.max_anisotropy), - ); - let (lod_low, lod_high) = pick_lod_range(s, (original.min_lod, original.max_lod)); - vk::SamplerCreateInfo { - mag_filter: mag, - min_filter: min, - mipmap_mode: pick_mipmap(s.mipmap, mip), - anisotropy_enable: aniso_enable, - max_anisotropy: aniso_max, - mip_lod_bias: pick_lod_bias(s.lod_bias, caps, original.mip_lod_bias), - min_lod: lod_low, - max_lod: lod_high, - ..*original - } -} - -fn patched_list( - s: &Settings, - caps: &DeviceCaps, - cis: *const vk::SamplerCreateInfo, - count: u32, -) -> Vec { - unsafe { std::slice::from_raw_parts(cis, count as usize) } - .iter() - .map(|original| patched_ci(s, caps, original)) - .collect() -} - -pub(crate) fn call_create_sampler( - dev: &VkDevState, - ci: *const vk::SamplerCreateInfo, - alloc: *const vk::AllocationCallbacks, - out: *mut vk::Sampler, -) -> vk::Result { - let patched = patched_ci(ensure_settings(), &dev.caps, unsafe { &*ci }); - match unsafe { dev.device.create_sampler(&patched, alloc.as_ref()) } { - Ok(sampler) => { - unsafe { *out = sampler }; - vk::Result::SUCCESS - } - Err(e) => e, - } -} - -fn call_samplers_through( - dev: &VkDevState, - fp: PfnWriteSamplers, - handle: vk::Device, - count: u32, - cis: *const vk::SamplerCreateInfo, - descriptors: *const c_void, -) -> vk::Result { - let patched = patched_list(ensure_settings(), &dev.caps, cis, count); - unsafe { fp(handle, count, patched.as_ptr(), descriptors) } -} - -pub(crate) fn call_write_sampler_descriptors( - dev: &VkDevState, - handle: vk::Device, - count: u32, - cis: *const vk::SamplerCreateInfo, - descriptors: *const c_void, -) -> vk::Result { - match dev.samplers_fp { - Some(fp) => call_samplers_through(dev, fp, handle, count, cis, descriptors), - None => vk::Result::ERROR_INITIALIZATION_FAILED, - } -} diff --git a/volt/swapchain.rs b/volt/swapchain.rs deleted file mode 100644 index da53044..0000000 --- a/volt/swapchain.rs +++ /dev/null @@ -1,844 +0,0 @@ -use std::ffi::c_void; -use std::ptr; - -use ash::vk; - -use crate::config::ensure_settings; -use crate::config::Settings; -use crate::consts::ALPHA_MISS_WARN; -use crate::consts::ALPHA_OPAQUE_INFO; -use crate::consts::DEPTH_EMPTY_WARN; -use crate::consts::FORMAT_FORCED_WARN; -use crate::consts::MODE_LIST_TYPES; -use crate::consts::PRESENT_EMPTY_WARN; -use crate::consts::PRESENT_EXTENDED_INFO; -use crate::consts::PRESENT_MISS_WARN; -use crate::consts::SPACE_EMPTY_WARN; -use crate::consts::SPACE_EXTENDED_INFO; -use crate::consts::SPACE_FORCED_WARN; -use crate::consts::TOGGLE_ON; -use crate::consts::TRANSFER_EMPTY_WARN; -use crate::consts::TRANSFER_ENCODED_INFO; -use crate::consts::TRANSFER_FORCED_WARN; -use crate::consts::TRANSFER_SHADER_INFO; -use crate::device::VkDevState; -use crate::env::env_probe_active; -use crate::instance::call_write_list; -use crate::instance::insts_get; -use crate::instance::owning_instance; -use crate::instance::PfnCreateSharedSwapchains; -use crate::instance::PfnSurfaceCaps2; -use crate::instance::PfnSurfaceFormats2; -use crate::instance::PfnSurfaceModes2; -use crate::instance::VkChainNode; -use crate::instance::VkInstState; -use crate::instance::VkPhysicalDeviceSurfaceInfo2; -use crate::instance::VkPresentModeList; -use crate::instance::VkSurfaceCapabilities2; -use crate::instance::VkSurfaceFormat2; -use crate::instance::SURFACE_FORMAT_2; -use crate::lists::filtered; -use crate::lists::forced; -use crate::logging::log_at; -use crate::logging::LogLevel; -use crate::probe::build_probe; -use crate::probe::call_write_probe; -use crate::ranks::alpha_semantic; -use crate::ranks::format_display; -use crate::ranks::format_semantic; -use crate::ranks::numeric_semantic; -use crate::ranks::present_semantic; -use crate::ranks::space_semantic; -use crate::ranks::Numeric; - -fn mode_value(mode: &vk::PresentModeKHR) -> Option { - Some(mode.as_raw() as u32) -} - -fn depth_of(f: &vk::SurfaceFormatKHR) -> Option { - format_semantic(f.format.as_raw() as u32).map(|facts| facts.depth) -} - -fn numeric_of(f: &vk::SurfaceFormatKHR) -> Option { - format_semantic(f.format.as_raw() as u32).map(|facts| facts.numeric) -} - -fn space_of(f: &vk::SurfaceFormatKHR) -> Option { - Some(f.color_space.as_raw() as u32) -} - -fn surface_filtered( - formats: Vec, - s: &Settings, -) -> Vec { - filtered( - filtered( - filtered(formats, s.depth, depth_of, DEPTH_EMPTY_WARN), - s.color_space, - space_of, - SPACE_EMPTY_WARN, - ), - s.transfer, - numeric_of, - TRANSFER_EMPTY_WARN, - ) -} - -fn present_filtered( - modes: Vec, - choice: Option, -) -> Vec { - filtered(modes, choice, mode_value, PRESENT_EMPTY_WARN) -} - -fn supported_mode(supported: &[vk::PresentModeKHR], value: u32) -> Option { - supported.iter().copied().find(|m| m.as_raw() as u32 == value) -} - -fn logged_mode_miss(original: vk::PresentModeKHR) -> vk::PresentModeKHR { - log_at(LogLevel::Warn, PRESENT_MISS_WARN); - original -} - -fn chosen_mode( - supported: &[vk::PresentModeKHR], - value: u32, - original: vk::PresentModeKHR, -) -> vk::PresentModeKHR { - match supported_mode(supported, value) { - Some(mode) => mode, - None => logged_mode_miss(original), - } -} - -fn pick_present_mode( - choice: Option, - supported: &[vk::PresentModeKHR], - original: vk::PresentModeKHR, -) -> vk::PresentModeKHR { - match choice { - Some(value) => chosen_mode(supported, value, original), - None => original, - } -} - -fn logged_alpha_miss(original: vk::CompositeAlphaFlagsKHR) -> vk::CompositeAlphaFlagsKHR { - log_at(LogLevel::Warn, ALPHA_MISS_WARN); - original -} - -fn chosen_alpha( - mask: vk::CompositeAlphaFlagsKHR, - value: u32, - original: vk::CompositeAlphaFlagsKHR, -) -> vk::CompositeAlphaFlagsKHR { - match mask.as_raw() & value != 0 { - true => vk::CompositeAlphaFlagsKHR::from_raw(value), - false => logged_alpha_miss(original), - } -} - -fn pick_alpha( - choice: Option, - mask: vk::CompositeAlphaFlagsKHR, - original: vk::CompositeAlphaFlagsKHR, -) -> vk::CompositeAlphaFlagsKHR { - match choice { - Some(value) => chosen_alpha(mask, value, original), - None => original, - } -} - -fn toggle_vk(value: u32) -> vk::Bool32 { - match value { - TOGGLE_ON => vk::TRUE, - _ => vk::FALSE, - } -} - -fn pick_clipped(choice: Option, original: vk::Bool32) -> vk::Bool32 { - match choice { - Some(value) => toggle_vk(value), - None => original, - } -} - -fn caps_upper(caps_max: u32) -> u32 { - match caps_max { - 0 => u32::MAX, - n => n, - } -} - -fn pick_image_count( - choice: Option, - caps: &vk::SurfaceCapabilitiesKHR, - original: u32, -) -> u32 { - forced(choice, original).clamp(caps.min_image_count, caps_upper(caps.max_image_count)) -} - -fn reported_high(high: u32) -> u32 { - match high { - u32::MAX => 0, - n => n, - } -} - -fn narrowed_caps(caps: vk::SurfaceCapabilitiesKHR, value: u32) -> vk::SurfaceCapabilitiesKHR { - let held = value.clamp(caps.min_image_count, caps_upper(caps.max_image_count)); - vk::SurfaceCapabilitiesKHR { - min_image_count: held, - max_image_count: reported_high(held), - ..caps - } -} - -fn clamped_caps( - caps: vk::SurfaceCapabilitiesKHR, - choice: Option, -) -> vk::SurfaceCapabilitiesKHR { - match choice { - Some(value) => narrowed_caps(caps, value), - None => caps, - } -} - -fn log_extended(extended: bool, message: &str) { - match extended { - true => log_at(LogLevel::Info, message), - false => (), - } -} - -fn maybe_log_space(choice: Option) { - match choice.and_then(space_semantic) { - Some(facts) => log_extended(facts.extended, SPACE_EXTENDED_INFO), - None => (), - } -} - -fn maybe_log_present(choice: Option) { - match choice.and_then(present_semantic) { - Some(facts) => log_extended(facts.extended, PRESENT_EXTENDED_INFO), - None => (), - } -} - -fn log_blending(blends: bool) { - match blends { - false => log_at(LogLevel::Info, ALPHA_OPAQUE_INFO), - true => (), - } -} - -fn maybe_log_alpha(choice: Option) { - match choice.and_then(alpha_semantic) { - Some(facts) => log_blending(facts.blends), - None => (), - } -} - -fn log_encoding(encoded: bool) { - match encoded { - true => log_at(LogLevel::Info, TRANSFER_ENCODED_INFO), - false => log_at(LogLevel::Info, TRANSFER_SHADER_INFO), - } -} - -fn maybe_log_transfer(choice: Option) { - match choice.and_then(numeric_semantic) { - Some(facts) => log_encoding(facts.encoded), - None => (), - } -} - -fn excluded(choice: Option, value: Option) -> bool { - match choice { - Some(wanted) => value != Some(wanted), - None => false, - } -} - -fn maybe_warn(kept_out: bool, warn: &str) { - match kept_out { - true => log_at(LogLevel::Warn, warn), - false => (), - } -} - -fn warn_excluded_format(kept_out: bool, format: u32) { - match kept_out { - true => log_at( - LogLevel::Warn, - &format!("{}: {}", FORMAT_FORCED_WARN, format_display(format)), - ), - false => (), - } -} - -fn warn_excluded_choice(asked: vk::SurfaceFormatKHR, s: &Settings) { - warn_excluded_format( - excluded(s.depth, depth_of(&asked)), - asked.format.as_raw() as u32, - ); - maybe_warn(excluded(s.color_space, space_of(&asked)), SPACE_FORCED_WARN); - maybe_warn(excluded(s.transfer, numeric_of(&asked)), TRANSFER_FORCED_WARN); -} - -fn asked_format(original: &vk::SwapchainCreateInfoKHR) -> vk::SurfaceFormatKHR { - vk::SurfaceFormatKHR { - format: original.image_format, - color_space: original.image_color_space, - } -} - -fn patched_swapchain_ci( - original: &vk::SwapchainCreateInfoKHR, - chosen: vk::PresentModeKHR, - caps: &vk::SurfaceCapabilitiesKHR, - s: &Settings, -) -> vk::SwapchainCreateInfoKHR { - vk::SwapchainCreateInfoKHR { - present_mode: chosen, - min_image_count: pick_image_count(s.image_count, caps, original.min_image_count), - composite_alpha: pick_alpha( - s.composite_alpha, - caps.supported_composite_alpha, - original.composite_alpha, - ), - clipped: pick_clipped(s.clipped, original.clipped), - ..*original - } -} - -pub(crate) fn call_query_present_modes( - inst: &VkInstState, - phys: vk::PhysicalDevice, - surface: vk::SurfaceKHR, -) -> Vec { - let mut n: u32 = 0; - let r1 = unsafe { - (inst.surface_fp.get_physical_device_surface_present_modes_khr)(phys, surface, &mut n, ptr::null_mut()) - }; - let mut v = vec![vk::PresentModeKHR::FIFO; n as usize]; - let r2 = unsafe { - (inst.surface_fp.get_physical_device_surface_present_modes_khr)(phys, surface, &mut n, v.as_mut_ptr()) - }; - match (r1, r2) { - (vk::Result::SUCCESS, vk::Result::SUCCESS) => v, - (_, _) => Vec::new(), - } -} - -fn call_query_surface_caps( - inst: &VkInstState, - phys: vk::PhysicalDevice, - surface: vk::SurfaceKHR, -) -> vk::SurfaceCapabilitiesKHR { - let mut caps = vk::SurfaceCapabilitiesKHR::default(); - match unsafe { - (inst.surface_fp.get_physical_device_surface_capabilities_khr)(phys, surface, &mut caps) - } { - vk::Result::SUCCESS => caps, - _ => vk::SurfaceCapabilitiesKHR { - min_image_count: 1, - max_image_count: 0, - ..caps - }, - } -} - -pub(crate) fn call_query_surface_formats_all( - inst: &VkInstState, - phys: vk::PhysicalDevice, - surface: vk::SurfaceKHR, -) -> Vec { - let mut n: u32 = 0; - let r1 = unsafe { - (inst.surface_fp.get_physical_device_surface_formats_khr)(phys, surface, &mut n, ptr::null_mut()) - }; - let mut v = vec![vk::SurfaceFormatKHR::default(); n as usize]; - let r2 = unsafe { - (inst.surface_fp.get_physical_device_surface_formats_khr)(phys, surface, &mut n, v.as_mut_ptr()) - }; - match (r1, r2) { - (vk::Result::SUCCESS, vk::Result::SUCCESS) => v, - (_, _) => Vec::new(), - } -} - -fn call_filtered_formats( - inst: &VkInstState, - phys: vk::PhysicalDevice, - surface: vk::SurfaceKHR, - s: &Settings, -) -> Vec { - maybe_log_space(s.color_space); - maybe_log_transfer(s.transfer); - surface_filtered(call_query_surface_formats_all(inst, phys, surface), s) -} - -pub(crate) fn call_surface_formats( - phys: vk::PhysicalDevice, - surface: vk::SurfaceKHR, - count: *mut u32, - formats: *mut vk::SurfaceFormatKHR, -) -> vk::Result { - match owning_instance(phys) { - None => vk::Result::ERROR_INITIALIZATION_FAILED, - Some((_, inst)) => call_write_list( - &call_filtered_formats(&inst, phys, surface, ensure_settings()), - count, - formats, - ), - } -} - -fn call_filtered_modes( - inst: &VkInstState, - phys: vk::PhysicalDevice, - surface: vk::SurfaceKHR, - choice: Option, -) -> Vec { - maybe_log_present(choice); - present_filtered(call_query_present_modes(inst, phys, surface), choice) -} - -pub(crate) fn call_surface_present_modes( - phys: vk::PhysicalDevice, - surface: vk::SurfaceKHR, - count: *mut u32, - modes: *mut vk::PresentModeKHR, -) -> vk::Result { - match owning_instance(phys) { - None => vk::Result::ERROR_INITIALIZATION_FAILED, - Some((_, inst)) => call_write_list( - &call_filtered_modes(&inst, phys, surface, ensure_settings().present_mode), - count, - modes, - ), - } -} - -fn call_narrowed_result( - queried: vk::Result, - out: *mut vk::SurfaceCapabilitiesKHR, - choice: Option, -) -> vk::Result { - match queried { - vk::Result::SUCCESS => { - unsafe { *out = clamped_caps(*out, choice) }; - vk::Result::SUCCESS - } - e => e, - } -} - -pub(crate) fn call_surface_capabilities( - phys: vk::PhysicalDevice, - surface: vk::SurfaceKHR, - out: *mut vk::SurfaceCapabilitiesKHR, -) -> vk::Result { - match owning_instance(phys) { - None => vk::Result::ERROR_INITIALIZATION_FAILED, - Some((_, inst)) => call_narrowed_result( - unsafe { - (inst.surface_fp.get_physical_device_surface_capabilities_khr)(phys, surface, out) - }, - out, - ensure_settings().image_count, - ), - } -} - -fn non_null_node(p: *mut c_void) -> Option<*mut VkChainNode> { - match p.is_null() { - true => None, - false => Some(p as *mut VkChainNode), - } -} - -fn chain_nodes(head: *mut c_void) -> Vec<*mut VkChainNode> { - std::iter::successors(non_null_node(head), |node| { - non_null_node(unsafe { (**node).p_next }) - }) - .collect() -} - -fn node_type(node: *mut VkChainNode) -> u32 { - unsafe { (*node).s_type.as_raw() as u32 } -} - -fn mode_lists(head: *mut c_void) -> Vec<*mut VkPresentModeList> { - chain_nodes(head) - .into_iter() - .filter(|node| MODE_LIST_TYPES.contains(&node_type(*node))) - .map(|node| node as *mut VkPresentModeList) - .collect() -} - -fn call_read_modes(list: *mut VkPresentModeList) -> Vec { - (0..unsafe { (*list).present_mode_count } as usize) - .map(|at| unsafe { *(*list).p_present_modes.add(at) }) - .collect() -} - -fn call_write_modes(list: *mut VkPresentModeList, kept_modes: &[vk::PresentModeKHR]) { - kept_modes - .iter() - .enumerate() - .for_each(|(at, mode)| unsafe { *(*list).p_present_modes.add(at) = *mode }); - unsafe { (*list).present_mode_count = kept_modes.len() as u32 }; -} - -fn call_filtered_mode_list(list: *mut VkPresentModeList, choice: Option) { - match unsafe { (*list).p_present_modes.is_null() } { - true => (), - false => call_write_modes(list, &present_filtered(call_read_modes(list), choice)), - } -} - -fn call_filtered_chain(head: *mut c_void, choice: Option) { - mode_lists(head) - .into_iter() - .for_each(|list| call_filtered_mode_list(list, choice)); -} - -fn call_caps2_through( - fp: PfnSurfaceCaps2, - phys: vk::PhysicalDevice, - info: *const VkPhysicalDeviceSurfaceInfo2, - out: *mut VkSurfaceCapabilities2, - s: &Settings, -) -> vk::Result { - match unsafe { fp(phys, info, out) } { - vk::Result::SUCCESS => { - unsafe { - (*out).surface_capabilities = - clamped_caps((*out).surface_capabilities, s.image_count) - }; - call_filtered_chain(unsafe { (*out).p_next }, s.present_mode); - vk::Result::SUCCESS - } - e => e, - } -} - -pub(crate) fn call_surface_capabilities2( - phys: vk::PhysicalDevice, - info: *const VkPhysicalDeviceSurfaceInfo2, - out: *mut VkSurfaceCapabilities2, -) -> vk::Result { - match owning_instance(phys).and_then(|(_, inst)| inst.caps2_fp) { - None => vk::Result::ERROR_INITIALIZATION_FAILED, - Some(fp) => call_caps2_through(fp, phys, info, out, ensure_settings()), - } -} - -fn empty_format2() -> VkSurfaceFormat2 { - VkSurfaceFormat2 { - s_type: vk::StructureType::from_raw(SURFACE_FORMAT_2 as i32), - p_next: ptr::null_mut(), - surface_format: vk::SurfaceFormatKHR::default(), - } -} - -fn unwrapped_formats(wrapped: Vec) -> Vec { - wrapped.into_iter().map(|one| one.surface_format).collect() -} - -fn kept_offsets( - filled: &[VkSurfaceFormat2], - kept_formats: &[vk::SurfaceFormatKHR], -) -> Vec { - filled - .iter() - .enumerate() - .filter(|(_, one)| kept_formats.contains(&one.surface_format)) - .map(|(at, _)| at) - .collect() -} - -fn call_read_filled(count: *mut u32, out: *mut VkSurfaceFormat2) -> Vec { - (0..unsafe { *count } as usize) - .map(|at| unsafe { *out.add(at) }) - .collect() -} - -fn call_compact_to(out: *mut VkSurfaceFormat2, offsets: &[usize]) { - offsets.iter().enumerate().for_each(|(at, from)| unsafe { - (*out.add(at)).surface_format = (*out.add(*from)).surface_format - }); -} - -fn call_query_formats2_all( - fp: PfnSurfaceFormats2, - phys: vk::PhysicalDevice, - info: *const VkPhysicalDeviceSurfaceInfo2, -) -> Vec { - let mut n: u32 = 0; - let r1 = unsafe { fp(phys, info, &mut n, ptr::null_mut()) }; - let mut v = vec![empty_format2(); n as usize]; - let r2 = unsafe { fp(phys, info, &mut n, v.as_mut_ptr()) }; - match (r1, r2) { - (vk::Result::SUCCESS, vk::Result::SUCCESS) => unwrapped_formats(v), - (_, _) => Vec::new(), - } -} - -fn completed(written: usize, available: usize) -> vk::Result { - match written == available { - true => vk::Result::SUCCESS, - false => vk::Result::INCOMPLETE, - } -} - -fn call_filtered_in_place( - queried: vk::Result, - kept_formats: &[vk::SurfaceFormatKHR], - count: *mut u32, - out: *mut VkSurfaceFormat2, -) -> vk::Result { - match queried { - vk::Result::SUCCESS | vk::Result::INCOMPLETE => { - let offsets = kept_offsets(&call_read_filled(count, out), kept_formats); - call_compact_to(out, &offsets); - unsafe { *count = offsets.len() as u32 }; - completed(offsets.len(), kept_formats.len()) - } - e => e, - } -} - -fn call_formats2_into( - fp: PfnSurfaceFormats2, - phys: vk::PhysicalDevice, - info: *const VkPhysicalDeviceSurfaceInfo2, - kept_formats: &[vk::SurfaceFormatKHR], - count: *mut u32, - out: *mut VkSurfaceFormat2, -) -> vk::Result { - call_filtered_in_place( - unsafe { fp(phys, info, count, out) }, - kept_formats, - count, - out, - ) -} - -fn call_formats2_count(kept_formats: &[vk::SurfaceFormatKHR], count: *mut u32) -> vk::Result { - unsafe { *count = kept_formats.len() as u32 }; - vk::Result::SUCCESS -} - -fn call_formats2_answer( - fp: PfnSurfaceFormats2, - phys: vk::PhysicalDevice, - info: *const VkPhysicalDeviceSurfaceInfo2, - kept_formats: &[vk::SurfaceFormatKHR], - count: *mut u32, - out: *mut VkSurfaceFormat2, -) -> vk::Result { - match out.is_null() { - true => call_formats2_count(kept_formats, count), - false => call_formats2_into(fp, phys, info, kept_formats, count, out), - } -} - -fn call_formats2_through( - fp: PfnSurfaceFormats2, - phys: vk::PhysicalDevice, - info: *const VkPhysicalDeviceSurfaceInfo2, - count: *mut u32, - out: *mut VkSurfaceFormat2, - s: &Settings, -) -> vk::Result { - maybe_log_space(s.color_space); - maybe_log_transfer(s.transfer); - call_formats2_answer( - fp, - phys, - info, - &surface_filtered(call_query_formats2_all(fp, phys, info), s), - count, - out, - ) -} - -pub(crate) fn call_surface_formats2( - phys: vk::PhysicalDevice, - info: *const VkPhysicalDeviceSurfaceInfo2, - count: *mut u32, - out: *mut VkSurfaceFormat2, -) -> vk::Result { - match owning_instance(phys).and_then(|(_, inst)| inst.formats2_fp) { - None => vk::Result::ERROR_INITIALIZATION_FAILED, - Some(fp) => call_formats2_through(fp, phys, info, count, out, ensure_settings()), - } -} - -fn call_query_modes2_all( - fp: PfnSurfaceModes2, - phys: vk::PhysicalDevice, - info: *const VkPhysicalDeviceSurfaceInfo2, -) -> Vec { - let mut n: u32 = 0; - let r1 = unsafe { fp(phys, info, &mut n, ptr::null_mut()) }; - let mut v = vec![vk::PresentModeKHR::FIFO; n as usize]; - let r2 = unsafe { fp(phys, info, &mut n, v.as_mut_ptr()) }; - match (r1, r2) { - (vk::Result::SUCCESS, vk::Result::SUCCESS) => v, - (_, _) => Vec::new(), - } -} - -pub(crate) fn call_surface_present_modes2( - phys: vk::PhysicalDevice, - info: *const VkPhysicalDeviceSurfaceInfo2, - count: *mut u32, - out: *mut vk::PresentModeKHR, -) -> vk::Result { - match owning_instance(phys).and_then(|(_, inst)| inst.modes2_fp) { - None => vk::Result::ERROR_INITIALIZATION_FAILED, - Some(fp) => call_write_list( - &present_filtered( - call_query_modes2_all(fp, phys, info), - ensure_settings().present_mode, - ), - count, - out, - ), - } -} - -fn maybe_probe( - inst: &VkInstState, - dev: &VkDevState, - surface: vk::SurfaceKHR, - supported: &[vk::PresentModeKHR], - caps: &vk::SurfaceCapabilitiesKHR, -) { - match env_probe_active() { - true => call_write_probe(build_probe( - inst, - dev, - supported, - caps, - &call_query_surface_formats_all(inst, dev.phys, surface), - )), - false => (), - } -} - -fn call_created_swapchain(created: vk::Result) -> vk::Result { - match created { - vk::Result::SUCCESS => { - log_at(LogLevel::Info, "swapchain created"); - vk::Result::SUCCESS - } - e => e, - } -} - -fn call_prepared_ci( - inst: &VkInstState, - dev: &VkDevState, - original: &vk::SwapchainCreateInfoKHR, - s: &Settings, -) -> vk::SwapchainCreateInfoKHR { - let supported = call_query_present_modes(inst, dev.phys, original.surface); - let caps = call_query_surface_caps(inst, dev.phys, original.surface); - maybe_probe(inst, dev, original.surface, &supported, &caps); - maybe_log_alpha(s.composite_alpha); - warn_excluded_choice(asked_format(original), s); - patched_swapchain_ci( - original, - pick_present_mode(s.present_mode, &supported, original.present_mode), - &caps, - s, - ) -} - -fn call_create_registered( - dev: &VkDevState, - handle: vk::Device, - inst: &VkInstState, - original: &vk::SwapchainCreateInfoKHR, - s: &Settings, - alloc: *const vk::AllocationCallbacks, - out: *mut vk::SwapchainKHR, -) -> vk::Result { - call_created_swapchain(unsafe { - (dev.swap_fp.create_swapchain_khr)( - handle, - &call_prepared_ci(inst, dev, original, s), - alloc, - out, - ) - }) -} - -pub(crate) fn call_create_swapchain( - dev: &VkDevState, - handle: vk::Device, - ci: *const vk::SwapchainCreateInfoKHR, - alloc: *const vk::AllocationCallbacks, - out: *mut vk::SwapchainKHR, -) -> vk::Result { - let s = ensure_settings(); - match insts_get(dev.instance_handle) { - Some(inst) => call_create_registered(dev, handle, &inst, unsafe { &*ci }, s, alloc, out), - None => unsafe { (dev.swap_fp.create_swapchain_khr)(handle, ci, alloc, out) }, - } -} - -fn call_shared_patched( - dev: &VkDevState, - inst: &VkInstState, - cis: *const vk::SwapchainCreateInfoKHR, - count: u32, - s: &Settings, -) -> Vec { - unsafe { std::slice::from_raw_parts(cis, count as usize) } - .iter() - .map(|original| call_prepared_ci(inst, dev, original, s)) - .collect() -} - -fn call_shared_through( - dev: &VkDevState, - inst: &VkInstState, - fp: PfnCreateSharedSwapchains, - handle: vk::Device, - count: u32, - cis: *const vk::SwapchainCreateInfoKHR, - alloc: *const vk::AllocationCallbacks, - out: *mut vk::SwapchainKHR, -) -> vk::Result { - call_created_swapchain(unsafe { - fp( - handle, - count, - call_shared_patched(dev, inst, cis, count, ensure_settings()).as_ptr(), - alloc, - out, - ) - }) -} - -pub(crate) fn call_create_shared_swapchains( - dev: &VkDevState, - handle: vk::Device, - count: u32, - cis: *const vk::SwapchainCreateInfoKHR, - alloc: *const vk::AllocationCallbacks, - out: *mut vk::SwapchainKHR, -) -> vk::Result { - match (dev.shared_fp, insts_get(dev.instance_handle)) { - (Some(fp), Some(inst)) => { - call_shared_through(dev, &inst, fp, handle, count, cis, alloc, out) - } - (_, _) => vk::Result::ERROR_INITIALIZATION_FAILED, - } -}