diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml index 454822c..aed02a9 100644 --- a/.github/workflows/artifacts.yml +++ b/.github/workflows/artifacts.yml @@ -22,7 +22,7 @@ jobs: run: | sudo apt-get update -qq sudo apt-get install -y --no-install-recommends \ - gcc-multilib libc6-dev-i386 libxcb1-dev python3-venv python3-dev \ + gcc-multilib libc6-dev-i386 python3-venv python3-dev \ flatpak ostree - name: Get version diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index f4f8e08..bf7e131 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -24,7 +24,7 @@ jobs: shell: bash run: | sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends gcc-multilib libc6-dev-i386 libxcb1-dev + sudo apt-get install -y --no-install-recommends gcc-multilib libc6-dev-i386 - name: Setup Rust id: setup-rust @@ -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' Cargo.toml | head -n1) - gui=$(cd volt-gui && python -c "import database; print(database.APP_VERSION)") + gui=$(cd src/volt-gui && python -c "import database; print(database.APP_VERSION)") echo "layer=${layer} gui=${gui}" test "${layer}" = "${gui}" diff --git a/Cargo.lock b/Cargo.lock index df9a6fb..b896dd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,7 +160,7 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "volt" -version = "2.1.0" +version = "2.2.0" dependencies = [ "ash", "libc", diff --git a/Cargo.toml b/Cargo.toml index a01e2f1..5b6b068 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,21 @@ [package] name = "volt" -version = "2.1.1" +version = "2.2.0" edition = "2021" rust-version = "1.85.1" [lib] name = "volt" crate-type = ["cdylib", "rlib"] -path = "volt/lib.rs" +path = "src/volt/lib.rs" [[bin]] name = "volt" -path = "volt/volt.rs" +path = "src/volt/volt.rs" [[bin]] name = "volt-probe" -path = "volt-probe/volt-probe.rs" +path = "src/volt-probe/volt-probe.rs" [dependencies] libc = "0.2" diff --git a/Makefile b/Makefile index dfa71ae..9ae53ad 100644 --- a/Makefile +++ b/Makefile @@ -68,8 +68,8 @@ PROBE := $(TARGET_DIR)/$(TRIPLE_64)/release/volt-probe GUI_BIN := $(BIN_DIR)/volt-gui DESKTOP := $(SHARE_DIR)/$(DESKTOP_FILE) -RUST_SOURCES := Cargo.toml Cargo.lock $(wildcard volt/*.rs) $(wildcard volt-probe/*.rs) -GUI_SOURCES := $(wildcard volt-gui/*.py) +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 @@ -95,7 +95,7 @@ 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; } -DIST_TREES := Cargo.toml Cargo.lock volt volt-probe volt-gui images flatpak container .github +DIST_TREES := Cargo.toml Cargo.lock src images flatpak container .github ifeq ($(DESTDIR),) ROOT_GUARD := check-root @@ -183,7 +183,7 @@ $(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 + src/volt-gui/volt-gui.py $(DESKTOP): Makefile | $(SHARE_DIR) @printf '%s\n' \ @@ -231,7 +231,7 @@ $(DIST_STAMP): $(GUI_BIN) \ install -Dm644 README.md $(DIST)/README.md install -Dm644 requirements.txt $(DIST)/requirements.txt cp -r $(DIST_TREES) $(DIST)/ - rm -rf $(DIST)/volt/target + rm -rf $(DIST)/src/volt/target mkdir -p $(DIST)/build/bundles cp $(FLATPAK_BUNDLES) $(DIST)/build/bundles/ touch $(DIST)/build/.venv diff --git a/README.md b/README.md index 781dbd7..d89eb54 100644 --- a/README.md +++ b/README.md @@ -74,13 +74,13 @@ 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. It opens a 1px window that is never mapped, creates a surface, swapchain and sampler, records what the device reported, and exits. Nothing appears on screen. +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 -- volt-probe ``` -It uses X11, which every desktop has through XWayland. Games may open Wayland or gamescope surfaces instead, and the profile is written before volt knows which. This only affects present modes, image counts and alpha modes, and the lists mostly agree. Where they don't, 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. +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 @@ -178,7 +178,7 @@ volt-gui is the PySide6 front end. Apply just saves the profile. No elevated per | GUI | Python 3.10+, PySide6 | | Flatpak bundles | `flatpak`, `ostree` | | Container release | `podman` or `docker` | -| Probe build | `libxcb` headers | +| Probe | `libxcb` or `libwayland-client` at runtime, neither required | No native aarch64 build. See [FEX-Emu / Box64](#fex-emu--box64). @@ -416,7 +416,7 @@ There's no environment override for the settings themselves. A profile file is t |------|------------| | `~/.config/volt-gui/default.toml` | default profile | | `~/.config/volt-gui/.toml` | named profiles | -| `~/.config/volt-gui/probe.toml` | what the last probe read | +| `~/.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. @@ -472,7 +472,7 @@ Your home directory is mounted into the sandbox, so profiles apply unchanged. **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. Frame limit, composite alpha and clipped presentation are left alone since those depend on your display. A preset naming something your hardware lacks resets that one to default and says which. -**Options** holds volt-gui's own preferences, not anything the layer reads: theme, transparency, 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. +**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 diff --git a/container/Containerfile b/container/Containerfile index 3e11ff3..29c02c2 100644 --- a/container/Containerfile +++ b/container/Containerfile @@ -4,7 +4,6 @@ FROM ${BASE} RUN apt-get update -qq \ && apt-get install -y --no-install-recommends \ gcc-multilib libc6-dev-i386 \ - libxcb1-dev \ python3 python3-venv python3-dev \ flatpak ostree \ ca-certificates make \ diff --git a/volt-gui/database.py b/src/volt-gui/database.py similarity index 96% rename from volt-gui/database.py rename to src/volt-gui/database.py index a999ba8..55084a4 100644 --- a/volt-gui/database.py +++ b/src/volt-gui/database.py @@ -15,7 +15,7 @@ from probe import shading_options -APP_VERSION: Final[str] = "2.1.1" +APP_VERSION: Final[str] = "2.2.0" APP_AUTHOR: Final[str] = "pythonlover02" APP_LICENSE: Final[str] = "GPL 3.0 License" APP_DESCRIPTION: Final[str] = "My AMD Adrenaline / NVIDIA Settings Linux Alternative" @@ -173,9 +173,15 @@ "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", }, diff --git a/volt-gui/presets.py b/src/volt-gui/presets.py similarity index 100% rename from volt-gui/presets.py rename to src/volt-gui/presets.py diff --git a/src/volt-gui/probe.py b/src/volt-gui/probe.py new file mode 100644 index 0000000..b7cbfa1 --- /dev/null +++ b/src/volt-gui/probe.py @@ -0,0 +1,297 @@ +import os + +from functools import reduce +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 +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" +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 _tags_for(value: str, sources: tuple) -> tuple: + return tuple(name for name, values in sources if value in values) + + +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 _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 _surface_lists(data: tuple, key: str) -> tuple: + return tuple( + (name, values) + for name, values in ( + (n, probe_list(v, key)) for n, v in probe_surfaces(data)) + if values != ()) + + +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]) -> tuple: + match limit: + case None: + return () + case value: + return ((OFF_VALUE, OFF_VALUE),) + plain_pairs( + _whole_values(WHOLE_STEP, int(value))) + + +def aniso_options(data: tuple) -> tuple: + match probe_flag(probe_device(data), "sampler_anisotropy"): + case False: + return () + case True: + return _aniso_ladder(probe_number(probe_device(data), "max_anisotropy")) + + +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) -> tuple: + match probe_flag(probe_device(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 _count_values(low: Optional[float], high: Optional[float]) -> tuple: + match (low, high): + case (None, _) | (_, None): + return () + case (start, stop): + return _whole_values(int(start), _count_ceiling(int(start), int(stop))) + + +def _count_sources(data: tuple) -> tuple: + return tuple( + (name, values) + for name, values in ( + (n, _count_values( + probe_number(v, "min_image_count"), + probe_number(v, "max_image_count"))) + for n, v in probe_surfaces(data)) + if values != ()) + + +def image_count_options(data: tuple) -> tuple: + return tagged_count_pairs(_count_sources(data)) + + +def _mip_values(limit: Optional[float]) -> tuple: + match limit: + case None: + return () + case value: + return plain_pairs(_whole_values(0, int(value))) + + +def mip_options(data: tuple) -> tuple: + return _mip_values(probe_number(probe_device(data), "max_lod_level")) + + +def _bias_ladder(span: int) -> tuple: + return plain_pairs(_fraction_values(-span, span)) + + +def _bias_values(limit: Optional[float]) -> tuple: + match limit: + case None: + return () + case value: + return _bias_ladder(_span_of(min(value, BIAS_CEILING))) + + +def lod_bias_options(data: tuple) -> tuple: + return _bias_values(probe_number(probe_device(data), "max_lod_bias")) diff --git a/volt-gui/profiles.py b/src/volt-gui/profiles.py similarity index 100% rename from volt-gui/profiles.py rename to src/volt-gui/profiles.py diff --git a/volt-gui/themes.py b/src/volt-gui/themes.py similarity index 100% rename from volt-gui/themes.py rename to src/volt-gui/themes.py diff --git a/volt-gui/ui.py b/src/volt-gui/ui.py similarity index 100% rename from volt-gui/ui.py rename to src/volt-gui/ui.py diff --git a/volt-gui/volt-gui.py b/src/volt-gui/volt-gui.py similarity index 95% rename from volt-gui/volt-gui.py rename to src/volt-gui/volt-gui.py index ff2264b..c3aca4f 100644 --- a/volt-gui/volt-gui.py +++ b/src/volt-gui/volt-gui.py @@ -78,7 +78,9 @@ PREVIEW_START_MS: Final[int] = 300 PREVIEW_STOP_MS: Final[int] = 1500 BUNDLE_ATTR: Final[str] = "_MEIPASS" -BUNDLE_VARS: Final[tuple] = ("LD_LIBRARY_PATH", "LD_PRELOAD") +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." @@ -139,9 +141,18 @@ def _cleaned_path(value: str, bundle: str) -> str: entry for entry in value.split(os.pathsep) if _outside_bundle(entry, bundle)) +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: - for name in BUNDLE_VARS: - os.environ.pop(name, None) + call_restore_lib_path() + os.environ.pop(PRELOAD_VAR, None) return None @@ -171,6 +182,25 @@ def calculate_initial_scale() -> None: return None +def build_platform_chain(platform: str) -> str: + match platform: + case "xcb": + return "xcb;wayland" + case "wayland": + return "wayland;xcb" + case other: + return other + + +def calculate_initial_platform() -> None: + match get_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, option_key: str) -> str: match main_window.options_widgets.get(option_key): case None: @@ -466,10 +496,11 @@ def process_tray_option_update(main_window, tray_enabled: bool) -> None: def process_options_application(main_window) -> None: process_theme_application(QApplication.instance(), get_resolved_option_value(main_window, "application_theme")) - match is_option_enabled(main_window, "window_transparency"): - case True: + match (is_option_enabled(main_window, "window_transparency"), + QApplication.instance().platformName()): + case (True, "xcb"): main_window.setWindowOpacity(0.95) - case False: + case _: main_window.setWindowOpacity(1.0) 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") @@ -611,13 +642,17 @@ def process_probe_rebuild(main_window) -> None: def process_probe_poll(main_window) -> None: - match call_probe_stamp(): - case stamp if stamp == main_window.probe_stamp: - return None - case stamp: + 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: @@ -737,6 +772,7 @@ def create_main_window_widget(singleton_socket): 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.setAttribute(Qt.WA_DontShowOnScreen, True) @@ -853,6 +889,7 @@ def main() -> None: pass os.environ.setdefault("QT_LOGGING_RULES", "qt.qpa.theme.gnome=false") call_clean_environment() + calculate_initial_platform() calculate_initial_scale() application = QApplication(sys.argv) application.setStyle("Fusion") diff --git a/volt-gui/welcome.py b/src/volt-gui/welcome.py similarity index 93% rename from volt-gui/welcome.py rename to src/volt-gui/welcome.py index e033366..98a6298 100644 --- a/volt-gui/welcome.py +++ b/src/volt-gui/welcome.py @@ -82,10 +82,10 @@ def get_welcome_settings() -> dict: ), "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", "It opens a one pixel window that is never mapped, creates a surface, a swapchain and a sampler so the layer sees every path it needs, records what the device reported, and exits. Nothing appears on screen and nothing is drawn."), - ("text", "It opens an X11 surface, which every desktop has, since a Wayland session runs XWayland. That is not the only surface a game opens: Wine and Proton have native Wayland drivers, and gamescope is its own path again. The profile is written before any of them exists, so volt cannot know which one the game will pick, and reporting two backends would offer you values belonging to the path the game did not take."), - ("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 lists mostly agree across backends, and where they do not, 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. Reading a native Wayland surface directly is on the list for later."), - ("text", "volt-probe is built by make and installed next to volt and volt-gui, so there is nothing extra to fetch. It links libxcb, which every desktop already carries."), + ("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:"), ) }, 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/volt-probe/volt-probe.rs b/src/volt-probe/volt-probe.rs similarity index 56% rename from volt-probe/volt-probe.rs rename to src/volt-probe/volt-probe.rs index f666226..9fec52f 100644 --- a/volt-probe/volt-probe.rs +++ b/src/volt-probe/volt-probe.rs @@ -1,5 +1,8 @@ +mod dl; +mod wayland; +mod xcb; + use std::ffi::c_char; -use std::ffi::c_int; use std::ffi::c_void; use std::ffi::CStr; use std::ffi::CString; @@ -13,123 +16,49 @@ const API_PATCH: u32 = 0; const API_VARIANT: u32 = 0; const EXIT_OK: i32 = 0; const EXIT_FAIL: i32 = 1; -const WINDOW_EDGE: u16 = 1; -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; const IMAGE_LAYERS: u32 = 1; const QUEUE_COUNT: u32 = 1; const QUEUE_PRIORITY: f32 = 1.0; -const SURFACE_TYPE: i32 = 1000005000; const EXT_SURFACE: &str = "VK_KHR_surface"; -const EXT_XCB_SURFACE: &str = "VK_KHR_xcb_surface"; const EXT_SWAPCHAIN: &str = "VK_KHR_swapchain"; -const FN_CREATE_SURFACE: &str = "vkCreateXcbSurfaceKHR"; const FN_CREATE_SWAPCHAIN: &str = "vkCreateSwapchainKHR"; const FN_DESTROY_SWAPCHAIN: &str = "vkDestroySwapchainKHR"; -const WANTED_EXTENSIONS: [&str; 2] = [EXT_SURFACE, EXT_XCB_SURFACE]; - -#[repr(C)] -pub struct XcbConnection { - _opaque: [u8; 0], -} - -#[repr(C)] -pub struct XcbSetup { - _opaque: [u8; 0], -} - -#[repr(C)] -pub struct XcbScreen { - pub root: u32, - pub default_colormap: u32, - pub white_pixel: u32, - pub black_pixel: u32, - pub current_input_masks: u32, - pub width_in_pixels: u16, - pub height_in_pixels: u16, - pub width_in_millimeters: u16, - pub height_in_millimeters: u16, - pub min_installed_maps: u16, - pub max_installed_maps: u16, - pub root_visual: u32, - pub backing_stores: u8, - pub save_unders: u8, - pub root_depth: u8, - pub allowed_depths_len: u8, -} - -#[repr(C)] -pub struct XcbScreenIterator { - pub data: *mut XcbScreen, - pub rem: c_int, - pub index: c_int, -} - -#[repr(C)] -pub struct VkXcbSurfaceCreateInfo { - pub s_type: vk::StructureType, - pub p_next: *const c_void, - pub flags: u32, - pub connection: *mut XcbConnection, - pub window: u32, -} - -pub type PfnCreateXcbSurface = unsafe extern "system" fn( - vk::Instance, - *const VkXcbSurfaceCreateInfo, - *const vk::AllocationCallbacks<'_>, - *mut vk::SurfaceKHR, -) -> vk::Result; +pub(crate) const WINDOW_EDGE: u16 = 1; -pub type PfnCreateSwapchain = unsafe extern "system" fn( +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; -pub type PfnDestroySwapchain = unsafe extern "system" fn( +type PfnDestroySwapchain = unsafe extern "system" fn( vk::Device, vk::SwapchainKHR, *const vk::AllocationCallbacks<'_>, ); -pub struct Window { - pub connection: *mut XcbConnection, - pub handle: u32, -} - -#[link(name = "xcb")] -extern "C" { - fn xcb_connect(name: *const c_char, screen: *mut c_int) -> *mut XcbConnection; - fn xcb_disconnect(connection: *mut XcbConnection); - fn xcb_connection_has_error(connection: *mut XcbConnection) -> c_int; - fn xcb_get_setup(connection: *mut XcbConnection) -> *const XcbSetup; - fn xcb_setup_roots_iterator(setup: *const XcbSetup) -> XcbScreenIterator; - fn xcb_generate_id(connection: *mut XcbConnection) -> u32; - fn xcb_create_window( - connection: *mut XcbConnection, - depth: u8, - window: u32, - parent: u32, - x: i16, - y: i16, - width: u16, - height: u16, - border: u16, - class: u16, - visual: u32, - mask: u32, - values: *const u32, - ) -> u32; - fn xcb_flush(connection: *mut XcbConnection) -> c_int; +fn wanted_extensions() -> Vec<&'static str> { + std::iter::once(EXT_SURFACE) + .chain(BACKENDS.iter().map(|backend| backend.extension)) + .collect() } fn available_name(one: &vk::ExtensionProperties) -> Option { @@ -149,10 +78,10 @@ fn available_names(entry: &ash::Entry) -> Vec { fn enabled_names(entry: &ash::Entry) -> Vec { let available = available_names(entry); - WANTED_EXTENSIONS - .iter() - .filter(|name| available.iter().any(|one| one == *name)) - .filter_map(|name| CString::new(*name).ok()) + wanted_extensions() + .into_iter() + .filter(|name| available.iter().any(|one| one.as_str() == *name)) + .filter_map(|name| CString::new(name).ok()) .collect() } @@ -215,70 +144,6 @@ fn sampler_info() -> vk::SamplerCreateInfo<'static> { } } -fn surface_info(window: &Window) -> VkXcbSurfaceCreateInfo { - VkXcbSurfaceCreateInfo { - s_type: vk::StructureType::from_raw(SURFACE_TYPE), - p_next: std::ptr::null(), - flags: NO_FLAGS, - connection: window.connection, - window: window.handle, - } -} - -fn call_connected(connection: *mut XcbConnection) -> Option<*mut XcbConnection> { - match unsafe { xcb_connection_has_error(connection) } { - NO_ERROR => Some(connection), - _ => None, - } -} - -fn call_open_connection() -> Option<*mut XcbConnection> { - call_connected(unsafe { xcb_connect(std::ptr::null(), std::ptr::null_mut()) }) -} - -fn call_root_screen(connection: *mut XcbConnection) -> Option<*mut XcbScreen> { - match unsafe { xcb_setup_roots_iterator(xcb_get_setup(connection)) }.data { - screen if screen.is_null() => None, - screen => Some(screen), - } -} - -fn call_place_window(connection: *mut XcbConnection, 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, - std::ptr::null(), - ) - }; - unsafe { xcb_flush(connection) }; - handle -} - -fn call_open_window() -> Option { - let connection = call_open_connection()?; - let screen = call_root_screen(connection)?; - Some(Window { - connection, - handle: call_place_window(connection, screen), - }) -} - -fn call_close_window(window: &Window) { - unsafe { xcb_disconnect(window.connection) }; -} - fn call_entry() -> Option { unsafe { ash::Entry::load() }.ok() } @@ -299,33 +164,6 @@ fn call_create_instance(entry: &ash::Entry) -> Option { unsafe { entry.create_instance(&info, None) }.ok() } -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 { std::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, - window: &Window, -) -> Option { - let create = call_surface_fn(entry, instance)?; - let info = surface_info(window); - let mut surface = vk::SurfaceKHR::null(); - call_surface_result( - unsafe { create(instance.handle(), &info, std::ptr::null(), &mut surface) }, - surface, - ) -} - fn call_first_physical(instance: &ash::Instance) -> Option { unsafe { instance.enumerate_physical_devices() } .ok() @@ -413,23 +251,75 @@ fn call_create_sampler(device: &ash::Device) -> Option { unsafe { device.create_sampler(&sampler_info(), None) }.ok() } -fn call_on_device( - entry: &ash::Entry, +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, - surface: vk::SurfaceKHR, device: &ash::Device, + surfaces: &ash::khr::surface::Instance, + surface: vk::SurfaceKHR, ) -> Option<()> { - let surfaces = ash::khr::surface::Instance::new(entry, instance); - let caps = call_surface_caps(&surfaces, phys, surface)?; - let format = call_first_format(&surfaces, phys, surface)?; + let caps = call_surface_caps(surfaces, phys, surface)?; + let format = call_first_format(surfaces, phys, surface)?; 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)?; - let sampler = call_create_sampler(device)?; - unsafe { device.destroy_sampler(sampler, None) }; 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, + backend: &Backend, +) -> Option<()> { + 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, +) { + BACKENDS.iter().for_each(|backend| { + let _ = call_on_backend(entry, instance, phys, device, backend); + }); +} + +fn call_on_device( + entry: &ash::Entry, + instance: &ash::Instance, + phys: vk::PhysicalDevice, + device: &ash::Device, +) -> Option<()> { + call_exercise_sampler(device)?; + call_every_backend(entry, instance, phys, device); Some(()) } @@ -437,33 +327,26 @@ fn call_with_device( entry: &ash::Entry, instance: &ash::Instance, phys: vk::PhysicalDevice, - surface: vk::SurfaceKHR, ) -> Option<()> { let device = call_create_device(instance, phys, call_graphics_family(instance, phys)?)?; - let done = call_on_device(entry, instance, phys, surface, &device); + let done = call_on_device(entry, instance, phys, &device); unsafe { device.destroy_device(None) }; done } -fn call_exercise(entry: &ash::Entry, instance: &ash::Instance, window: &Window) -> Option<()> { - let surface = call_create_surface(entry, instance, window)?; - let phys = call_first_physical(instance)?; - call_with_device(entry, instance, phys, surface) +fn call_on_instance(entry: &ash::Entry, instance: &ash::Instance) -> Option<()> { + call_with_device(entry, instance, call_first_physical(instance)?) } -fn call_with_instance(entry: &ash::Entry, window: &Window) -> Option<()> { +fn call_with_instance(entry: &ash::Entry) -> Option<()> { let instance = call_create_instance(entry)?; - let done = call_exercise(entry, &instance, window); + let done = call_on_instance(entry, &instance); unsafe { instance.destroy_instance(None) }; done } fn call_probe() -> Option<()> { - let entry = call_entry()?; - let window = call_open_window()?; - let done = call_with_instance(&entry, &window); - call_close_window(&window); - done + call_with_instance(&call_entry()?) } fn call_status() -> i32 { diff --git a/src/volt-probe/wayland.rs b/src/volt-probe/wayland.rs new file mode 100644 index 0000000..6e7b8da --- /dev/null +++ b/src/volt-probe/wayland.rs @@ -0,0 +1,289 @@ +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 LIBRARIES: [&str; 2] = ["libwayland-client.so.0", "libwayland-client.so"]; +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("wl_display_connect")?, + disconnect: library.symbol("wl_display_disconnect")?, + roundtrip: library.symbol("wl_display_roundtrip")?, + marshal: library.symbol("wl_proxy_marshal_flags")?, + add_listener: library.symbol("wl_proxy_add_listener")?, + get_version: library.symbol("wl_proxy_get_version")?, + registry_interface: library.address("wl_registry_interface")?, + compositor_interface: library.address("wl_compositor_interface")?, + surface_interface: library.address("wl_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::(), + ) + } +} + +unsafe extern "C" fn on_global( + data: *mut c_void, + registry: *mut c_void, + name: u32, + interface: *const c_char, + _version: u32, +) { + let state = &mut *(data as *mut BindState); + match CStr::from_ptr(interface).to_bytes() == COMPOSITOR_NAME { + true => state.compositor = call_bind(state.wl, registry, name), + false => (), + } +} + +unsafe 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..85120c9 --- /dev/null +++ b/src/volt-probe/xcb.rs @@ -0,0 +1,229 @@ +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 LIBRARIES: [&str; 2] = ["libxcb.so.1", "libxcb.so"]; +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("xcb_connect")?, + disconnect: library.symbol("xcb_disconnect")?, + has_error: library.symbol("xcb_connection_has_error")?, + get_setup: library.symbol("xcb_get_setup")?, + roots_iterator: library.symbol("xcb_setup_roots_iterator")?, + generate_id: library.symbol("xcb_generate_id")?, + create_window: library.symbol("xcb_create_window")?, + flush: library.symbol("xcb_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/volt/checks.rs b/src/volt/checks.rs similarity index 94% rename from volt/checks.rs rename to src/volt/checks.rs index 653d5f8..052ad2e 100644 --- a/volt/checks.rs +++ b/src/volt/checks.rs @@ -19,6 +19,8 @@ use crate::present::advanced; use crate::present::shifted_fps; 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; @@ -85,6 +87,12 @@ const ANISO_SIXTEEN_TEXT: &str = "16"; const BIAS_DOWN_TEXT: &str = "-0.6"; const OWNER_ONE: u64 = 1; const OWNER_TWO: u64 = 2; +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"; #[test] fn keeps_the_application_value_when_nothing_is_forced() { @@ -530,3 +538,19 @@ fn reports_a_setting_once_per_device_until_the_device_dies() { assert!(call_claim(&store, OWNER_ONE, SETTING_PRESENT_MODE)); assert!(!call_claim(&store, OWNER_TWO, SETTING_PRESENT_MODE)); } + +#[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/volt/config.rs b/src/volt/config.rs similarity index 100% rename from volt/config.rs rename to src/volt/config.rs diff --git a/volt/consts.rs b/src/volt/consts.rs similarity index 93% rename from volt/consts.rs rename to src/volt/consts.rs index 1743f05..d4bfcbd 100644 --- a/volt/consts.rs +++ b/src/volt/consts.rs @@ -25,6 +25,9 @@ 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"; @@ -170,6 +173,19 @@ 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 TAG_XCB: &str = "xcb"; +pub(crate) const TAG_WAYLAND: &str = "wayland"; + +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"; diff --git a/volt/device.rs b/src/volt/device.rs similarity index 97% rename from volt/device.rs rename to src/volt/device.rs index 0de2741..3d95324 100644 --- a/volt/device.rs +++ b/src/volt/device.rs @@ -17,6 +17,7 @@ use crate::consts::FN_SHARED_SWAPCHAINS; use crate::consts::FN_WRITE_SAMPLERS; use crate::consts::GPU_MISS_WARN; use crate::consts::SETTING_GPU; +use crate::env::env_probe_active; use crate::instance::all_devices; use crate::instance::call_next_gdpa; use crate::instance::call_next_gipa; @@ -34,6 +35,8 @@ use crate::instance::VkLayerLinkInfo; use crate::logging::info_wanted; use crate::logging::log_at; use crate::logging::LogLevel; +use crate::probe::build_device; +use crate::probe::call_record_device; use crate::report::call_forget_reports; use crate::report::call_report_choice; use crate::report::call_report_reading; @@ -367,6 +370,13 @@ fn gpu_line_wanted(chosen: Option) -> bool { } } +fn maybe_probe_device(inst: &VkInstState, phys: vk::PhysicalDevice, caps: &DeviceCaps) { + match env_probe_active() { + true => call_record_device(build_device(inst, phys, caps)), + false => (), + } +} + fn call_report_gpu(inst: &VkInstState, phys: vk::PhysicalDevice, handle: vk::Device) { let chosen = ensure_settings().gpu; match gpu_line_wanted(chosen) { @@ -406,6 +416,7 @@ fn register_device( instance_handle: inst_handle, }, ); + maybe_probe_device(inst, phys, &caps); call_report_gpu(inst, phys, handle); log_at(LogLevel::Info, "vk device registered"); } diff --git a/volt/env.rs b/src/volt/env.rs similarity index 100% rename from volt/env.rs rename to src/volt/env.rs diff --git a/volt/instance.rs b/src/volt/instance.rs similarity index 82% rename from volt/instance.rs rename to src/volt/instance.rs index 1ffc78b..6c4153e 100644 --- a/volt/instance.rs +++ b/src/volt/instance.rs @@ -9,12 +9,14 @@ use ash::vk; use ash::vk::Handle; use crate::config::ensure_settings; +use crate::consts::FN_DESTROY_SURFACE; 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_MODES_2; use crate::consts::GPU_EMPTY_WARN; use crate::consts::GROUP_EMPTY_WARN; +use crate::consts::SURFACE_CREATORS; use crate::lists::filtered; use crate::lists::kept; use crate::logging::log_at; @@ -66,6 +68,19 @@ pub(crate) type PfnCmdSetDepthClamp = 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, @@ -126,10 +141,13 @@ pub(crate) struct VkInstState { pub(crate) modes2_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, } 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 @@ -171,6 +189,7 @@ pub(crate) fn insts_put(h: u64, v: VkInstState) { pub(crate) fn insts_del(h: u64) { phys_owner_forget(h); + surface_tags_forget(h); match INSTS.write() { Ok(mut g) => { g.get_or_insert_with(HashMap::new).remove(&h); @@ -183,6 +202,81 @@ pub(crate) fn owning_instance(phys: vk::PhysicalDevice) -> Option<(u64, VkInstSt phys_owner_get(phys.as_raw()).and_then(|h| insts_get(h).map(|st| (h, st))) } +fn surface_tag_put(surface: u64, inst: u64, tag: &'static str) { + match SURFACE_TAGS.write() { + Ok(mut g) => { + g.get_or_insert_with(HashMap::new).insert(surface, (inst, tag)); + } + Err(_) => (), + } +} + +fn surface_tag_del(surface: u64) { + match SURFACE_TAGS.write() { + Ok(mut g) => { + g.get_or_insert_with(HashMap::new).remove(&surface); + } + Err(_) => (), + } +} + +fn surface_tags_forget(inst: u64) { + match SURFACE_TAGS.write() { + Ok(mut g) => g + .iter_mut() + .for_each(|m| m.retain(|_, owner| owner.0 != inst)), + Err(_) => (), + } +} + +pub(crate) fn surface_tag(surface: vk::SurfaceKHR) -> Option<&'static str> { + SURFACE_TAGS.read().ok().and_then(|g| { + g.as_ref() + .and_then(|m| m.get(&surface.as_raw()).map(|owner| owner.1)) + }) +} + +fn call_tagged_result( + result: vk::Result, + inst: vk::Instance, + tag: &'static str, + out: *mut vk::SurfaceKHR, +) -> vk::Result { + match result { + vk::Result::SUCCESS => { + surface_tag_put(unsafe { (*out).as_raw() }, inst.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<'_>, +) { + surface_tag_del(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 all_devices(inst: &VkInstState) -> Vec { call_owned_devices(&inst.instance) } @@ -448,6 +542,18 @@ fn call_remember_owner(handle: vk::Instance, devices: Vec) { .for_each(|phys| 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 register_instance(gipa: vk::PFN_vkGetInstanceProcAddr, handle: vk::Instance) { let static_fn = ash::StaticFn { get_instance_proc_addr: gipa }; let instance = unsafe { ash::Instance::load(&static_fn, handle) }; @@ -462,6 +568,8 @@ fn register_instance(gipa: vk::PFN_vkGetInstanceProcAddr, handle: vk::Instance) 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), + surface_fps: call_surface_creators(gipa, handle), + destroy_surface_fp: call_typed_instance_fp(gipa, handle, FN_DESTROY_SURFACE), }, ); log_at(LogLevel::Info, "vk instance registered"); diff --git a/volt/launcher.rs b/src/volt/launcher.rs similarity index 100% rename from volt/launcher.rs rename to src/volt/launcher.rs diff --git a/volt/layer.rs b/src/volt/layer.rs similarity index 92% rename from volt/layer.rs rename to src/volt/layer.rs index 5d9b088..1d85755 100644 --- a/volt/layer.rs +++ b/src/volt/layer.rs @@ -11,6 +11,10 @@ use ash::vk::Handle; use crate::config::ensure_settings; use crate::config::Settings; use crate::consts::FN_CREATE_SWAPCHAIN; +use crate::consts::FN_CREATE_WAYLAND_SURFACE; +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; @@ -30,6 +34,8 @@ use crate::consts::LAYER_LINK_INFO; use crate::consts::LAYER_NAME; 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; @@ -44,6 +50,8 @@ 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_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; @@ -134,6 +142,10 @@ fn instance_extension_hook(name: &str) -> Option<*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), + 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, } } @@ -144,6 +156,8 @@ fn instance_fp_present(inst: vk::Instance, name: &str) -> bool { (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(), + (Some(st), FN_DESTROY_SURFACE) => st.destroy_surface_fp.is_some(), + (Some(st), other) => st.surface_fps.contains_key(other), (_, _) => false, } } @@ -586,6 +600,41 @@ unsafe extern "system" fn vkDestroySwapchainKHR( } } +unsafe 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) +} + +unsafe 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) +} + +unsafe 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) +} + +unsafe extern "system" fn vkDestroySurfaceKHR( + inst: vk::Instance, + surface: vk::SurfaceKHR, + alloc: *const vk::AllocationCallbacks<'_>, +) { + call_destroy_tagged_surface(inst, surface, alloc) +} + unsafe extern "system" fn vkGetPhysicalDeviceSurfacePresentModesKHR( phys: vk::PhysicalDevice, surface: vk::SurfaceKHR, diff --git a/volt/lib.rs b/src/volt/lib.rs similarity index 100% rename from volt/lib.rs rename to src/volt/lib.rs diff --git a/volt/lists.rs b/src/volt/lists.rs similarity index 100% rename from volt/lists.rs rename to src/volt/lists.rs diff --git a/volt/logging.rs b/src/volt/logging.rs similarity index 100% rename from volt/logging.rs rename to src/volt/logging.rs diff --git a/volt/pipeline.rs b/src/volt/pipeline.rs similarity index 100% rename from volt/pipeline.rs rename to src/volt/pipeline.rs diff --git a/volt/present.rs b/src/volt/present.rs similarity index 100% rename from volt/present.rs rename to src/volt/present.rs diff --git a/volt/probe.rs b/src/volt/probe.rs similarity index 54% rename from volt/probe.rs rename to src/volt/probe.rs index e65224c..186785e 100644 --- a/volt/probe.rs +++ b/src/volt/probe.rs @@ -1,6 +1,7 @@ use std::ffi::CStr; use std::fs; -use std::sync::Once; +use std::path::PathBuf; +use std::sync::Mutex; use ash::vk; @@ -10,9 +11,12 @@ 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::VkDevState; +use crate::device::DeviceCaps; use crate::instance::all_devices; use crate::instance::device_index; use crate::instance::VkInstState; @@ -21,15 +25,9 @@ use crate::logging::LogLevel; use crate::ranks::alpha_display; use crate::ranks::present_display; -static WRITTEN: Once = Once::new(); - -pub(crate) struct ProbeData { +pub(crate) struct DeviceFacts { pub(crate) index: u32, pub(crate) names: Vec, - pub(crate) present: 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, @@ -39,6 +37,27 @@ pub(crate) struct ProbeData { 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(); @@ -100,24 +119,23 @@ fn pair(key: &str, value: &str) -> String { format!("{} = \"{}\"\n", key, value) } -pub(crate) fn build_probe( +fn section_head(tag: &str) -> String { + format!("{}{}{}\n", PROBE_SECTION_OPEN, tag, PROBE_SECTION_CLOSE) +} + +pub(crate) fn build_device( inst: &VkInstState, - dev: &VkDevState, - supported: &[vk::PresentModeKHR], - caps: &vk::SurfaceCapabilitiesKHR, -) -> ProbeData { + phys: vk::PhysicalDevice, + caps: &DeviceCaps, +) -> DeviceFacts { let all = all_devices(inst); - let features = device_features(inst, dev.phys); - ProbeData { - index: device_index(&all, dev.phys), + let features = device_features(inst, phys); + DeviceFacts { + index: device_index(&all, phys), names: device_names(inst, &all), - 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, - max_anisotropy: dev.caps.max_anisotropy, - max_lod_bias: dev.caps.max_lod_bias, - max_lod_level: dev.caps.max_lod_level, + 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), @@ -125,16 +143,24 @@ pub(crate) fn build_probe( } } -fn render(d: &ProbeData) -> String { +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(), "\n".to_string(), pair("device_index", &d.index.to_string()), pair("device_names", &joined(&d.names)), - pair("present_modes", &joined(&d.present)), - 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()), @@ -146,14 +172,93 @@ fn render(d: &ProbeData) -> String { .concat() } -fn call_write_file(text: &str) { - let _ = fs::create_dir_all(config_dir()); - match fs::write(config_dir().join(PROBE_FILE), text) { +pub(crate) fn render_surface(tag: &str, s: &SurfaceFacts) -> String { + [ + section_head(tag), + pair("present_modes", &joined(&s.present)), + pair("composite_alphas", &joined(&s.alphas)), + pair("min_image_count", &s.min_images.to_string()), + pair("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("\n") +} + +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(()) => 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))); +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/volt/ranks.rs b/src/volt/ranks.rs similarity index 100% rename from volt/ranks.rs rename to src/volt/ranks.rs diff --git a/volt/report.rs b/src/volt/report.rs similarity index 100% rename from volt/report.rs rename to src/volt/report.rs diff --git a/volt/sampler.rs b/src/volt/sampler.rs similarity index 100% rename from volt/sampler.rs rename to src/volt/sampler.rs diff --git a/volt/swapchain.rs b/src/volt/swapchain.rs similarity index 98% rename from volt/swapchain.rs rename to src/volt/swapchain.rs index ba86777..1fdfdb6 100644 --- a/volt/swapchain.rs +++ b/src/volt/swapchain.rs @@ -27,6 +27,7 @@ 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::surface_tag; use crate::instance::PfnCreateSharedSwapchains; use crate::instance::PfnSurfaceCaps2; use crate::instance::PfnSurfaceModes2; @@ -43,8 +44,8 @@ use crate::logging::LogLevel; use crate::present::cadence_display; use crate::present::method_display; use crate::present::pacing_display; -use crate::probe::build_probe; -use crate::probe::call_write_probe; +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; @@ -525,14 +526,13 @@ pub(crate) fn call_surface_present_modes2( } fn maybe_probe( - inst: &VkInstState, - dev: &VkDevState, + tag: Option<&'static str>, supported: &[vk::PresentModeKHR], caps: &vk::SurfaceCapabilitiesKHR, ) { - match env_probe_active() { - true => call_write_probe(build_probe(inst, dev, supported, caps)), - false => (), + match (env_probe_active(), tag) { + (true, Some(name)) => call_record_surface(name, build_surface(supported, caps)), + (_, _) => (), } } @@ -554,7 +554,7 @@ fn call_prepared_ci<'a>( ) -> vk::SwapchainCreateInfoKHR<'a> { 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, &supported, &caps); + maybe_probe(surface_tag(original.surface), &supported, &caps); maybe_log_alpha(s.composite_alpha); let patched = patched_swapchain_ci( original, diff --git a/volt/volt.rs b/src/volt/volt.rs similarity index 100% rename from volt/volt.rs rename to src/volt/volt.rs diff --git a/volt-gui/probe.py b/volt-gui/probe.py deleted file mode 100644 index 0c7c60f..0000000 --- a/volt-gui/probe.py +++ /dev/null @@ -1,209 +0,0 @@ -import os - -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" - -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" -TOGGLE_VALUES: Final[tuple] = ("off", "on") - - -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) -> Optional[float]: - match _is_number(probe_text(data, key)): - case True: - return float(probe_text(data, key)) - case False: - return None - - -def probe_flag(data: dict, key: str) -> bool: - return probe_text(data, key) == PROBE_ON - - -def probe_list(data: dict, key: str) -> tuple: - return tuple(v.lower() for v in probe_text(data, key).split(PROBE_SEP) if v != "") - - -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")) - - -def alpha_options(data: dict) -> tuple: - return plain_pairs(probe_list(data, "composite_alphas")) - - -def gpu_options(data: dict) -> tuple: - return tuple( - (str(at + 1), name) - for at, name in enumerate(probe_list(data, "device_names"))) - - -def _aniso_ladder(limit: Optional[float]) -> tuple: - match limit: - case None: - return () - case value: - return ((OFF_VALUE, OFF_VALUE),) + plain_pairs( - _whole_values(WHOLE_STEP, int(value))) - - -def aniso_options(data: dict) -> tuple: - match probe_flag(data, "sampler_anisotropy"): - case False: - return () - case True: - return _aniso_ladder(probe_number(data, "max_anisotropy")) - - -def _toggle_ladder(held: bool) -> tuple: - match held: - case False: - return () - case True: - return plain_pairs(TOGGLE_VALUES) - - -def alpha_one_options(data: dict) -> tuple: - return _toggle_ladder(probe_flag(data, "alpha_to_one")) - - -def clamp_options(data: dict) -> tuple: - return _toggle_ladder(probe_flag(data, "depth_clamp")) - - -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 _count_values(low: Optional[float], high: Optional[float]) -> tuple: - match (low, high): - case (None, _) | (_, None): - return () - case (start, stop): - return plain_pairs( - _whole_values(int(start), _count_ceiling(int(start), int(stop)))) - - -def image_count_options(data: dict) -> tuple: - return _count_values( - probe_number(data, "min_image_count"), - probe_number(data, "max_image_count")) - - -def _mip_values(limit: Optional[float]) -> tuple: - match limit: - case None: - return () - case value: - return plain_pairs(_whole_values(0, int(value))) - - -def mip_options(data: dict) -> tuple: - return _mip_values(probe_number(data, "max_lod_level")) - - -def _bias_ladder(span: int) -> tuple: - return plain_pairs(_fraction_values(-span, span)) - - -def _bias_values(limit: Optional[float]) -> tuple: - match limit: - case None: - return () - case value: - return _bias_ladder(_span_of(min(value, BIAS_CEILING))) - - -def lod_bias_options(data: dict) -> tuple: - return _bias_values(probe_number(data, "max_lod_bias"))