pax_global_header00006660000000000000000000000064150245727050014521gustar00rootroot0000000000000052 comment=eb620ab0f2ce4c39a1321e8f594c157c2c810c3e ValveSoftware-gamescope-eb620ab/000077500000000000000000000000001502457270500167355ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/.editorconfig000066400000000000000000000001621502457270500214110ustar00rootroot00000000000000root = true [*] end_of_line = lf insert_final_newline = true trim_trailing_whitespace = false indent_style = tab ValveSoftware-gamescope-eb620ab/.github/000077500000000000000000000000001502457270500202755ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/.github/ISSUE_TEMPLATE/000077500000000000000000000000001502457270500224605ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/.github/ISSUE_TEMPLATE/bug_report.yml000066400000000000000000000062431502457270500253600ustar00rootroot00000000000000name: Issue report description: File an issue report body: - type: checkboxes attributes: label: Is there an existing issue for this? description: Please search to see if an issue already exists for the bug you encountered. options: - label: I have searched the existing issues required: true - type: checkboxes attributes: label: Are you using any gamescope patches or a forked version of gamescope? description: Please confirm any issues occur on upstream gamescope without any patches before filing an issue here. options: - label: The issue occurs on upstream gamescope without any modifications required: true - type: textarea attributes: label: Current Behavior description: A concise description of the issue you're experiencing. validations: required: false - type: textarea attributes: label: Steps To Reproduce description: Steps to reproduce the issue. placeholder: | 1. Launch Dota 2 from Steam with the gamescope launch command `gamescope -f -r 120 -- %command%`... 2. Enter a bot match 3. Move the cursor around validations: required: false - type: textarea attributes: label: Hardware information description: | examples: - **Distro**: SteamOS 3.6.15 (`cat /etc/os-release`) - **CPU**: 32-core AMD Ryzen Threadripper 7970X (`inxi` or `cat /proc/cpuinfo`) - **GPU**: Advanced Micro Devices [AMD/ATI] Navi 31 [Radeon RX 7900 XT/7900 XTX/7900M] (`lspci -nn | grep VGA` or `lshw -C display -numeric` or `vulkaninfo --summary | grep deviceName` - **Driver Version**: Mesa 24.2.3 or NVIDIA 560.35.03 (`vulkaninfo --summary | grep driverInfo` or `nvidia-smi`) value: | - Distro: - CPU: - GPU: - Driver Version: render: markdown validations: required: false - type: textarea attributes: label: Software information description: | examples: - **Desktop environment**: KDE 6.1.5 - **Session type**: wayland (`echo $XDG_SESSION_TYPE`) - **Gamescope version**: gamescope version 3.15.9-8-gddf0d76 (gcc 14.2.1) (find this with `gamescope --version`) - **Gamescope launch command(s)**: `gamescope -f -h 2160 -w 7680 -r 120 -- %command%` value: | - Desktop environment: - Session type: - Gamescope version: - Gamescope launch command(s): render: markdown validations: required: false - type: checkboxes id: backend attributes: label: Which gamescope backends have the issue you are reporting? description: You may select more than one. options: - label: Wayland (default for nested gamescope) - label: DRM (default for embedded gamescope, i.e. gamescope-session) - label: SDL - label: OpenVR validations: required: true - type: textarea attributes: label: Logging, screenshots, or anything else description: | Please include any relevant logging or screenshots that will give us more context about the issue you are reporting. Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. validations: required: false ValveSoftware-gamescope-eb620ab/.github/ISSUE_TEMPLATE/feature_request.yml000066400000000000000000000003471502457270500264120ustar00rootroot00000000000000name: Feature request description: Share ideas for new features body: - type: textarea attributes: label: Feature request description: Share your idea for a new feature within gamescope validations: required: false ValveSoftware-gamescope-eb620ab/.github/workflows/000077500000000000000000000000001502457270500223325ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/.github/workflows/main.yml000066400000000000000000000025431502457270500240050ustar00rootroot00000000000000name: CI on: push: branches: [ master ] pull_request: branches: [ master ] workflow_dispatch: jobs: build: runs-on: ubuntu-latest container: archlinux:base-devel steps: - name: Prepare run: | pacman-key --init pacman -Syu --noconfirm pacman -S --noconfirm git meson clang glslang libcap wlroots0.18 \ sdl2 vulkan-headers libx11 libxmu libxcomposite libxrender libxres \ libxtst libxkbcommon libdrm libinput wayland-protocols benchmark \ xorg-xwayland pipewire cmake \ libavif libheif aom rav1e libdecor libxdamage \ luajit - uses: actions/checkout@v4 with: submodules: recursive - name: Build with gcc run: | export CC=gcc CXX=g++ meson build-gcc/ -Dinput_emulation=disabled --werror --auto-features=enabled ninja -C build-gcc/ - name: Build with gcc (no vr) run: | export CC=gcc CXX=g++ meson build-gcc-novr/ -Dinput_emulation=disabled -Denable_openvr_support=false --werror --auto-features=enabled ninja -C build-gcc-novr/ # - name: Build with clang # run: | # export CC=clang CXX=clang++ # meson build-clang/ -Dinput_emulation=disabled --werror --auto-features=enabled # ninja -C build-clang/ ValveSoftware-gamescope-eb620ab/.gitmodules000066400000000000000000000014721502457270500211160ustar00rootroot00000000000000[submodule "subprojects/wlroots"] path = subprojects/wlroots url = https://github.com/Joshua-Ashton/wlroots.git [submodule "subprojects/libliftoff"] path = subprojects/libliftoff url = https://gitlab.freedesktop.org/emersion/libliftoff.git [submodule "subprojects/vkroots"] path = subprojects/vkroots url = https://github.com/Joshua-Ashton/vkroots [submodule "subprojects/libdisplay-info"] path = subprojects/libdisplay-info url = https://gitlab.freedesktop.org/emersion/libdisplay-info [submodule "subprojects/openvr"] path = subprojects/openvr url = https://github.com/ValveSoftware/openvr.git [submodule "src/reshade"] path = src/reshade url = https://github.com/Joshua-Ashton/reshade [submodule "thirdparty/SPIRV-Headers"] path = thirdparty/SPIRV-Headers url = https://github.com/KhronosGroup/SPIRV-Headers/ ValveSoftware-gamescope-eb620ab/LICENSE000066400000000000000000000107611502457270500177470ustar00rootroot00000000000000BSD 2-Clause License Copyright (c) 2013-2022, Valve Corporation Copyright (c) 2022, NVIDIA CORPORATION All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ======================================================== Tetrahedal 3D LUT code from OpenColorIO Licensed under BSD 3-Clause "New" or "Revised" License: Copyright Contributors to the OpenColorIO Project. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ======================================================== Reshade effects compiler from Reshade Copyright 2014 Patrick Mours. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ValveSoftware-gamescope-eb620ab/README.md000066400000000000000000000116421502457270500202200ustar00rootroot00000000000000## gamescope: the micro-compositor formerly known as steamcompmgr In an embedded session usecase, gamescope does the same thing as steamcompmgr, but with less extra copies and latency: - It's getting game frames through Wayland by way of Xwayland, so there's no copy within X itself before it gets the frame. - It can use DRM/KMS to directly flip game frames to the screen, even when stretching or when notifications are up, removing another copy. - When it does need to composite with the GPU, it does so with async Vulkan compute, meaning you get to see your frame quick even if the game already has the GPU busy with the next frame. It also runs on top of a regular desktop, the 'nested' usecase steamcompmgr didn't support. - Because the game is running in its own personal Xwayland sandbox desktop, it can't interfere with your desktop and your desktop can't interfere with it. - You can spoof a virtual screen with a desired resolution and refresh rate as the only thing the game sees, and control/resize the output as needed. This can be useful in exotic display configurations like ultrawide or multi-monitor setups that involve rotation. It runs on Mesa + AMD or Intel, and could be made to run on other Mesa/DRM drivers with minimal work. AMD requires Mesa 20.3+, Intel requires Mesa 21.2+. For NVIDIA's proprietary driver, version 515.43.04+ is required (make sure the `nvidia-drm.modeset=1` kernel parameter is set). If running RadeonSI clients with older cards (GFX8 and below), currently have to set `R600_DEBUG=nodcc`, or corruption will be observed until the stack picks up DRM modifiers support. ## Building ``` git submodule update --init meson setup build/ ninja -C build/ build/gamescope -- ``` Install with: ``` meson install -C build/ --skip-subprojects ``` ## Keyboard shortcuts * **Super + F** : Toggle fullscreen * **Super + N** : Toggle nearest neighbour filtering * **Super + U** : Toggle FSR upscaling * **Super + Y** : Toggle NIS upscaling * **Super + I** : Increase FSR sharpness by 1 * **Super + O** : Decrease FSR sharpness by 1 * **Super + S** : Take screenshot (currently goes to `/tmp/gamescope_$DATE.png`) * **Super + G** : Toggle keyboard grab ## Examples On any X11 or Wayland desktop, you can set the Steam launch arguments of your game as follows: ```sh # Upscale a 720p game to 1440p with integer scaling gamescope -h 720 -H 1440 -S integer -- %command% # Limit a vsynced game to 30 FPS gamescope -r 30 -- %command% # Run the game at 1080p, but scale output to a fullscreen 3440×1440 pillarboxed ultrawide window gamescope -w 1920 -h 1080 -W 3440 -H 1440 -b -- %command% ``` ## Options See `gamescope --help` for a full list of options. * `-W`, `-H`: set the resolution used by gamescope. Resizing the gamescope window will update these settings. Ignored in embedded mode. If `-H` is specified but `-W` isn't, a 16:9 aspect ratio is assumed. Defaults to 1280×720. * `-w`, `-h`: set the resolution used by the game. If `-h` is specified but `-w` isn't, a 16:9 aspect ratio is assumed. Defaults to the values specified in `-W` and `-H`. * `-r`: set a frame-rate limit for the game. Specified in frames per second. Defaults to unlimited. * `-o`: set a frame-rate limit for the game when unfocused. Specified in frames per second. Defaults to unlimited. * `-F fsr`: use AMD FidelityFX™ Super Resolution 1.0 for upscaling * `-F nis`: use NVIDIA Image Scaling v1.0.3 for upscaling * `-S integer`: use integer scaling. * `-S stretch`: use stretch scaling, the game will fill the window. (e.g. 4:3 to 16:9) * `-b`: create a border-less window. * `-f`: create a full-screen window. ## Reshade support Gamescope supports a subset of Reshade effects/shaders using the `--reshade-effect [path]` and `--reshade-technique-idx [idx]` command line parameters. This provides an easy way to do shader effects (ie. CRT shader, film grain, debugging HDR with histograms, etc) on top of whatever is being displayed in Gamescope without having to hook into the underlying process. Uniform/shader options can be modified programmatically via the `gamescope-reshade` wayland interface. Otherwise, they will just use their initializer values. Using Reshade effects will increase latency as there will be work performed on the general gfx + compute queue as opposed to only using the realtime async compute queue which can run in tandem with the game's gfx work. Using Reshade effects is **highly discouraged** for doing simple transformations which can be achieved with LUTs/CTMs which are possible to do in the DC (Display Core) on AMDGPU at scanout time, or with the current regular async compute composite path. The looks system where you can specify your own 3D LUTs would be a better alternative for such transformations. Pull requests for improving Reshade compatibility support are appreciated. ## Status of Gamescope Packages [![Packaging status](https://repology.org/badge/vertical-allrepos/gamescope.svg)](https://repology.org/project/gamescope/versions) ValveSoftware-gamescope-eb620ab/default_extras_install.sh000077500000000000000000000007371502457270500240430ustar00rootroot00000000000000#!/usr/bin/env sh # Remove old Gamescope default configs and add our own. mkdir -p "${DESTDIR}/${MESON_INSTALL_PREFIX}/share/gamescope" rm -rf "${DESTDIR}/${MESON_INSTALL_PREFIX}/share/gamescope/scripts" || true rm -rf "${DESTDIR}/${MESON_INSTALL_PREFIX}/share/gamescope/looks" || true cp -r "${MESON_SOURCE_ROOT}/scripts" "${DESTDIR}/${MESON_INSTALL_PREFIX}/share/gamescope/scripts" cp -r "${MESON_SOURCE_ROOT}/looks" "${DESTDIR}/${MESON_INSTALL_PREFIX}/share/gamescope/looks" ValveSoftware-gamescope-eb620ab/layer/000077500000000000000000000000001502457270500200515ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/layer/VkLayer_FROG_gamescope_wsi.cpp000066400000000000000000001776531502457270500256770ustar00rootroot00000000000000#define VK_USE_PLATFORM_WAYLAND_KHR #define VK_USE_PLATFORM_XCB_KHR #define VK_USE_PLATFORM_XLIB_KHR #include "vkroots.h" #include "xcb_helpers.hpp" #include "vulkan_operators.hpp" #include "gamescope-swapchain-client-protocol.h" #include "../src/color_helpers.h" #include "../src/layer_defines.h" #include #include #include #include #include #include #include #include // For limiter file. #include #include #include #include "../src/messagey.h" using namespace std::literals; namespace GamescopeWSILayer { static const size_t MaxPastPresentationTimes = 16; static uint64_t timespecToNanos(struct timespec& spec) { return spec.tv_sec * 1'000'000'000ul + spec.tv_nsec; } [[maybe_unused]] static uint64_t getTimeMonotonic() { timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return timespecToNanos(ts); } static bool contains(const std::vector vec, std::string_view lookupValue) { return std::ranges::any_of(vec, std::bind_front(std::equal_to{}, lookupValue)); } static int waylandPumpEvents(wl_display *display) { int wlFd = wl_display_get_fd(display); while (true) { int ret = 0; if ((ret = wl_display_dispatch_pending(display)) < 0) return ret; if ((ret = wl_display_prepare_read(display)) < 0) { if (errno == EAGAIN) continue; return -1; } pollfd pollfd = { .fd = wlFd, .events = POLLIN, }; timespec zeroTimeout = {}; ret = ppoll(&pollfd, 1, &zeroTimeout, NULL); if (ret <= 0) { wl_display_cancel_read(display); if (ret == 0) wl_display_flush(display); return ret; } ret = wl_display_read_events(display); if (ret < 0) return ret; ret = wl_display_flush(display); return ret; } } uint32_t clientAppId() { const char *appid = getenv("SteamAppId"); if (!appid || !*appid) return 0; return atoi(appid); } static const char* gamescopeWaylandSocket() { return std::getenv("GAMESCOPE_WAYLAND_DISPLAY"); } static bool isAppInfoGamescope(const VkApplicationInfo *appInfo) { if (!appInfo || !appInfo->pApplicationName) return false; return appInfo->pApplicationName == "gamescope"sv; } static bool isRunningUnderGamescope() { static bool s_isRunningUnderGamescope = []() -> bool { const char *gamescopeSocketName = gamescopeWaylandSocket(); if (!gamescopeSocketName || !*gamescopeSocketName) return false; // Gamescope always sets or unsets WAYLAND_SOCKET. // So if that is set to something else, we know we cannot be running // under Gamescope and must be in a nested Wayland session inside of gamescope. const char *waylandSocketName = std::getenv("WAYLAND_DISPLAY"); if (waylandSocketName && *waylandSocketName && strcmp(gamescopeSocketName, waylandSocketName) != 0) return false; return true; }(); return s_isRunningUnderGamescope; } template std::optional parseEnv(const char *envName) { const char *str = std::getenv(envName); if (!str || !*str) return std::nullopt; T value; auto result = std::from_chars(str, str + strlen(str), value); if (result.ec != std::errc{}) return std::nullopt; return value; } template <> std::optional parseEnv(const char *envName) { const char *str = std::getenv(envName); if (!str || !*str) return std::nullopt; if (str == "true"sv || str == "1"sv) return true; return false; } static uint32_t getMinImageCount() { static uint32_t s_minImageCount = []() -> uint32_t { if (auto minCount = parseEnv("GAMESCOPE_WSI_MIN_IMAGE_COUNT")) { fprintf(stderr, "[Gamescope WSI] minImageCount overridden by GAMESCOPE_WSI_MIN_IMAGE_COUNT: %u\n", *minCount); return *minCount; } if (auto minCount = parseEnv("vk_wsi_override_min_image_count")) { fprintf(stderr, "[Gamescope WSI] minImageCount overridden by vk_wsi_override_min_image_count: %u\n", *minCount); return *minCount; } if (auto minCount = parseEnv("vk_x11_override_min_image_count")) { fprintf(stderr, "[Gamescope WSI] minImageCount overridden by vk_x11_override_min_image_count: %u\n", *minCount); return *minCount; } return 3u; }(); return s_minImageCount; } static bool getEnsureMinImageCount() { static bool s_ensureMinImageCount = []() -> bool { if (auto ensure = parseEnv("GAMESCOPE_WSI_ENSURE_MIN_IMAGE_COUNT")) { return *ensure; } if (auto ensure = parseEnv("vk_x11_ensure_min_image_count")) { return *ensure; } return false; }(); return s_ensureMinImageCount; } // Taken from Mesa, licensed under MIT. // // No real reason to rewrite this code, // it works :) static char * __getProgramName() { char * arg = strrchr(program_invocation_name, '/'); if (arg) { char *program_name = NULL; /* If the / character was found this is likely a linux path or * an invocation path for a 64-bit wine program. * * However, some programs pass command line arguments into argv[0]. * Strip these arguments out by using the realpath only if it was * a prefix of the invocation name. */ char *path = realpath("/proc/self/exe", NULL); if (path && strncmp(path, program_invocation_name, strlen(path)) == 0) { /* This shouldn't be null because path is a a prefix, * but check it anyway since path is static. */ char * name = strrchr(path, '/'); if (name) program_name = strdup(name + 1); } if (path) { free(path); } if (!program_name) { program_name = strdup(arg+1); } return program_name; } /* If there was no '/' at all we likely have a windows like path from * a wine application. */ arg = strrchr(program_invocation_name, '\\'); if (arg) return strdup(arg+1); return strdup(program_invocation_name); } std::string_view getExecutableName() { static std::string s_execName = []() -> std::string { const char *mesaExecutableEnv = getenv("MESA_DRICONF_EXECUTABLE_OVERRIDE"); if (mesaExecutableEnv && *mesaExecutableEnv) { fprintf(stderr, "[Gamescope WSI] Executable name overriden by MESA_DRICONF_EXECUTABLE_OVERRIDE: %s\n", mesaExecutableEnv); return mesaExecutableEnv; } const char *mesaProcessName = getenv("MESA_PROCESS_NAME"); if (mesaProcessName && *mesaProcessName) { fprintf(stderr, "[Gamescope WSI] Executable name overriden by MESA_PROCESS_NAME: %s\n", mesaExecutableEnv); return mesaProcessName; } std::string name; { char *programNameCStr = __getProgramName(); name = programNameCStr; free(programNameCStr); } fprintf(stderr, "[Gamescope WSI] Executable name: %s\n", name.c_str()); return name; }(); return s_execName; } static GamescopeLayerClient::Flags defaultLayerClientFlags(const VkApplicationInfo *pApplicationInfo, uint32_t appid) { GamescopeLayerClient::Flags flags = 0; const char *bypassEnv = getenv("GAMESCOPE_WSI_FORCE_BYPASS"); if (bypassEnv && *bypassEnv && atoi(bypassEnv) != 0) flags |= GamescopeLayerClient::Flag::ForceBypass; // My Little Pony: A Maretime Bay Adventure picks a HDR colorspace if available, // but does not render as HDR at all. if (appid == 1600780) flags |= GamescopeLayerClient::Flag::DisableHDR; const char *frameLimiterAwareEnv = getenv("GAMESCOPE_WSI_FRAME_LIMITER_AWARE"); if (frameLimiterAwareEnv && *frameLimiterAwareEnv) { if (atoi(frameLimiterAwareEnv) != 0) flags |= GamescopeLayerClient::Flag::FrameLimiterAware; } else if (pApplicationInfo && pApplicationInfo->pEngineName) { // This matches regular vkd3d, not just vkd3d-proton as well... // Oh well... /shrug. if ((pApplicationInfo->pEngineName == "vkd3d"sv && pApplicationInfo->engineVersion >= VK_MAKE_VERSION(2, 12, 0)) || (pApplicationInfo->pEngineName == "DXVK"sv && pApplicationInfo->engineVersion >= VK_MAKE_VERSION(2, 3, 0))) { flags |= GamescopeLayerClient::Flag::FrameLimiterAware; } } std::string_view executable = getExecutableName(); // Work around various Croteam games not handling // suboptimal and swapchain extent correctly. if (executable == "Talos"sv || executable == "Talos_Unrestricted"sv || executable == "Talos_VR"sv || executable == "Talos_Unrestricted_VR"sv || executable == "Sam2017"sv || executable == "Sam2017_Unrestricted"sv) { flags |= GamescopeLayerClient::Flag::ForceSwapchainExtent; flags |= GamescopeLayerClient::Flag::NoSuboptimal; } { const char *forceSwapchainExtentEnvVar = getenv("vk_wsi_force_swapchain_to_current_extent"); if (forceSwapchainExtentEnvVar && *forceSwapchainExtentEnvVar) { if (forceSwapchainExtentEnvVar == "true"sv) flags |= GamescopeLayerClient::Flag::ForceSwapchainExtent; else flags &= ~GamescopeLayerClient::Flag::ForceSwapchainExtent; } } { const char *ignoreSuboptimalEnvVar = getenv("vk_x11_ignore_suboptimal"); if (ignoreSuboptimalEnvVar && *ignoreSuboptimalEnvVar) { if (ignoreSuboptimalEnvVar == "true"sv) flags |= GamescopeLayerClient::Flag::NoSuboptimal; else flags &= ~GamescopeLayerClient::Flag::NoSuboptimal; } } return flags; } // TODO: Maybe move to Wayland event or something. // This just utilizes the same code as the Mesa path used // for without the layer or GL though. Need to keep it around anyway. static std::mutex gamescopeSwapchainLimiterFDMutex; static uint32_t gamescopeFrameLimiterOverride() { const char *path = getenv("GAMESCOPE_LIMITER_FILE"); if (!path) return 0; int fd = -1; { std::unique_lock lock(gamescopeSwapchainLimiterFDMutex); static int s_limiterFD = -1; if (s_limiterFD < 0) s_limiterFD = open(path, O_RDONLY); fd = s_limiterFD; } if (fd < 0) return 0; uint32_t overrideValue = 0; pread(fd, &overrideValue, sizeof(overrideValue), 0); return overrideValue; } static bool gamescopeIsForcingFifo() { return gamescopeFrameLimiterOverride() == 1; } struct GamescopeWaylandObjects { wl_compositor* compositor; gamescope_swapchain_factory_v2* gamescopeSwapchainFactory; static GamescopeWaylandObjects get(wl_display *display) { wl_registry *registry = wl_display_get_registry(display); if (!registry) return {}; GamescopeWaylandObjects waylandObjects{}; wl_registry_add_listener(registry, &s_registryListener, reinterpret_cast(&waylandObjects)); // Dispatch then roundtrip to get registry info. wl_display_dispatch(display); wl_display_roundtrip(display); wl_registry_destroy(registry); return waylandObjects; } bool valid() const { return compositor && gamescopeSwapchainFactory; } static const wl_registry_listener s_registryListener; }; const wl_registry_listener GamescopeWaylandObjects::s_registryListener = { .global = [](void* data, wl_registry* registry, uint32_t name, const char* interface, uint32_t version) { auto objects = reinterpret_cast(data); if (interface == "wl_compositor"sv) { objects->compositor = reinterpret_cast( wl_registry_bind(registry, name, &wl_compositor_interface, version)); } else if (interface == "gamescope_swapchain_factory_v2"sv) { objects->gamescopeSwapchainFactory = reinterpret_cast( wl_registry_bind(registry, name, &gamescope_swapchain_factory_v2_interface, version)); } }, .global_remove = [](void* data, wl_registry* registry, uint32_t name) { }, }; struct GamescopeInstanceData { wl_display* display; uint32_t appId = 0; std::string engineName; GamescopeLayerClient::Flags flags = 0; }; VKROOTS_DEFINE_SYNCHRONIZED_MAP_TYPE(GamescopeInstance, VkInstance); struct GamescopeSurfaceData { VkInstance instance; wl_display *display; GamescopeWaylandObjects waylandObjects; VkSurfaceKHR fallbackSurface; wl_surface* surface; xcb_connection_t* connection; xcb_window_t window; GamescopeLayerClient::Flags flags; bool hdrOutput; // Cached for comparison. std::optional cachedWindowRect; bool isWayland() const { // Is native Wayland? return connection == nullptr; } bool frameLimiterAware() const { return !!(flags & GamescopeLayerClient::Flag::FrameLimiterAware); } bool shouldExposeHDR() const { const bool hdrAllowed = !(flags & GamescopeLayerClient::Flag::DisableHDR); return hdrOutput && hdrAllowed; } bool canBypassXWayland() { if (isWayland()) return true; auto rect = xcb::getWindowRect(connection, window); auto largestObscuringWindowSize = xcb::getLargestObscuringChildWindowSize(connection, window); auto toplevelWindow = xcb::getToplevelWindow(connection, window); if (!rect || !largestObscuringWindowSize || !toplevelWindow) { fprintf(stderr, "[Gamescope WSI] canBypassXWayland: failed to get window info for window 0x%x.\n", window); return false; } cachedWindowRect = *rect; auto toplevelRect = xcb::getWindowRect(connection, *toplevelWindow); if (!toplevelRect) { fprintf(stderr, "[Gamescope WSI] canBypassXWayland: failed to get window info for window 0x%x.\n", window); return false; } // Some games do things like have a 1280x800 top-level window and // a 1280x720 child window for "fullscreen". // To avoid Glamor work on the XWayland side of things, have a // flag to force bypassing this. if (!!(flags & GamescopeLayerClient::Flag::ForceBypass)) return true; // If we have any child windows obscuring us bigger than 1x1, // then we cannot flip. // (There can be dummy composite redirect windows and whatever.) if (largestObscuringWindowSize->width > 1 || largestObscuringWindowSize->height > 1) { #if GAMESCOPE_WSI_BYPASS_DEBUG fprintf(stderr, "[Gamescope WSI] Largest obscuring window size: %u %u\n", largestObscuringWindowSize->width, largestObscuringWindowSize->height); #endif return false; } // If this window is not within 2px margin of error for the size of // it's top level window, then it cannot be flipped. // // Some games like Halo Infinite, make a child window that is 1280x802px // I have no idea how thtat happens, or whether its an app or Wine bug or not. if (*toplevelWindow != window) { if (iabs(rect->offset.x) > 1 || iabs(rect->offset.y) > 1 || iabs(int32_t(toplevelRect->extent.width) - int32_t(rect->extent.width)) > 2 || iabs(int32_t(toplevelRect->extent.height) - int32_t(rect->extent.height)) > 2) { #if GAMESCOPE_WSI_BYPASS_DEBUG fprintf(stderr, "[Gamescope WSI] Not within 1px margin of error. Offset: %d %d Extent: %u %u vs %u %u\n", rect->offset.x, rect->offset.y, toplevelRect->extent.width, toplevelRect->extent.height, rect->extent.width, rect->extent.height); #endif return false; } } // I want to add more checks wrt. composite redirects and such here, // but it seems what is exposed in xcb_composite is quite limited. // So let's see how it goes for now. :-) // Come back to this eventually. return true; } }; VKROOTS_DEFINE_SYNCHRONIZED_MAP_TYPE(GamescopeSurface, VkSurfaceKHR); struct GamescopeSwapchainData { gamescope_swapchain *object; wl_display* display; VkSurfaceKHR surface; // Always the Gamescope Surface surface -- so the Wayland one. bool isWayland; bool isBypassingXWayland; bool forceFifo; VkPresentModeKHR presentMode; VkExtent2D extent; uint32_t serverId = 0; bool retired = false; std::unique_ptr presentTimingMutex = std::make_unique(); std::vector pastPresentTimings; uint64_t refreshCycle = 16'666'666; }; VKROOTS_DEFINE_SYNCHRONIZED_MAP_TYPE(GamescopeSwapchain, VkSwapchainKHR); static constexpr gamescope_swapchain_listener s_swapchainListener = { .past_present_timing = []( void *data, gamescope_swapchain *object, uint32_t present_id, uint32_t desired_present_time_hi, uint32_t desired_present_time_lo, uint32_t actual_present_time_hi, uint32_t actual_present_time_lo, uint32_t earliest_present_time_hi, uint32_t earliest_present_time_lo, uint32_t present_margin_hi, uint32_t present_margin_lo) { GamescopeSwapchainData *swapchain = reinterpret_cast(data); std::unique_lock lock(*swapchain->presentTimingMutex); swapchain->pastPresentTimings.emplace_back(VkPastPresentationTimingGOOGLE { .presentID = present_id, .desiredPresentTime = (uint64_t(desired_present_time_hi) << 32) | desired_present_time_lo, .actualPresentTime = (uint64_t(actual_present_time_hi) << 32) | actual_present_time_lo, .earliestPresentTime = (uint64_t(earliest_present_time_hi) << 32) | earliest_present_time_lo, .presentMargin = (uint64_t(present_margin_hi) << 32) | present_margin_lo }); // Remove the first element if we are already at the max size. if (swapchain->pastPresentTimings.size() >= MaxPastPresentationTimes) swapchain->pastPresentTimings.erase(swapchain->pastPresentTimings.begin()); }, .refresh_cycle = []( void *data, gamescope_swapchain *object, uint32_t refresh_cycle_hi, uint32_t refresh_cycle_lo) { GamescopeSwapchainData *swapchain = reinterpret_cast(data); { std::unique_lock lock(*swapchain->presentTimingMutex); swapchain->refreshCycle = (uint64_t(refresh_cycle_hi) << 32) | refresh_cycle_lo; } fprintf(stderr, "[Gamescope WSI] Swapchain recieved new refresh cycle: %.2fms\n", swapchain->refreshCycle / 1'000'000.0); }, .retired = []( void *data, gamescope_swapchain *object) { GamescopeSwapchainData *swapchain = reinterpret_cast(data); { swapchain->retired = true; } fprintf(stderr, "[Gamescope WSI] Swapchain retired\n"); }, }; class VkInstanceOverrides { public: static VkResult CreateInstance( PFN_vkCreateInstance pfnCreateInstanceProc, const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* pInstance) { // If we are an app running under gamescope and we aren't gamescope itself, // then setup our state for xwayland bypass. if (!isRunningUnderGamescope() || isAppInfoGamescope(pCreateInfo->pApplicationInfo)) return pfnCreateInstanceProc(pCreateInfo, pAllocator, pInstance); auto enabledExts = std::vector( pCreateInfo->ppEnabledExtensionNames, pCreateInfo->ppEnabledExtensionNames + pCreateInfo->enabledExtensionCount); if (!contains(enabledExts, VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME)) enabledExts.push_back(VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME); if (!contains(enabledExts, VK_KHR_XCB_SURFACE_EXTENSION_NAME)) enabledExts.push_back(VK_KHR_XCB_SURFACE_EXTENSION_NAME); VkInstanceCreateInfo createInfo = *pCreateInfo; createInfo.enabledExtensionCount = uint32_t(enabledExts.size()); createInfo.ppEnabledExtensionNames = enabledExts.data(); setenv("vk_xwayland_wait_ready", "false", 0); setenv("vk_khr_present_wait", "true", 0); VkResult result = pfnCreateInstanceProc(&createInfo, pAllocator, pInstance); if (result != VK_SUCCESS) return result; wl_display *display = wl_display_connect(gamescopeWaylandSocket()); if (!display) { fprintf(stderr, "[Gamescope WSI] Failed to connect to gamescope socket: %s. Bypass layer will be unavailable.\n", gamescopeWaylandSocket()); return result; } { if (pCreateInfo->pApplicationInfo) { fprintf(stderr, "[Gamescope WSI] Application info:\n"); fprintf(stderr, " pApplicationName: %s\n", pCreateInfo->pApplicationInfo->pApplicationName); fprintf(stderr, " applicationVersion: %u\n", pCreateInfo->pApplicationInfo->applicationVersion); fprintf(stderr, " pEngineName: %s\n", pCreateInfo->pApplicationInfo->pEngineName); fprintf(stderr, " engineVersion: %u\n", pCreateInfo->pApplicationInfo->engineVersion); fprintf(stderr, " apiVersion: %u\n", pCreateInfo->pApplicationInfo->apiVersion); } else { fprintf(stderr, "[Gamescope WSI] No application info given.\n"); } } { uint32_t appId = clientAppId(); std::string engineName; if (pCreateInfo->pApplicationInfo && pCreateInfo->pApplicationInfo->pEngineName) engineName = pCreateInfo->pApplicationInfo->pEngineName; auto state = GamescopeInstance::create(*pInstance, GamescopeInstanceData { .display = display, .appId = appId, .engineName = engineName, .flags = defaultLayerClientFlags(pCreateInfo->pApplicationInfo, appId), }); // If we know at instance creation time we should disable HDR, force off // DXVK_HDR now. if (state->flags & GamescopeLayerClient::Flag::DisableHDR) setenv("DXVK_HDR", "0", 1); } // Work around the Mesa implementation of this being broken. // ( https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/31134 ) setenv("vk_wsi_force_swapchain_to_current_extent", "false", 0); return result; } static void DestroyInstance( const vkroots::VkInstanceDispatch* pDispatch, VkInstance instance, const VkAllocationCallbacks* pAllocator) { if (auto state = GamescopeInstance::get(instance)) { wl_display_disconnect(state->display); } GamescopeInstance::remove(instance); pDispatch->DestroyInstance(instance, pAllocator); } static VkResult CreateDevice( const vkroots::VkInstanceDispatch* pDispatch, VkPhysicalDevice physicalDevice, const VkDeviceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDevice* pDevice) { VkDeviceCreateInfo deviceCreateInfo = *pCreateInfo; std::vector extensions(pCreateInfo->ppEnabledExtensionNames, pCreateInfo->ppEnabledExtensionNames + pCreateInfo->enabledExtensionCount); if (!contains(extensions, VK_EXT_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME)) extensions.push_back(VK_EXT_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME); deviceCreateInfo.ppEnabledExtensionNames = extensions.data(); deviceCreateInfo.enabledExtensionCount = uint32_t(extensions.size()); vkroots::ChainPatcher maintenance1Patcher(&deviceCreateInfo, [&](VkPhysicalDeviceSwapchainMaintenance1FeaturesEXT *pMaintenance1) { fprintf(stderr, "[Gamescope WSI] Forcing on VK_EXT_swapchain_maintenance1.\n"); pMaintenance1->swapchainMaintenance1 = VK_TRUE; return true; }); return pDispatch->CreateDevice(physicalDevice, &deviceCreateInfo, pAllocator, pDevice); } static VkBool32 GetPhysicalDeviceXcbPresentationSupportKHR( const vkroots::VkInstanceDispatch* pDispatch, VkPhysicalDevice physicalDevice, uint32_t queueFamilyIndex, xcb_connection_t* connection, xcb_visualid_t visual_id) { auto gamescopeInstance = GamescopeInstance::get(pDispatch->Instance); if (!gamescopeInstance) return pDispatch->GetPhysicalDeviceXcbPresentationSupportKHR(physicalDevice, queueFamilyIndex, connection, visual_id); return GetPhysicalDeviceGamescopePresentationSupport(pDispatch, gamescopeInstance, physicalDevice, queueFamilyIndex); } static VkBool32 GetPhysicalDeviceXlibPresentationSupportKHR( const vkroots::VkInstanceDispatch* pDispatch, VkPhysicalDevice physicalDevice, uint32_t queueFamilyIndex, Display* dpy, VisualID visualID) { auto gamescopeInstance = GamescopeInstance::get(pDispatch->Instance); if (!gamescopeInstance) return pDispatch->GetPhysicalDeviceXlibPresentationSupportKHR(physicalDevice, queueFamilyIndex, dpy, visualID); return GetPhysicalDeviceGamescopePresentationSupport(pDispatch, gamescopeInstance, physicalDevice, queueFamilyIndex); } static VkResult CreateXcbSurfaceKHR( const vkroots::VkInstanceDispatch* pDispatch, VkInstance instance, const VkXcbSurfaceCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* pSurface) { auto gamescopeInstance = GamescopeInstance::get(instance); if (!gamescopeInstance) return pDispatch->CreateXcbSurfaceKHR(instance, pCreateInfo, pAllocator, pSurface); return CreateGamescopeSurface(pDispatch, gamescopeInstance, instance, pCreateInfo->connection, pCreateInfo->window, pAllocator, pSurface); } static VkResult CreateXlibSurfaceKHR( const vkroots::VkInstanceDispatch* pDispatch, VkInstance instance, const VkXlibSurfaceCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* pSurface) { auto gamescopeInstance = GamescopeInstance::get(instance); if (!gamescopeInstance) return pDispatch->CreateXlibSurfaceKHR(instance, pCreateInfo, pAllocator, pSurface); return CreateGamescopeSurface(pDispatch, gamescopeInstance, instance, XGetXCBConnection(pCreateInfo->dpy), xcb_window_t(pCreateInfo->window), pAllocator, pSurface); } static VkResult CreateWaylandSurfaceKHR( const vkroots::VkInstanceDispatch* pDispatch, VkInstance instance, const VkWaylandSurfaceCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* pSurface) { auto gamescopeInstance = GamescopeInstance::get(instance); if (!gamescopeInstance) return pDispatch->CreateWaylandSurfaceKHR(instance, pCreateInfo, pAllocator, pSurface); GamescopeWaylandObjects waylandObjects = GamescopeWaylandObjects::get(pCreateInfo->display); if (!waylandObjects.valid()) { fprintf(stderr, "[Gamescope WSI] Failed to get Wayland objects\n"); return VK_ERROR_SURFACE_LOST_KHR; } VkResult res = pDispatch->CreateWaylandSurfaceKHR(instance, pCreateInfo, pAllocator, pSurface); if (res != VK_SUCCESS) return res; auto gamescopeSurface = GamescopeSurface::create(*pSurface, GamescopeSurfaceData { .instance = instance, .display = pCreateInfo->display, .waylandObjects = waylandObjects, .surface = pCreateInfo->surface, .flags = gamescopeInstance->flags, .hdrOutput = false, // XXXX FIXME FIXME FIXME //hdrOutput, }); DumpGamescopeSurfaceState(gamescopeInstance, gamescopeSurface); return res; } static constexpr std::array s_ExtraHDRSurfaceFormat2s = {{ { .surfaceFormat = { VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_COLOR_SPACE_HDR10_ST2084_EXT, } }, { .surfaceFormat = { VK_FORMAT_A2R10G10B10_UNORM_PACK32, VK_COLOR_SPACE_HDR10_ST2084_EXT, } }, { .surfaceFormat = { VK_FORMAT_R16G16B16A16_SFLOAT, VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT, } }, }}; static constexpr auto s_ExtraHDRSurfaceFormats = []() { std::array array; for (size_t i = 0; i < s_ExtraHDRSurfaceFormat2s.size(); i++) array[i] = s_ExtraHDRSurfaceFormat2s[i].surfaceFormat; return array; }(); static VkResult GetPhysicalDeviceSurfaceFormatsKHR( const vkroots::VkInstanceDispatch* pDispatch, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, uint32_t* pSurfaceFormatCount, VkSurfaceFormatKHR* pSurfaceFormats) { auto gamescopeSurface = GamescopeSurface::get(surface); if (!gamescopeSurface) return pDispatch->GetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, pSurfaceFormatCount, pSurfaceFormats); const bool canBypass = gamescopeSurface->canBypassXWayland(); VkSurfaceKHR selectedSurface = canBypass ? surface : gamescopeSurface->fallbackSurface; if (!canBypass || !gamescopeSurface->shouldExposeHDR()) return pDispatch->GetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, selectedSurface, pSurfaceFormatCount, pSurfaceFormats); return vkroots::helpers::append( pDispatch->GetPhysicalDeviceSurfaceFormatsKHR, s_ExtraHDRSurfaceFormats, pSurfaceFormatCount, pSurfaceFormats, physicalDevice, selectedSurface); } static VkResult GetPhysicalDeviceSurfaceFormats2KHR( const vkroots::VkInstanceDispatch* pDispatch, VkPhysicalDevice physicalDevice, const VkPhysicalDeviceSurfaceInfo2KHR* pSurfaceInfo, uint32_t* pSurfaceFormatCount, VkSurfaceFormat2KHR* pSurfaceFormats) { auto gamescopeSurface = GamescopeSurface::get(pSurfaceInfo->surface); if (!gamescopeSurface) return pDispatch->GetPhysicalDeviceSurfaceFormats2KHR(physicalDevice, pSurfaceInfo, pSurfaceFormatCount, pSurfaceFormats); VkPhysicalDeviceSurfaceInfo2KHR surfaceInfo = *pSurfaceInfo; const bool canBypass = gamescopeSurface->canBypassXWayland(); surfaceInfo.surface = canBypass ? surfaceInfo.surface : gamescopeSurface->fallbackSurface; if (!canBypass || !gamescopeSurface->shouldExposeHDR()) return pDispatch->GetPhysicalDeviceSurfaceFormats2KHR(physicalDevice, &surfaceInfo, pSurfaceFormatCount, pSurfaceFormats); return vkroots::helpers::append( pDispatch->GetPhysicalDeviceSurfaceFormats2KHR, s_ExtraHDRSurfaceFormat2s, pSurfaceFormatCount, pSurfaceFormats, physicalDevice, &surfaceInfo); } static VkResult GetPhysicalDeviceSurfaceCapabilitiesKHR( const vkroots::VkInstanceDispatch* pDispatch, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, VkSurfaceCapabilitiesKHR* pSurfaceCapabilities) { auto gamescopeSurface = GamescopeSurface::get(surface); if (!gamescopeSurface) return pDispatch->GetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, pSurfaceCapabilities); VkResult res = VK_SUCCESS; if ((res = pDispatch->GetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, pSurfaceCapabilities)) != VK_SUCCESS) return res; if (!gamescopeSurface->isWayland()) { auto rect = xcb::getWindowRect(gamescopeSurface->connection, gamescopeSurface->window); if (!rect) return VK_ERROR_SURFACE_LOST_KHR; pSurfaceCapabilities->currentExtent = rect->extent; } pSurfaceCapabilities->minImageCount = getMinImageCount(); return VK_SUCCESS; } static VkResult GetPhysicalDeviceSurfaceCapabilities2KHR( const vkroots::VkInstanceDispatch* pDispatch, VkPhysicalDevice physicalDevice, const VkPhysicalDeviceSurfaceInfo2KHR* pSurfaceInfo, VkSurfaceCapabilities2KHR* pSurfaceCapabilities) { auto gamescopeSurface = GamescopeSurface::get(pSurfaceInfo->surface); if (!gamescopeSurface) return pDispatch->GetPhysicalDeviceSurfaceCapabilities2KHR(physicalDevice, pSurfaceInfo, pSurfaceCapabilities); // Incomplete writes here, do not return VK_INCOMPLETE. if (gamescopeIsForcingFifo() && gamescopeSurface->frameLimiterAware()) { const auto *pPresentMode = vkroots::FindInChain(pSurfaceInfo); const std::array s_SingleMode = {{ pPresentMode ? pPresentMode->presentMode : VK_PRESENT_MODE_FIFO_KHR, }}; auto [pPresentModeCompat, pPresentModeCompatParent] = vkroots::RemoveFromChain(pSurfaceCapabilities); if (pPresentModeCompat) vkroots::helpers::array(s_SingleMode, &pPresentModeCompat->presentModeCount, pPresentModeCompat->pPresentModes); VkResult res = VK_SUCCESS; if ((res = pDispatch->GetPhysicalDeviceSurfaceCapabilities2KHR(physicalDevice, pSurfaceInfo, pSurfaceCapabilities)) != VK_SUCCESS) return res; if (pPresentModeCompat) vkroots::AddToChain(pPresentModeCompatParent, pPresentModeCompat); } else { VkResult res = VK_SUCCESS; if ((res = pDispatch->GetPhysicalDeviceSurfaceCapabilities2KHR(physicalDevice, pSurfaceInfo, pSurfaceCapabilities)) != VK_SUCCESS) return res; } if (!gamescopeSurface->isWayland()) { auto rect = xcb::getWindowRect(gamescopeSurface->connection, gamescopeSurface->window); if (!rect) return VK_ERROR_SURFACE_LOST_KHR; pSurfaceCapabilities->surfaceCapabilities.currentExtent = rect->extent; } pSurfaceCapabilities->surfaceCapabilities.minImageCount = getMinImageCount(); return VK_SUCCESS; } static void GetPhysicalDeviceFeatures2( const vkroots::VkInstanceDispatch* pDispatch, VkPhysicalDevice physicalDevice, VkPhysicalDeviceFeatures2* pFeatures) { pDispatch->GetPhysicalDeviceFeatures2(physicalDevice, pFeatures); } static void GetPhysicalDeviceFeatures2KHR( const vkroots::VkInstanceDispatch* pDispatch, VkPhysicalDevice physicalDevice, VkPhysicalDeviceFeatures2* pFeatures) { GetPhysicalDeviceFeatures2(pDispatch, physicalDevice, pFeatures); } static VkResult GetPhysicalDeviceSurfacePresentModesKHR( const vkroots::VkInstanceDispatch* pDispatch, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, uint32_t* pPresentModeCount, VkPresentModeKHR* pPresentModes) { static constexpr std::array s_FifoPresentModes = {{ VK_PRESENT_MODE_FIFO_KHR, }}; if (auto state = GamescopeSurface::get(surface)) { if (gamescopeIsForcingFifo() && state->frameLimiterAware()) return vkroots::helpers::array(s_FifoPresentModes, pPresentModeCount, pPresentModes); } return pDispatch->GetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, pPresentModeCount, pPresentModes); } static void DestroySurfaceKHR( const vkroots::VkInstanceDispatch* pDispatch, VkInstance instance, VkSurfaceKHR surface, const VkAllocationCallbacks* pAllocator) { if (auto state = GamescopeSurface::get(surface)) { pDispatch->DestroySurfaceKHR(instance, state->fallbackSurface, pAllocator); wl_surface_destroy(state->surface); } GamescopeSurface::remove(surface); pDispatch->DestroySurfaceKHR(instance, surface, pAllocator); } static VkResult EnumerateDeviceExtensionProperties( const vkroots::VkInstanceDispatch* pDispatch, VkPhysicalDevice physicalDevice, const char* pLayerName, uint32_t* pPropertyCount, VkExtensionProperties* pProperties) { static constexpr std::array s_LayerExposedExts = {{ { VK_EXT_HDR_METADATA_EXTENSION_NAME, VK_EXT_HDR_METADATA_SPEC_VERSION }, { VK_GOOGLE_DISPLAY_TIMING_EXTENSION_NAME, VK_GOOGLE_DISPLAY_TIMING_SPEC_VERSION }, }}; if (pLayerName) { if (pLayerName == "VK_LAYER_FROG_gamescope_wsi"sv) { return vkroots::helpers::array(s_LayerExposedExts, pPropertyCount, pProperties); } else { return pDispatch->EnumerateDeviceExtensionProperties(physicalDevice, pLayerName, pPropertyCount, pProperties); } } VkResult result = vkroots::helpers::append( pDispatch->EnumerateDeviceExtensionProperties, s_LayerExposedExts, pPropertyCount, pProperties, physicalDevice, pLayerName); return result; } private: static VkResult CreateGamescopeSurface( const vkroots::VkInstanceDispatch* pDispatch, GamescopeInstance& gamescopeInstance, VkInstance instance, xcb_connection_t* connection, xcb_window_t window, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* pSurface) { fprintf(stderr, "[Gamescope WSI] Creating Gamescope surface: xid: 0x%x\n", window); GamescopeWaylandObjects waylandObjects = GamescopeWaylandObjects::get(gamescopeInstance->display); if (!waylandObjects.valid()) { fprintf(stderr, "[Gamescope WSI] Failed to get Wayland objects\n"); return VK_ERROR_SURFACE_LOST_KHR; } wl_surface* waylandSurface = wl_compositor_create_surface(waylandObjects.compositor); if (!waylandSurface) { fprintf(stderr, "[Gamescope WSI] Failed to create wayland surface - xid: 0x%x\n", window); return VK_ERROR_SURFACE_LOST_KHR; } GamescopeLayerClient::Flags flags = gamescopeInstance->flags; if (auto prop = xcb::getPropertyValue(connection, "GAMESCOPE_LAYER_CLIENT_FLAGS"sv)) flags = *prop; bool hdrOutput = false; if (auto prop = xcb::getPropertyValue(connection, "GAMESCOPE_HDR_OUTPUT_FEEDBACK"sv)) hdrOutput = !!*prop; wl_display_flush(gamescopeInstance->display); VkWaylandSurfaceCreateInfoKHR waylandCreateInfo = { .sType = VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR, .pNext = nullptr, .flags = 0, .display = gamescopeInstance->display, .surface = waylandSurface, }; VkResult result = pDispatch->CreateWaylandSurfaceKHR(instance, &waylandCreateInfo, pAllocator, pSurface); if (result != VK_SUCCESS) { fprintf(stderr, "[Gamescope WSI] Failed to create Vulkan wayland surface - vr: %s xid: 0x%x\n", vkroots::helpers::enumString(result), window); return result; } VkXcbSurfaceCreateInfoKHR xcbCreateInfo = { .sType = VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR, .pNext = nullptr, .flags = 0, .connection = connection, .window = window, }; VkSurfaceKHR fallbackSurface = VK_NULL_HANDLE; result = pDispatch->CreateXcbSurfaceKHR(instance, &xcbCreateInfo, pAllocator, &fallbackSurface); if (result != VK_SUCCESS) { fprintf(stderr, "[Gamescope WSI] Failed to create Vulkan xcb (fallback) surface - vr: %s xid: 0x%x\n", vkroots::helpers::enumString(result), window); return result; } fprintf(stderr, "[Gamescope WSI] Made gamescope surface for xid: 0x%x\n", window); auto gamescopeSurface = GamescopeSurface::create(*pSurface, GamescopeSurfaceData { .instance = instance, .display = gamescopeInstance->display, .waylandObjects = waylandObjects, .fallbackSurface = fallbackSurface, .surface = waylandSurface, .connection = connection, .window = window, .flags = flags, .hdrOutput = hdrOutput, }); DumpGamescopeSurfaceState(gamescopeInstance, gamescopeSurface); return result; } static void DumpGamescopeSurfaceState(GamescopeInstance& instance, GamescopeSurface& surface) { fprintf(stderr, "[Gamescope WSI] Surface state:\n"); fprintf(stderr, " steam app id: %u\n", instance->appId); fprintf(stderr, " window xid: 0x%x\n", surface->window); fprintf(stderr, " wayland surface res id: %u\n", wl_proxy_get_id(reinterpret_cast(surface->surface))); fprintf(stderr, " layer client flags: 0x%x\n", surface->flags); fprintf(stderr, " server hdr output enabled: %s\n", surface->hdrOutput ? "true" : "false"); fprintf(stderr, " hdr formats exposed to client: %s\n", surface->shouldExposeHDR() ? "true" : "false"); } static VkBool32 GetPhysicalDeviceGamescopePresentationSupport( const vkroots::VkInstanceDispatch* pDispatch, GamescopeInstance& gamescopeInstance, VkPhysicalDevice physicalDevice, uint32_t queueFamilyIndex) { return pDispatch->GetPhysicalDeviceWaylandPresentationSupportKHR(physicalDevice, queueFamilyIndex, gamescopeInstance->display); } }; class VkDeviceOverrides { public: static void DestroySwapchainKHR( const vkroots::VkDeviceDispatch* pDispatch, VkDevice device, VkSwapchainKHR swapchain, const VkAllocationCallbacks* pAllocator) { if (auto state = GamescopeSwapchain::get(swapchain)) { gamescope_swapchain_destroy(state->object); } GamescopeSwapchain::remove(swapchain); fprintf(stderr, "[Gamescope WSI] Destroying swapchain: %p\n", reinterpret_cast(swapchain)); pDispatch->DestroySwapchainKHR(device, swapchain, pAllocator); fprintf(stderr, "[Gamescope WSI] Destroyed swapchain: %p\n", reinterpret_cast(swapchain)); } static VkResult CreateSwapchainKHR( const vkroots::VkDeviceDispatch* pDispatch, VkDevice device, const VkSwapchainCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSwapchainKHR* pSwapchain) { auto gamescopeSurface = GamescopeSurface::get(pCreateInfo->surface); if (!gamescopeSurface) { static bool s_warned = false; if (!s_warned) { int messageId = -1; messagey::ShowSimple( "CreateSwapchainKHR: Creating swapchain for non-Gamescope swapchain.\nHooking has failed somewhere!\nYou may have a bad Vulkan layer interfering.\nPress OK to try to power through this error, or Cancel to stop.", "Gamescope WSI Layer Error", messagey::MessageBoxFlag::Warning | messagey::MessageBoxFlag::Simple_Cancel | messagey::MessageBoxFlag::Simple_OK, &messageId); if (messageId == 0) // Cancel abort(); s_warned = true; } return pDispatch->CreateSwapchainKHR(device, pCreateInfo, pAllocator, pSwapchain); } const bool canBypass = gamescopeSurface->canBypassXWayland(); VkSwapchainCreateInfoKHR swapchainInfo = *pCreateInfo; if (pCreateInfo->oldSwapchain) { if (auto gamescopeSwapchain = GamescopeSwapchain::get(pCreateInfo->oldSwapchain)) { gamescopeSwapchain->retired = true; // If we are going to/from being able to bypass XWayland, make sure // we NULL out oldSwapchain, as they'll be for different surfaces and swapchain types. if (gamescopeSwapchain->isBypassingXWayland != canBypass) swapchainInfo.oldSwapchain = VK_NULL_HANDLE; } } if (gamescopeSurface->flags & GamescopeLayerClient::Flag::ForceSwapchainExtent) { if (!gamescopeSurface->isWayland()) { auto rect = xcb::getWindowRect(gamescopeSurface->connection, gamescopeSurface->window); if (!rect) return VK_ERROR_SURFACE_LOST_KHR; swapchainInfo.imageExtent = rect->extent; } } // If we can't flip, fallback to the regular XCB surface on the XCB window. if (!canBypass) swapchainInfo.surface = gamescopeSurface->fallbackSurface; // We yolo to 3 min images always in Gamescope WSI, regardless of the underlying implementation. // Anyway, deal with present modes passed in... vkroots::ChainPatcher presentModePatcher(&swapchainInfo, [&](VkSwapchainPresentModesCreateInfoEXT *pPresentModesCreateInfo) { // Always send MAILBOX as the mode to the driver, as we implement FIFO ourselves -- using the // Gamescope swapchain protocol. static constexpr std::array s_MailboxMode = {{ VK_PRESENT_MODE_MAILBOX_KHR, }}; pPresentModesCreateInfo->presentModeCount = uint32_t(s_MailboxMode.size()); pPresentModesCreateInfo->pPresentModes = s_MailboxMode.data(); return true; }); // Force the colorspace to sRGB before sending to the driver. swapchainInfo.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; // We always send MAILBOX to the driver. swapchainInfo.presentMode = VK_PRESENT_MODE_MAILBOX_KHR; uint32_t minImageCount = swapchainInfo.minImageCount; if (getEnsureMinImageCount()) minImageCount = std::max(getMinImageCount(), minImageCount); swapchainInfo.minImageCount = minImageCount; fprintf(stderr, "[Gamescope WSI] Creating swapchain for xid: 0x%0x - oldSwapchain: %p - provided minImageCount: %u - minImageCount: %u - format: %s - colorspace: %s - flip: %s\n", gamescopeSurface->window, reinterpret_cast(pCreateInfo->oldSwapchain), pCreateInfo->minImageCount, minImageCount, vkroots::helpers::enumString(pCreateInfo->imageFormat), vkroots::helpers::enumString(pCreateInfo->imageColorSpace), canBypass ? "true" : "false"); // Check for VkFormat support and return VK_ERROR_INITIALIZATION_FAILED // if that VkFormat is unsupported for the underlying surface. { std::vector supportedSurfaceFormats; vkroots::helpers::enumerate( pDispatch->pPhysicalDeviceDispatch->pInstanceDispatch->GetPhysicalDeviceSurfaceFormatsKHR, supportedSurfaceFormats, pDispatch->PhysicalDevice, swapchainInfo.surface); bool supportedSwapchainFormat = std::ranges::any_of( supportedSurfaceFormats, std::bind_front(std::equal_to{}, swapchainInfo.imageFormat), &VkSurfaceFormatKHR::format) ; if (!supportedSwapchainFormat) { fprintf(stderr, "[Gamescope WSI] Refusing to make swapchain (unsupported VkFormat) for xid: 0x%0x - format: %s - colorspace: %s - flip: %s\n", gamescopeSurface->window, vkroots::helpers::enumString(pCreateInfo->imageFormat), vkroots::helpers::enumString(pCreateInfo->imageColorSpace), canBypass ? "true" : "false"); return VK_ERROR_INITIALIZATION_FAILED; } } uint32_t serverId = ~0u; if (!gamescopeSurface->isWayland()) { auto oServerId = xcb::getPropertyValue(gamescopeSurface->connection, "GAMESCOPE_XWAYLAND_SERVER_ID"sv); if (!oServerId) { fprintf(stderr, "[Gamescope WSI] Failed to get Xwayland server id. Failing swapchain creation.\n"); return VK_ERROR_SURFACE_LOST_KHR; } serverId = *oServerId; } auto gamescopeInstance = GamescopeInstance::get(gamescopeSurface->instance); if (!gamescopeInstance) { fprintf(stderr, "[Gamescope WSI] CreateSwapchainKHR: Instance for swapchain was already destroyed. (App use after free).\n"); return VK_ERROR_SURFACE_LOST_KHR; } VkResult result = pDispatch->CreateSwapchainKHR(device, &swapchainInfo, pAllocator, pSwapchain); if (result != VK_SUCCESS) { fprintf(stderr, "[Gamescope WSI] Failed to create swapchain - vr: %s xid: 0x%x\n", vkroots::helpers::enumString(result), gamescopeSurface->window); return result; } gamescope_swapchain *gamescopeSwapchainObject = gamescope_swapchain_factory_v2_create_swapchain( gamescopeSurface->waylandObjects.gamescopeSwapchainFactory, gamescopeSurface->surface); { auto gamescopeSwapchain = GamescopeSwapchain::create(*pSwapchain, GamescopeSwapchainData{ .object = gamescopeSwapchainObject, .display = gamescopeSurface->display, .surface = pCreateInfo->surface, // Always the Wayland side surface. .isWayland = gamescopeSurface->isWayland(), .isBypassingXWayland = canBypass, .forceFifo = gamescopeIsForcingFifo(), // Were we forcing fifo when this swapchain was made? .presentMode = pCreateInfo->presentMode, // The new present mode. .extent = pCreateInfo->imageExtent, .serverId = serverId, }); gamescopeSwapchain->pastPresentTimings.reserve(MaxPastPresentationTimes); gamescope_swapchain_add_listener(gamescopeSwapchainObject, &s_swapchainListener, reinterpret_cast(gamescopeSwapchain.get())); } uint32_t imageCount = 0; pDispatch->GetSwapchainImagesKHR(device, *pSwapchain, &imageCount, nullptr); fprintf(stderr, "[Gamescope WSI] Created swapchain for xid: 0x%0x swapchain: %p - imageCount: %u\n", gamescopeSurface->window, reinterpret_cast(*pSwapchain), imageCount); gamescope_swapchain_swapchain_feedback( gamescopeSwapchainObject, imageCount, uint32_t(pCreateInfo->imageFormat), uint32_t(pCreateInfo->imageColorSpace), uint32_t(pCreateInfo->compositeAlpha), uint32_t(pCreateInfo->preTransform), uint32_t(pCreateInfo->clipped), gamescopeInstance->engineName.c_str()); return VK_SUCCESS; } static VkResult AcquireNextImageKHR( const vkroots::VkDeviceDispatch* pDispatch, VkDevice device, VkSwapchainKHR swapchain, uint64_t timeout, VkSemaphore semaphore, VkFence fence, uint32_t* pImageIndex) { VkAcquireNextImageInfoKHR acquireInfo = { .sType = VK_STRUCTURE_TYPE_ACQUIRE_NEXT_IMAGE_INFO_KHR, .swapchain = swapchain, .timeout = timeout, .semaphore = semaphore, .fence = fence, .deviceMask = 0x1, }; return AcquireNextImage2KHR(pDispatch, device, &acquireInfo, pImageIndex); } static VkResult AcquireNextImage2KHR( const vkroots::VkDeviceDispatch* pDispatch, VkDevice device, const VkAcquireNextImageInfoKHR* pAcquireInfo, uint32_t* pImageIndex) { if (auto gamescopeSwapchain = GamescopeSwapchain::get(pAcquireInfo->swapchain)) { if (gamescopeSwapchain->retired) return VK_ERROR_OUT_OF_DATE_KHR; } return pDispatch->AcquireNextImage2KHR(device, pAcquireInfo, pImageIndex); } static VkResult QueuePresentKHR( const vkroots::VkDeviceDispatch* pDispatch, VkQueue queue, const VkPresentInfoKHR* pPresentInfo) { VkPresentInfoKHR presentInfo = *pPresentInfo; bool forceFifo = gamescopeIsForcingFifo(); auto pPresentTimes = vkroots::FindInChain(&presentInfo); wl_display *display = nullptr; for (uint32_t i = 0; i < presentInfo.swapchainCount; i++) { if (auto gamescopeSwapchain = GamescopeSwapchain::get(presentInfo.pSwapchains[i])) { if (gamescopeSwapchain->retired) { return VK_ERROR_OUT_OF_DATE_KHR; } if (pPresentTimes && pPresentTimes->pTimes) { assert(pPresentTimes->swapchainCount == presentInfo.swapchainCount); #if GAMESCOPE_WSI_DISPLAY_TIMING_DEBUG fprintf(stderr, "[Gamescope WSI] QueuePresentKHR: presentID: %u - desiredPresentTime: %lu - now: %lu\n", pPresentTimes->pTimes[i].presentID, pPresentTimes->pTimes[i].desiredPresentTime, getTimeMonotonic()); #endif gamescope_swapchain_set_present_time( gamescopeSwapchain->object, pPresentTimes->pTimes[i].presentID, pPresentTimes->pTimes[i].desiredPresentTime >> 32, pPresentTimes->pTimes[i].desiredPresentTime & 0xffffffff); } assert(display == nullptr || display == gamescopeSwapchain->display); display = gamescopeSwapchain->display; } } // All VkSurfaceKHR's come from the same VkInstance, so we only need to check one surface. bool frameLimiterAware = [&]() { for (uint32_t i = 0; i < presentInfo.swapchainCount; i++) { if (auto gamescopeSwapchain = GamescopeSwapchain::get(presentInfo.pSwapchains[i])) { auto gamescopeSurface = GamescopeSurface::get(gamescopeSwapchain->surface); if (gamescopeSurface) return gamescopeSurface->frameLimiterAware(); } } return false; }(); // Grab the actual intended present modes. std::optional oOriginalPresentModeInfo; const auto *pPresentModeInfo = vkroots::FindInChain(&presentInfo); if (pPresentModeInfo) oOriginalPresentModeInfo = *pPresentModeInfo; // Force all present modes to MAILBOX to the underlying driver // We implement fifo ourselves. vkroots::ChainPatcher> presentModePatcher(&presentInfo, [&](std::vector& mailboxModes, VkSwapchainPresentModeInfoEXT *pMaintenance1) { for (uint32_t i = 0; i < presentInfo.swapchainCount; i++) { if (auto gamescopeSwapchain = GamescopeSwapchain::get(presentInfo.pSwapchains[i])) { mailboxModes.emplace_back(VK_PRESENT_MODE_MAILBOX_KHR); } } pMaintenance1->pPresentModes = mailboxModes.data(); return true; }); if (display) { waylandPumpEvents(display); } else { static bool s_warned = false; if (!s_warned) { int messageId = -1; messagey::ShowSimple( "QueuePresentKHR: Attempting to present to a non-hooked swapchain.\nHooking has failed somewhere!\nYou may have a bad Vulkan layer interfering.\nPress OK to try to power through this error, or Cancel to stop.", "Gamescope WSI Layer Error", messagey::MessageBoxFlag::Warning | messagey::MessageBoxFlag::Simple_Cancel | messagey::MessageBoxFlag::Simple_OK, &messageId); if (messageId == 0) // Cancel abort(); s_warned = true; } } for (uint32_t i = 0; i < presentInfo.swapchainCount; i++) { if (auto gamescopeSwapchain = GamescopeSwapchain::get(presentInfo.pSwapchains[i])) { auto gamescopeSurface = GamescopeSurface::get(gamescopeSwapchain->surface); if (gamescopeSwapchain->isWayland || gamescopeSwapchain->isBypassingXWayland) { if (!gamescopeSwapchain->isWayland) { gamescope_swapchain_override_window_content(gamescopeSwapchain->object, gamescopeSwapchain->serverId, gamescopeSurface->window); } VkPresentModeKHR presentMode = oOriginalPresentModeInfo ? oOriginalPresentModeInfo->pPresentModes[i] : gamescopeSwapchain->presentMode; if (forceFifo && !frameLimiterAware) presentMode = VK_PRESENT_MODE_FIFO_KHR; gamescope_swapchain_set_present_mode(gamescopeSwapchain->object, uint32_t(presentMode)); } } } VkResult result = pDispatch->QueuePresentKHR(queue, &presentInfo); for (uint32_t i = 0; i < presentInfo.swapchainCount; i++) { VkSwapchainKHR swapchain = presentInfo.pSwapchains[i]; auto UpdateSwapchainResult = [&](VkResult newResult) { if (presentInfo.pResults && presentInfo.pResults[i] >= VK_SUCCESS) presentInfo.pResults[i] = newResult; if (result >= VK_SUCCESS) result = newResult; }; if (auto gamescopeSwapchain = GamescopeSwapchain::get(swapchain)) { // If we are a frame limiter aware application like DXVK or VKD3D-Proton, we don't // transparently change their vkQueuePresent to just FIFO modes, we change what is // exposed as supported in order for them to handle presentation latency like they // would as in FIFO mode. if (frameLimiterAware && gamescopeSwapchain->forceFifo != forceFifo) { fprintf(stderr, "[Gamescope WSI] Forcing swapchain recreation as frame limiter changed, and we want the app to know the exposed modes changed.\n"); UpdateSwapchainResult(VK_ERROR_OUT_OF_DATE_KHR); } auto gamescopeSurface = GamescopeSurface::get(gamescopeSwapchain->surface); if (!gamescopeSurface) { fprintf(stderr, "[Gamescope WSI] QueuePresentKHR: Surface for swapchain %u was already destroyed. (App use after free).\n", i); abort(); continue; } const bool canBypass = gamescopeSurface->canBypassXWayland(); if (canBypass != gamescopeSwapchain->isBypassingXWayland) { if (canBypass) { if (!(gamescopeSurface->flags & GamescopeLayerClient::Flag::NoSuboptimal)) UpdateSwapchainResult(VK_SUBOPTIMAL_KHR); } else { UpdateSwapchainResult(VK_ERROR_OUT_OF_DATE_KHR); } } // Emulate behaviour when currentExtent changes in X11 swapchain. if (!gamescopeSurface->isWayland() && !(gamescopeSurface->flags & GamescopeLayerClient::Flag::ForceSwapchainExtent)) { // gamescopeSurface->cachedWindowSize is set by canBypassXWayland. // TODO: Rename that to be some update cached vars thing, then read back canBypassXWayland. if (gamescopeSurface->cachedWindowRect) { const bool windowSizeChanged = gamescopeSurface->cachedWindowRect->extent != gamescopeSwapchain->extent; if (windowSizeChanged) UpdateSwapchainResult(VK_ERROR_OUT_OF_DATE_KHR); } else { fprintf(stderr, "[Gamescope WSI] QueuePresentKHR: Failed to get cached window size for swapchain %u\n", i); } } } } return result; } static void SetHdrMetadataEXT( const vkroots::VkDeviceDispatch* pDispatch, VkDevice device, uint32_t swapchainCount, const VkSwapchainKHR* pSwapchains, const VkHdrMetadataEXT* pMetadata) { for (uint32_t i = 0; i < swapchainCount; i++) { auto gamescopeSwapchain = GamescopeSwapchain::get(pSwapchains[i]); if (!gamescopeSwapchain) { fprintf(stderr, "[Gamescope WSI] SetHdrMetadataEXT: Swapchain %u does not support HDR.\n", i); continue; } const VkHdrMetadataEXT& metadata = pMetadata[i]; gamescope_swapchain_set_hdr_metadata( gamescopeSwapchain->object, color_xy_to_u16(metadata.displayPrimaryRed.x), color_xy_to_u16(metadata.displayPrimaryRed.y), color_xy_to_u16(metadata.displayPrimaryGreen.x), color_xy_to_u16(metadata.displayPrimaryGreen.y), color_xy_to_u16(metadata.displayPrimaryBlue.x), color_xy_to_u16(metadata.displayPrimaryBlue.y), color_xy_to_u16(metadata.whitePoint.x), color_xy_to_u16(metadata.whitePoint.y), nits_to_u16(metadata.maxLuminance), nits_to_u16_dark(metadata.minLuminance), nits_to_u16(metadata.maxContentLightLevel), nits_to_u16(metadata.maxFrameAverageLightLevel)); fprintf(stderr, "[Gamescope WSI] VkHdrMetadataEXT: display primaries:\n"); fprintf(stderr, " r: %.4g %.4g\n", metadata.displayPrimaryRed.x, metadata.displayPrimaryRed.y); fprintf(stderr, " g: %.4g %.4g\n", metadata.displayPrimaryGreen.x, metadata.displayPrimaryGreen.y); fprintf(stderr, " b: %.4g %.4g\n", metadata.displayPrimaryBlue.x, metadata.displayPrimaryBlue.y); fprintf(stderr, " w: %.4g %.4g\n", metadata.whitePoint.x, metadata.whitePoint.y); fprintf(stderr, " mastering luminance: min %g nits, max %g nits\n", metadata.minLuminance, metadata.maxLuminance); fprintf(stderr, " maxContentLightLevel: %g nits\n", metadata.maxContentLightLevel); fprintf(stderr, " maxFrameAverageLightLevel: %g nits\n", metadata.maxFrameAverageLightLevel); } } static VkResult GetPastPresentationTimingGOOGLE( const vkroots::VkDeviceDispatch* pDispatch, VkDevice device, VkSwapchainKHR swapchain, uint32_t* pPresentationTimingCount, VkPastPresentationTimingGOOGLE* pPresentationTimings) { auto gamescopeSwapchain = GamescopeSwapchain::get(swapchain); if (!gamescopeSwapchain) { fprintf(stderr, "[Gamescope WSI] GetPastPresentationTimingGOOGLE: Not a gamescope swapchain.\n"); return VK_ERROR_SURFACE_LOST_KHR; } // Dispatch to get the latest timings. if (waylandPumpEvents(gamescopeSwapchain->display) < 0) return VK_ERROR_SURFACE_LOST_KHR; uint32_t originalCount = *pPresentationTimingCount; std::unique_lock lock(*gamescopeSwapchain->presentTimingMutex); auto& timings = gamescopeSwapchain->pastPresentTimings; VkResult result = vkroots::helpers::array(timings, pPresentationTimingCount, pPresentationTimings); // Erase those that we returned so we don't return them again. timings.erase(timings.begin(), timings.begin() + originalCount); return result; } static VkResult GetRefreshCycleDurationGOOGLE( const vkroots::VkDeviceDispatch* pDispatch, VkDevice device, VkSwapchainKHR swapchain, VkRefreshCycleDurationGOOGLE* pDisplayTimingProperties) { auto gamescopeSwapchain = GamescopeSwapchain::get(swapchain); if (!gamescopeSwapchain) { fprintf(stderr, "[Gamescope WSI] GetRefreshCycleDurationGOOGLE: Not a gamescope swapchain.\n"); return VK_ERROR_SURFACE_LOST_KHR; } // Dispatch to get the latest cycle. if (waylandPumpEvents(gamescopeSwapchain->display) < 0) return VK_ERROR_SURFACE_LOST_KHR; std::unique_lock lock(*gamescopeSwapchain->presentTimingMutex); pDisplayTimingProperties->refreshDuration = gamescopeSwapchain->refreshCycle; return VK_SUCCESS; } }; } VKROOTS_DEFINE_LAYER_INTERFACES(GamescopeWSILayer::VkInstanceOverrides, vkroots::NoOverrides, GamescopeWSILayer::VkDeviceOverrides); VKROOTS_IMPLEMENT_SYNCHRONIZED_MAP_TYPE(GamescopeWSILayer::GamescopeInstance); VKROOTS_IMPLEMENT_SYNCHRONIZED_MAP_TYPE(GamescopeWSILayer::GamescopeSurface); VKROOTS_IMPLEMENT_SYNCHRONIZED_MAP_TYPE(GamescopeWSILayer::GamescopeSwapchain); ValveSoftware-gamescope-eb620ab/layer/VkLayer_FROG_gamescope_wsi.json.in000066400000000000000000000011711502457270500264500ustar00rootroot00000000000000{ "file_format_version" : "1.0.0", "layer" : { "name": "VK_LAYER_FROG_gamescope_wsi_@family@", "type": "GLOBAL", "api_version": "1.3.221", "library_path": "@lib_dir@/libVkLayer_FROG_gamescope_wsi_@family@.so", "implementation_version": "1", "description": "Gamescope WSI (XWayland Bypass) Layer (@family@)", "functions": { "vkNegotiateLoaderLayerInterfaceVersion": "vkNegotiateLoaderLayerInterfaceVersion" }, "enable_environment": { "ENABLE_GAMESCOPE_WSI": "1" }, "disable_environment": { "DISABLE_GAMESCOPE_WSI": "1" } } } ValveSoftware-gamescope-eb620ab/layer/meson.build000066400000000000000000000014601502457270500222140ustar00rootroot00000000000000vkroots_dep = dependency('vkroots') dep_xcb = dependency('xcb') dep_x11_xcb = dependency('x11-xcb') wayland_client = dependency('wayland-client') gamescope_wsi_layer = shared_library('VkLayer_FROG_gamescope_wsi_' + build_machine.cpu_family(), 'VkLayer_FROG_gamescope_wsi.cpp', protocols_client_src, dependencies : [ vkroots_dep, dep_xcb, dep_x11, dep_x11_xcb, glm_dep, wayland_client ], install : true ) out_lib_dir = join_paths(prefix, lib_dir) configure_file( input : 'VkLayer_FROG_gamescope_wsi.json.in', output : 'VkLayer_FROG_gamescope_wsi.' + build_machine.cpu_family() + '.json', configuration : {'family' : build_machine.cpu_family(), 'lib_dir' : out_lib_dir }, install : true, install_dir : join_paths(data_dir, 'vulkan', 'implicit_layer.d'), )ValveSoftware-gamescope-eb620ab/layer/vulkan_operators.hpp000066400000000000000000000035401502457270500241620ustar00rootroot00000000000000#pragma once #include inline bool operator == ( const VkImageSubresourceRange& a, const VkImageSubresourceRange& b) { return a.aspectMask == b.aspectMask && a.baseMipLevel == b.baseMipLevel && a.levelCount == b.levelCount && a.baseArrayLayer == b.baseArrayLayer && a.layerCount == b.layerCount; } inline bool operator != ( const VkImageSubresourceRange& a, const VkImageSubresourceRange& b) { return !operator == (a, b); } inline bool operator == ( const VkImageSubresourceLayers& a, const VkImageSubresourceLayers& b) { return a.aspectMask == b.aspectMask && a.mipLevel == b.mipLevel && a.baseArrayLayer == b.baseArrayLayer && a.layerCount == b.layerCount; } inline bool operator != ( const VkImageSubresourceLayers& a, const VkImageSubresourceLayers& b) { return !operator == (a, b); } inline bool operator == (VkExtent3D a, VkExtent3D b) { return a.width == b.width && a.height == b.height && a.depth == b.depth; } inline bool operator != (VkExtent3D a, VkExtent3D b) { return a.width != b.width || a.height != b.height || a.depth != b.depth; } inline bool operator == (VkExtent2D a, VkExtent2D b) { return a.width == b.width && a.height == b.height; } inline bool operator != (VkExtent2D a, VkExtent2D b) { return a.width != b.width || a.height != b.height; } inline bool operator == (VkOffset3D a, VkOffset3D b) { return a.x == b.x && a.y == b.y && a.z == b.z; } inline bool operator != (VkOffset3D a, VkOffset3D b) { return a.x != b.x || a.y != b.y || a.z != b.z; } inline bool operator == (VkOffset2D a, VkOffset2D b) { return a.x == b.x && a.y == b.y; } inline bool operator != (VkOffset2D a, VkOffset2D b) { return a.x != b.x || a.y != b.y; } ValveSoftware-gamescope-eb620ab/layer/xcb_helpers.hpp000066400000000000000000000125161502457270500230650ustar00rootroot00000000000000#pragma once #include #include #include #include namespace xcb { struct ReplyDeleter { template void operator()(T* ptr) const { free(const_cast*>(ptr)); } }; template using Reply = std::unique_ptr; static std::optional getAtom(xcb_connection_t* connection, std::string_view name) { xcb_intern_atom_cookie_t cookie = xcb_intern_atom(connection, false, name.length(), name.data()); auto reply = Reply{ xcb_intern_atom_reply(connection, cookie, nullptr) }; if (!reply) { fprintf(stderr, "[Gamescope WSI] Failed to get xcb atom.\n"); return std::nullopt; } xcb_atom_t atom = reply->atom; return atom; } template static std::optional getPropertyValue(xcb_connection_t* connection, xcb_atom_t atom) { static_assert(sizeof(T) % 4 == 0); xcb_screen_t* screen = xcb_setup_roots_iterator(xcb_get_setup(connection)).data; xcb_get_property_cookie_t cookie = xcb_get_property(connection, false, screen->root, atom, XCB_ATOM_CARDINAL, 0, sizeof(T) / sizeof(uint32_t)); auto reply = Reply{ xcb_get_property_reply(connection, cookie, nullptr) }; if (!reply) { fprintf(stderr, "[Gamescope WSI] Failed to read T root window property.\n"); return std::nullopt; } if (reply->type != XCB_ATOM_CARDINAL) { fprintf(stderr, "[Gamescope WSI] Atom of T was wrong type. Expected XCB_ATOM_CARDINAL.\n"); return std::nullopt; } T value = *reinterpret_cast(xcb_get_property_value(reply.get())); return value; } template static std::optional getPropertyValue(xcb_connection_t* connection, std::string_view name) { std::optional atom = getAtom(connection, name); if (!atom) return std::nullopt; return getPropertyValue(connection, *atom); } static std::optional getToplevelWindow(xcb_connection_t* connection, xcb_window_t window) { for (;;) { xcb_query_tree_cookie_t cookie = xcb_query_tree(connection, window); auto reply = Reply{ xcb_query_tree_reply(connection, cookie, nullptr) }; if (!reply) { fprintf(stderr, "[Gamescope WSI] getToplevelWindow: xcb_query_tree failed for window 0x%x.\n", window); return std::nullopt; } if (reply->root == reply->parent) return window; window = reply->parent; } } static std::optional getWindowRect(xcb_connection_t* connection, xcb_window_t window) { xcb_get_geometry_cookie_t cookie = xcb_get_geometry(connection, window); auto reply = Reply{ xcb_get_geometry_reply(connection, cookie, nullptr) }; if (!reply) { fprintf(stderr, "[Gamescope WSI] getWindowRect: xcb_get_geometry failed for window 0x%x.\n", window); return std::nullopt; } VkRect2D rect = { .offset = { reply->x, reply->y }, .extent = { reply->width, reply->height }, }; return rect; } static VkRect2D clip(VkRect2D parent, VkRect2D child) { return VkRect2D { .offset = child.offset, .extent = VkExtent2D { .width = std::min(child.extent.width, std::max(parent.extent.width - child.offset.x, 0)), .height = std::min(child.extent.height, std::max(parent.extent.height - child.offset.y, 0)), }, }; } static VkExtent2D max(VkExtent2D a, VkExtent2D b) { return VkExtent2D { .width = std::max(a.width, b.width), .height = std::max(a.height, b.height), }; } static std::optional getLargestObscuringChildWindowSize(xcb_connection_t* connection, xcb_window_t window) { VkExtent2D largestExtent = {}; xcb_query_tree_cookie_t cookie = xcb_query_tree(connection, window); auto reply = Reply{ xcb_query_tree_reply(connection, cookie, nullptr) }; if (!reply) { fprintf(stderr, "[Gamescope WSI] getLargestObscuringWindowSize: xcb_query_tree failed for window 0x%x.\n", window); return std::nullopt; } auto ourRect = getWindowRect(connection, window); if (!ourRect) { fprintf(stderr, "[Gamescope WSI] getLargestObscuringWindowSize: getWindowRect failed for main window 0x%x.\n", window); return std::nullopt; } xcb_window_t* children = xcb_query_tree_children(reply.get()); for (uint32_t i = 0; i < reply->children_len; i++) { xcb_window_t child = children[i]; xcb_get_window_attributes_cookie_t attributeCookie = xcb_get_window_attributes(connection, child); auto attributeReply = Reply{ xcb_get_window_attributes_reply(connection, attributeCookie, nullptr) }; const bool obscuring = attributeReply && attributeReply->map_state == XCB_MAP_STATE_VIEWABLE && !attributeReply->override_redirect; if (obscuring) { if (auto childRect = getWindowRect(connection, child)) { VkRect2D clippedRect = clip(*ourRect, *childRect); largestExtent = max(largestExtent, clippedRect.extent); } } } return largestExtent; } } inline int32_t iabs(int32_t a) { if (a < 0) return -a; return a; } ValveSoftware-gamescope-eb620ab/looks/000077500000000000000000000000001502457270500200645ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/looks/grayscale_g22.cube000066400000000000000000000065371502457270500233630ustar00rootroot00000000000000TITLE "Generated by Foundry::LUT" LUT_3D_SIZE 5 0.000000 0.000000 0.000000 0.053125 0.053125 0.053125 0.106250 0.106250 0.106250 0.159375 0.159375 0.159375 0.212500 0.212500 0.212500 0.178850 0.178850 0.178850 0.231975 0.231975 0.231975 0.285100 0.285100 0.285100 0.338225 0.338225 0.338225 0.391350 0.391350 0.391350 0.357700 0.357700 0.357700 0.410825 0.410825 0.410825 0.463950 0.463950 0.463950 0.517075 0.517075 0.517075 0.570200 0.570200 0.570200 0.536550 0.536550 0.536550 0.589675 0.589675 0.589675 0.642800 0.642800 0.642800 0.695925 0.695925 0.695925 0.749050 0.749050 0.749050 0.715400 0.715400 0.715400 0.768525 0.768525 0.768525 0.821650 0.821650 0.821650 0.874775 0.874775 0.874775 0.927900 0.927900 0.927900 0.018025 0.018025 0.018025 0.071150 0.071150 0.071150 0.124275 0.124275 0.124275 0.177400 0.177400 0.177400 0.230525 0.230525 0.230525 0.196875 0.196875 0.196875 0.250000 0.250000 0.250000 0.303125 0.303125 0.303125 0.356250 0.356250 0.356250 0.409375 0.409375 0.409375 0.375725 0.375725 0.375725 0.428850 0.428850 0.428850 0.481975 0.481975 0.481975 0.535100 0.535100 0.535100 0.588225 0.588225 0.588225 0.554575 0.554575 0.554575 0.607700 0.607700 0.607700 0.660825 0.660825 0.660825 0.713950 0.713950 0.713950 0.767075 0.767075 0.767075 0.733425 0.733425 0.733425 0.786550 0.786550 0.786550 0.839675 0.839675 0.839675 0.892800 0.892800 0.892800 0.945925 0.945925 0.945925 0.036050 0.036050 0.036050 0.089175 0.089175 0.089175 0.142300 0.142300 0.142300 0.195425 0.195425 0.195425 0.248550 0.248550 0.248550 0.214900 0.214900 0.214900 0.268025 0.268025 0.268025 0.321150 0.321150 0.321150 0.374275 0.374275 0.374275 0.427400 0.427400 0.427400 0.393750 0.393750 0.393750 0.446875 0.446875 0.446875 0.500000 0.500000 0.500000 0.553125 0.553125 0.553125 0.606250 0.606250 0.606250 0.572600 0.572600 0.572600 0.625725 0.625725 0.625725 0.678850 0.678850 0.678850 0.731975 0.731975 0.731975 0.785100 0.785100 0.785100 0.751450 0.751450 0.751450 0.804575 0.804575 0.804575 0.857700 0.857700 0.857700 0.910825 0.910825 0.910825 0.963950 0.963950 0.963950 0.054075 0.054075 0.054075 0.107200 0.107200 0.107200 0.160325 0.160325 0.160325 0.213450 0.213450 0.213450 0.266575 0.266575 0.266575 0.232925 0.232925 0.232925 0.286050 0.286050 0.286050 0.339175 0.339175 0.339175 0.392300 0.392300 0.392300 0.445425 0.445425 0.445425 0.411775 0.411775 0.411775 0.464900 0.464900 0.464900 0.518025 0.518025 0.518025 0.571150 0.571150 0.571150 0.624275 0.624275 0.624275 0.590625 0.590625 0.590625 0.643750 0.643750 0.643750 0.696875 0.696875 0.696875 0.750000 0.750000 0.750000 0.803125 0.803125 0.803125 0.769475 0.769475 0.769475 0.822600 0.822600 0.822600 0.875725 0.875725 0.875725 0.928850 0.928850 0.928850 0.981975 0.981975 0.981975 0.072100 0.072100 0.072100 0.125225 0.125225 0.125225 0.178350 0.178350 0.178350 0.231475 0.231475 0.231475 0.284600 0.284600 0.284600 0.250950 0.250950 0.250950 0.304075 0.304075 0.304075 0.357200 0.357200 0.357200 0.410325 0.410325 0.410325 0.463450 0.463450 0.463450 0.429800 0.429800 0.429800 0.482925 0.482925 0.482925 0.536050 0.536050 0.536050 0.589175 0.589175 0.589175 0.642300 0.642300 0.642300 0.608650 0.608650 0.608650 0.661775 0.661775 0.661775 0.714900 0.714900 0.714900 0.768025 0.768025 0.768025 0.821150 0.821150 0.821150 0.787500 0.787500 0.787500 0.840625 0.840625 0.840625 0.893750 0.893750 0.893750 0.946875 0.946875 0.946875 1.000000 1.000000 1.000000 ValveSoftware-gamescope-eb620ab/looks/grayscale_pq.cube000066400000000000000000000065371502457270500234110ustar00rootroot00000000000000TITLE "Generated by Foundry::LUT" LUT_3D_SIZE 5 0.000000 0.000000 0.000000 0.053125 0.053125 0.053125 0.106250 0.106250 0.106250 0.159375 0.159375 0.159375 0.212500 0.212500 0.212500 0.178850 0.178850 0.178850 0.231975 0.231975 0.231975 0.285100 0.285100 0.285100 0.338225 0.338225 0.338225 0.391350 0.391350 0.391350 0.357700 0.357700 0.357700 0.410825 0.410825 0.410825 0.463950 0.463950 0.463950 0.517075 0.517075 0.517075 0.570200 0.570200 0.570200 0.536550 0.536550 0.536550 0.589675 0.589675 0.589675 0.642800 0.642800 0.642800 0.695925 0.695925 0.695925 0.749050 0.749050 0.749050 0.715400 0.715400 0.715400 0.768525 0.768525 0.768525 0.821650 0.821650 0.821650 0.874775 0.874775 0.874775 0.927900 0.927900 0.927900 0.018025 0.018025 0.018025 0.071150 0.071150 0.071150 0.124275 0.124275 0.124275 0.177400 0.177400 0.177400 0.230525 0.230525 0.230525 0.196875 0.196875 0.196875 0.250000 0.250000 0.250000 0.303125 0.303125 0.303125 0.356250 0.356250 0.356250 0.409375 0.409375 0.409375 0.375725 0.375725 0.375725 0.428850 0.428850 0.428850 0.481975 0.481975 0.481975 0.535100 0.535100 0.535100 0.588225 0.588225 0.588225 0.554575 0.554575 0.554575 0.607700 0.607700 0.607700 0.660825 0.660825 0.660825 0.713950 0.713950 0.713950 0.767075 0.767075 0.767075 0.733425 0.733425 0.733425 0.786550 0.786550 0.786550 0.839675 0.839675 0.839675 0.892800 0.892800 0.892800 0.945925 0.945925 0.945925 0.036050 0.036050 0.036050 0.089175 0.089175 0.089175 0.142300 0.142300 0.142300 0.195425 0.195425 0.195425 0.248550 0.248550 0.248550 0.214900 0.214900 0.214900 0.268025 0.268025 0.268025 0.321150 0.321150 0.321150 0.374275 0.374275 0.374275 0.427400 0.427400 0.427400 0.393750 0.393750 0.393750 0.446875 0.446875 0.446875 0.500000 0.500000 0.500000 0.553125 0.553125 0.553125 0.606250 0.606250 0.606250 0.572600 0.572600 0.572600 0.625725 0.625725 0.625725 0.678850 0.678850 0.678850 0.731975 0.731975 0.731975 0.785100 0.785100 0.785100 0.751450 0.751450 0.751450 0.804575 0.804575 0.804575 0.857700 0.857700 0.857700 0.910825 0.910825 0.910825 0.963950 0.963950 0.963950 0.054075 0.054075 0.054075 0.107200 0.107200 0.107200 0.160325 0.160325 0.160325 0.213450 0.213450 0.213450 0.266575 0.266575 0.266575 0.232925 0.232925 0.232925 0.286050 0.286050 0.286050 0.339175 0.339175 0.339175 0.392300 0.392300 0.392300 0.445425 0.445425 0.445425 0.411775 0.411775 0.411775 0.464900 0.464900 0.464900 0.518025 0.518025 0.518025 0.571150 0.571150 0.571150 0.624275 0.624275 0.624275 0.590625 0.590625 0.590625 0.643750 0.643750 0.643750 0.696875 0.696875 0.696875 0.750000 0.750000 0.750000 0.803125 0.803125 0.803125 0.769475 0.769475 0.769475 0.822600 0.822600 0.822600 0.875725 0.875725 0.875725 0.928850 0.928850 0.928850 0.981975 0.981975 0.981975 0.072100 0.072100 0.072100 0.125225 0.125225 0.125225 0.178350 0.178350 0.178350 0.231475 0.231475 0.231475 0.284600 0.284600 0.284600 0.250950 0.250950 0.250950 0.304075 0.304075 0.304075 0.357200 0.357200 0.357200 0.410325 0.410325 0.410325 0.463450 0.463450 0.463450 0.429800 0.429800 0.429800 0.482925 0.482925 0.482925 0.536050 0.536050 0.536050 0.589175 0.589175 0.589175 0.642300 0.642300 0.642300 0.608650 0.608650 0.608650 0.661775 0.661775 0.661775 0.714900 0.714900 0.714900 0.768025 0.768025 0.768025 0.821150 0.821150 0.821150 0.787500 0.787500 0.787500 0.840625 0.840625 0.840625 0.893750 0.893750 0.893750 0.946875 0.946875 0.946875 1.000000 1.000000 1.000000 ValveSoftware-gamescope-eb620ab/looks/invertbrightness_g22.cube000066400000000000000000001062221502457270500250010ustar00rootroot00000000000000TITLE "Generated by Foundry::LUT" LUT_3D_SIZE 11 1.000000 1.000000 1.000000 1.000000 0.949835 0.950590 1.000000 0.850301 0.848960 1.000000 0.766414 0.719730 1.000000 0.692346 0.607205 1.000000 0.614679 0.501135 1.000000 0.532794 0.401341 1.000000 0.445571 0.307895 1.000000 0.349750 0.218907 1.000000 0.235010 0.125806 1.000000 0.008906 0.002244 0.843722 0.946433 0.846231 0.905732 0.908133 0.807066 0.998540 0.840339 0.737290 1.000000 0.769566 0.653021 1.000000 0.695678 0.561736 1.000000 0.618181 0.468871 1.000000 0.536895 0.377937 1.000000 0.450727 0.290648 1.000000 0.356667 0.203893 1.000000 0.245828 0.110406 0.981848 0.058543 0.000000 0.684261 0.884835 0.649084 0.748848 0.861447 0.632834 0.837403 0.817661 0.598096 0.914540 0.758989 0.545280 0.966939 0.691531 0.479698 0.995996 0.618354 0.406902 1.000000 0.540559 0.331111 1.000000 0.457813 0.252367 0.988061 0.368041 0.168860 0.965405 0.264782 0.070795 0.936369 0.116640 0.000000 0.559935 0.813468 0.488629 0.602063 0.799281 0.479210 0.672112 0.769688 0.458339 0.747475 0.725304 0.424506 0.810493 0.669703 0.378661 0.855421 0.605920 0.322801 0.882580 0.535824 0.258812 0.894364 0.460057 0.187236 0.893534 0.377731 0.103720 0.882573 0.284863 0.000000 0.863201 0.165225 0.000000 0.417140 0.734827 0.343855 0.449702 0.725491 0.337227 0.508238 0.705080 0.322222 0.578241 0.672500 0.296959 0.644132 0.629128 0.261136 0.697917 0.576833 0.214900 0.737087 0.517230 0.157620 0.761963 0.451281 0.083133 0.774002 0.378854 0.000000 0.774776 0.297632 0.000000 0.766125 0.199022 0.000000 0.252894 0.652063 0.191026 0.284777 0.645611 0.185471 0.341483 0.631184 0.172655 0.410057 0.607355 0.150165 0.477215 0.574370 0.115523 0.535716 0.533084 0.061228 0.582460 0.484474 0.000000 0.616636 0.429246 0.000000 0.638591 0.367476 0.000000 0.649759 0.297875 0.000000 0.651734 0.215708 0.000000 0.000000 0.567735 0.000000 0.000000 0.563139 0.000000 0.133600 0.552734 0.000000 0.227915 0.535192 0.000000 0.305942 0.510258 0.000000 0.371161 0.478131 0.000000 0.424054 0.439226 0.000000 0.465375 0.393896 0.000000 0.495947 0.342173 0.000000 0.516399 0.283375 0.000000 0.527469 0.214894 0.000000 0.000000 0.481966 0.000000 0.000000 0.478586 0.000000 0.000000 0.470878 0.000000 0.000000 0.457710 0.000000 0.083274 0.438657 0.000000 0.194482 0.413582 0.000000 0.264138 0.382546 0.000000 0.315942 0.345663 0.000000 0.354916 0.302930 0.000000 0.382797 0.253640 0.000000 0.399456 0.195016 0.000000 0.000000 0.392619 0.000000 0.000000 0.390063 0.000000 0.000000 0.384206 0.000000 0.000000 0.374124 0.000000 0.000000 0.359373 0.000000 0.000000 0.339601 0.000000 0.101007 0.314310 0.000000 0.176406 0.283326 0.000000 0.224307 0.246597 0.000000 0.257581 0.204038 0.000000 0.280310 0.154974 0.000000 0.000000 0.293774 0.000000 0.000000 0.291632 0.000000 0.000000 0.286720 0.000000 0.000000 0.278250 0.000000 0.000000 0.265828 0.000000 0.000000 0.249223 0.000000 0.000000 0.228348 0.000000 0.094081 0.203241 0.000000 0.144337 0.174013 0.000000 0.178894 0.140704 0.000000 0.204367 0.102817 0.000000 0.000000 0.206917 0.000000 0.000000 0.205324 0.000000 0.000000 0.201692 0.000000 0.000000 0.195484 0.000000 0.000000 0.186503 0.000000 0.000000 0.174739 0.000000 0.000000 0.160330 0.000000 0.000000 0.143524 0.000000 0.060695 0.123071 0.000000 0.109658 0.096847 0.000000 0.145058 0.060662 0.000000 0.980689 0.983380 1.000000 1.000000 0.933026 1.000000 1.000000 0.840919 0.935656 1.000000 0.763033 0.810629 1.000000 0.690589 0.698784 1.000000 0.613604 0.591008 1.000000 0.531976 0.487334 1.000000 0.444716 0.387665 1.000000 0.348617 0.291522 1.000000 0.233284 0.197516 1.000000 0.001344 0.100278 0.829162 0.933301 0.933650 0.895428 0.895428 0.895428 0.979347 0.833875 0.828014 1.000000 0.766214 0.744507 1.000000 0.693705 0.652217 1.000000 0.616896 0.556653 1.000000 0.535878 0.461022 1.000000 0.449687 0.366796 1.000000 0.355412 0.274317 1.000000 0.244068 0.182428 0.979914 0.052228 0.084346 0.674513 0.876890 0.739958 0.734822 0.854927 0.723311 0.820551 0.813011 0.687725 0.897954 0.755861 0.633312 0.952481 0.689390 0.564996 0.984387 0.616764 0.487927 0.997690 0.539201 0.406031 0.996436 0.456492 0.321785 0.983927 0.366608 0.236175 0.962759 0.262986 0.147684 0.934798 0.112960 0.041479 0.546252 0.809113 0.568335 0.588381 0.795290 0.558206 0.658952 0.766309 0.535718 0.735674 0.722574 0.499167 0.800580 0.667502 0.449923 0.847543 0.604086 0.390500 0.876620 0.534209 0.323512 0.890074 0.458533 0.250869 0.890616 0.376173 0.172754 0.880726 0.283078 0.083030 0.862143 0.162523 0.000000 0.407779 0.731915 0.405978 0.440825 0.722693 0.399053 0.500193 0.702499 0.383428 0.571243 0.670196 0.357280 0.638289 0.627095 0.320578 0.693256 0.575024 0.274062 0.733554 0.515579 0.218559 0.759442 0.449715 0.153679 0.772335 0.377286 0.072122 0.773788 0.295930 0.000000 0.765316 0.196977 0.000000 0.246946 0.649961 0.246907 0.279485 0.643548 0.241581 0.337084 0.629202 0.229410 0.406499 0.605487 0.208522 0.474396 0.572629 0.177900 0.533568 0.531460 0.136081 0.580924 0.482943 0.077693 0.615640 0.427767 0.000000 0.638049 0.366000 0.000000 0.649162 0.296408 0.000000 0.650973 0.214148 0.000000 0.000000 0.566152 0.052306 0.000000 0.561570 0.043985 0.132684 0.551194 0.023561 0.227421 0.533696 0.000000 0.305679 0.508815 0.000000 0.371094 0.476739 0.000000 0.424167 0.437878 0.000000 0.465237 0.392617 0.000000 0.495504 0.340962 0.000000 0.515739 0.282204 0.000000 0.526650 0.213697 0.000000 0.000000 0.480745 0.000000 0.000000 0.477373 0.000000 0.000000 0.469681 0.000000 0.000000 0.456541 0.000000 0.085080 0.437525 0.000000 0.194692 0.412496 0.000000 0.263847 0.381511 0.000000 0.315360 0.344678 0.000000 0.354133 0.301993 0.000000 0.381849 0.252724 0.000000 0.398361 0.194089 0.000000 0.000000 0.391769 0.000000 0.000000 0.389218 0.000000 0.000000 0.383367 0.000000 0.000000 0.373298 0.000000 0.000000 0.358565 0.000000 0.000000 0.338805 0.000000 0.098344 0.313531 0.000000 0.174546 0.282571 0.000000 0.222624 0.245877 0.000000 0.255949 0.203360 0.000000 0.278690 0.154343 0.000000 0.000000 0.293141 0.000000 0.000000 0.291002 0.000000 0.000000 0.286098 0.000000 0.000000 0.277641 0.000000 0.000000 0.265238 0.000000 0.000000 0.248660 0.000000 0.000000 0.227819 0.000000 0.089680 0.202756 0.000000 0.141282 0.173582 0.000000 0.176358 0.140343 0.000000 0.202113 0.102543 0.000000 0.000000 0.206497 0.000000 0.000000 0.204909 0.000000 0.000000 0.201288 0.000000 0.000000 0.195097 0.000000 0.000000 0.186144 0.000000 0.000000 0.174415 0.000000 0.000000 0.160050 0.000000 0.000000 0.143289 0.000000 0.054607 0.122847 0.000000 0.105992 0.096629 0.000000 0.142277 0.060438 0.000000 0.971160 0.941159 1.000000 1.000000 0.893025 1.000000 1.000000 0.821309 1.000000 1.000000 0.753772 0.931559 1.000000 0.684579 0.818308 1.000000 0.609210 0.705904 1.000000 0.528415 0.596174 1.000000 0.441469 0.490095 1.000000 0.345218 0.388206 1.000000 0.228846 0.290682 0.995614 0.000000 0.197062 0.833504 0.900898 1.000000 0.885982 0.869928 1.000000 0.953747 0.818808 0.946729 1.000000 0.756920 0.863302 1.000000 0.687361 0.768184 1.000000 0.612217 0.668027 1.000000 0.532088 0.566799 1.000000 0.446266 0.466791 1.000000 0.351902 0.369319 1.000000 0.239672 0.274992 0.975494 0.035494 0.183450 0.673603 0.859143 0.855201 0.724272 0.839690 0.838047 0.801315 0.801315 0.801315 0.875719 0.747301 0.744830 0.931374 0.683054 0.673382 0.966252 0.611857 0.592263 0.983053 0.535140 0.505805 0.985191 0.452836 0.417118 0.975692 0.362950 0.328241 0.957047 0.258686 0.240216 0.931086 0.104222 0.152427 0.535232 0.798523 0.672247 0.575121 0.785455 0.661602 0.643299 0.757786 0.637987 0.719264 0.715528 0.599624 0.785162 0.661746 0.547951 0.834169 0.599304 0.485671 0.865711 0.530082 0.415786 0.881646 0.454765 0.340952 0.884459 0.372459 0.263100 0.876510 0.278951 0.182895 0.859477 0.156320 0.097286 0.395697 0.724978 0.497555 0.428716 0.716009 0.490331 0.488230 0.696306 0.474061 0.559870 0.664644 0.446923 0.628031 0.622194 0.409042 0.684498 0.570686 0.361493 0.726481 0.511665 0.305832 0.754056 0.446059 0.243548 0.768502 0.373684 0.175232 0.771285 0.292067 0.097964 0.763798 0.192068 0.000000 0.236925 0.645074 0.331253 0.270385 0.638753 0.325970 0.329200 0.624594 0.313966 0.399762 0.601148 0.293608 0.468737 0.568593 0.264453 0.528983 0.527714 0.226530 0.577408 0.479428 0.179829 0.613146 0.424398 0.123009 0.636477 0.362655 0.046021 0.648204 0.292935 0.000000 0.649670 0.210389 0.000000 0.000000 0.562535 0.164373 0.000000 0.557984 0.159820 0.129879 0.547677 0.149299 0.225740 0.530283 0.130789 0.304543 0.505526 0.102255 0.370404 0.473576 0.058401 0.423905 0.434820 0.000000 0.465281 0.389637 0.000000 0.495030 0.338105 0.000000 0.514694 0.279421 0.000000 0.525184 0.210829 0.000000 0.000000 0.477954 0.000000 0.000000 0.474592 0.000000 0.000000 0.466924 0.000000 0.000000 0.453828 0.000000 0.091954 0.434881 0.000000 0.196486 0.409955 0.000000 0.264081 0.379082 0.000000 0.314716 0.342362 0.000000 0.352903 0.299778 0.000000 0.380140 0.250538 0.000000 0.396254 0.191865 0.000000 0.000000 0.389780 0.000000 0.000000 0.387233 0.000000 0.000000 0.381402 0.000000 0.000000 0.371360 0.000000 0.000000 0.356669 0.000000 0.000000 0.336933 0.000000 0.093800 0.311698 0.000000 0.171156 0.280784 0.000000 0.219404 0.244166 0.000000 0.252719 0.201741 0.000000 0.275407 0.152816 0.000000 0.000000 0.291653 0.000000 0.000000 0.289523 0.000000 0.000000 0.284634 0.000000 0.000000 0.276207 0.000000 0.000000 0.263846 0.000000 0.000000 0.247327 0.000000 0.000000 0.226562 0.000000 0.080200 0.201596 0.000000 0.134842 0.172542 0.000000 0.171009 0.139452 0.000000 0.197340 0.101843 0.000000 0.000000 0.205501 0.000000 0.000000 0.203924 0.000000 0.000000 0.200325 0.000000 0.000000 0.194175 0.000000 0.000000 0.185283 0.000000 0.000000 0.173632 0.000000 0.000000 0.159365 0.000000 0.000000 0.142705 0.000000 0.040053 0.122277 0.000000 0.097896 0.096069 0.000000 0.136226 0.059825 0.000000 1.000000 0.871644 1.000000 1.000000 0.840564 1.000000 1.000000 0.792214 1.000000 1.000000 0.736193 1.000000 1.000000 0.671773 0.925425 1.000000 0.599390 0.813747 1.000000 0.520419 0.702570 1.000000 0.434437 0.593722 1.000000 0.338295 0.488386 1.000000 0.220350 0.387402 0.987581 0.000000 0.291353 0.854151 0.856356 1.000000 0.885881 0.834523 1.000000 0.935153 0.794131 1.000000 0.982897 0.739427 0.968765 1.000000 0.674488 0.875417 1.000000 0.602303 0.775025 1.000000 0.524000 0.672034 1.000000 0.439164 0.569243 1.000000 0.344973 0.468484 0.997302 0.231428 0.371009 0.967930 0.004628 0.277630 0.687606 0.831478 0.960198 0.725766 0.814935 0.943387 0.788648 0.781099 0.907205 0.854849 0.731579 0.851064 0.908273 0.670861 0.779283 0.944371 0.602159 0.696938 0.963976 0.527082 0.608417 0.969469 0.445718 0.517077 0.963339 0.356073 0.425363 0.947804 0.250889 0.335026 0.924529 0.087705 0.247100 0.534394 0.780515 0.776666 0.569365 0.768507 0.765854 0.631157 0.742760 0.741853 0.702790 0.702790 0.702790 0.767342 0.651128 0.650009 0.817179 0.590392 0.586172 0.850763 0.522406 0.514347 0.869266 0.447854 0.437416 0.874746 0.365798 0.357802 0.869299 0.271712 0.277314 0.854449 0.145386 0.196825 0.387080 0.712893 0.596602 0.418739 0.704319 0.589179 0.476424 0.685399 0.572468 0.546881 0.654790 0.544609 0.615000 0.613450 0.505750 0.672416 0.562939 0.457053 0.715992 0.504710 0.400278 0.745480 0.439632 0.337383 0.761892 0.367436 0.270107 0.766521 0.285449 0.199539 0.760644 0.183576 0.125451 0.225668 0.636592 0.424841 0.259738 0.630423 0.419477 0.319294 0.616581 0.407313 0.390577 0.593597 0.386757 0.460396 0.561572 0.357519 0.521702 0.521211 0.319954 0.571368 0.473360 0.274818 0.608439 0.418621 0.222942 0.633090 0.356962 0.164726 0.646029 0.286988 0.098606 0.648169 0.203513 0.013755 0.000000 0.556326 0.260836 0.000000 0.551832 0.256697 0.123395 0.541645 0.247247 0.221657 0.524435 0.231069 0.301460 0.499902 0.207546 0.368080 0.468180 0.176285 0.422304 0.429623 0.136653 0.464394 0.384570 0.086237 0.494712 0.333067 0.014069 0.513839 0.274406 0.000000 0.523513 0.205590 0.000000 0.000000 0.473207 0.084896 0.000000 0.469864 0.080381 0.000000 0.462235 0.069627 0.000000 0.449201 0.049211 0.100896 0.430335 0.015616 0.200782 0.405503 0.000000 0.266287 0.374767 0.000000 0.315065 0.338228 0.000000 0.351993 0.295804 0.000000 0.378227 0.246584 0.000000 0.393534 0.187796 0.000000 0.000000 0.386245 0.000000 0.000000 0.383711 0.000000 0.000000 0.377907 0.000000 0.000000 0.367915 0.000000 0.000000 0.353290 0.000000 0.000000 0.333596 0.000000 0.089793 0.308404 0.000000 0.167399 0.277580 0.000000 0.215366 0.241076 0.000000 0.248368 0.198790 0.000000 0.270779 0.149985 0.000000 0.000000 0.288997 0.000000 0.000000 0.286875 0.000000 0.000000 0.282017 0.000000 0.000000 0.273636 0.000000 0.000000 0.261348 0.000000 0.000000 0.244924 0.000000 0.000000 0.224285 0.000000 0.065280 0.199475 0.000000 0.125154 0.170618 0.000000 0.162956 0.137760 0.000000 0.190103 0.100433 0.000000 0.000000 0.203696 0.000000 0.000000 0.202137 0.000000 0.000000 0.198575 0.000000 0.000000 0.192492 0.000000 0.000000 0.183698 0.000000 0.000000 0.172178 0.000000 0.000000 0.158081 0.000000 0.000000 0.141564 0.000000 0.016026 0.121148 0.000000 0.084632 0.094919 0.000000 0.126650 0.058527 0.000000 0.987090 0.809287 1.000000 0.991970 0.791429 1.000000 1.000000 0.757965 1.000000 1.000000 0.711171 1.000000 1.000000 0.652129 1.000000 1.000000 0.583669 0.910579 1.000000 0.507331 0.800669 1.000000 0.422876 0.691403 1.000000 0.327036 0.584479 1.000000 0.206641 0.481130 0.976041 0.000000 0.382288 0.864766 0.810277 1.000000 0.884634 0.794423 1.000000 0.919204 0.762243 1.000000 0.956986 0.714459 1.000000 0.987827 0.654966 0.970394 1.000000 0.586664 0.872329 1.000000 0.510971 0.769963 1.000000 0.427674 0.666470 1.000000 0.333864 0.564029 0.982898 0.218324 0.464214 0.956951 0.000000 0.368160 0.704551 0.796561 1.000000 0.731894 0.782680 1.000000 0.780344 0.753395 1.000000 0.835968 0.708906 0.946456 0.884654 0.652533 0.876125 0.920200 0.587156 0.794427 0.941594 0.514423 0.705639 0.949992 0.434506 0.613187 0.947177 0.345328 0.519688 0.934967 0.238812 0.427132 0.914756 0.057783 0.336876 0.541392 0.755660 0.873147 0.570279 0.744832 0.862433 0.623334 0.721314 0.838603 0.687851 0.684151 0.799687 0.748755 0.635246 0.746848 0.797891 0.576852 0.682578 0.832666 0.510654 0.609854 0.853388 0.437284 0.531566 0.861542 0.355690 0.450254 0.858831 0.260810 0.367958 0.846546 0.128301 0.286077 0.383916 0.695487 0.692483 0.412913 0.687411 0.684977 0.466712 0.669495 0.668071 0.533936 0.640277 0.639861 0.600453 0.600453 0.600453 0.657827 0.551360 0.550975 0.702485 0.494302 0.493191 0.733736 0.430048 0.429138 0.752220 0.358183 0.360755 0.758973 0.275707 0.289659 0.755103 0.170912 0.217170 0.215118 0.624211 0.517685 0.249130 0.618249 0.512231 0.308481 0.604844 0.499864 0.379629 0.582505 0.478979 0.449687 0.551241 0.449299 0.511714 0.511640 0.411237 0.562517 0.464444 0.365680 0.601006 0.410169 0.313786 0.627208 0.348677 0.256769 0.641697 0.278363 0.195691 0.645268 0.193291 0.131075 0.000000 0.547278 0.351902 0.000000 0.542865 0.347788 0.110784 0.532854 0.338416 0.213845 0.515914 0.322444 0.295322 0.491712 0.299429 0.363059 0.460336 0.269347 0.418308 0.422087 0.232478 0.461425 0.377245 0.189213 0.492761 0.325786 0.139680 0.512801 0.266871 0.082488 0.522147 0.197399 0.012107 0.000000 0.466336 0.191631 0.000000 0.463020 0.188255 0.000000 0.455452 0.180516 0.000000 0.442515 0.167156 0.104559 0.423771 0.147468 0.202930 0.399069 0.120729 0.268236 0.368448 0.085526 0.316727 0.332006 0.036315 0.352269 0.289731 0.000000 0.376822 0.240483 0.000000 0.390815 0.181437 0.000000 0.000000 0.380919 0.000000 0.000000 0.378397 0.000000 0.000000 0.372625 0.000000 0.000000 0.362693 0.000000 0.000000 0.348151 0.000000 0.000000 0.328492 0.000000 0.089753 0.303383 0.000000 0.164977 0.272671 0.000000 0.211694 0.236320 0.000000 0.243805 0.194199 0.000000 0.265552 0.145503 0.000000 0.000000 0.284947 0.000000 0.000000 0.282847 0.000000 0.000000 0.278024 0.000000 0.000000 0.269712 0.000000 0.000000 0.257522 0.000000 0.000000 0.241233 0.000000 0.000000 0.220768 0.000000 0.045253 0.196175 0.000000 0.113227 0.167577 0.000000 0.153026 0.135028 0.000000 0.181086 0.098035 0.000000 0.000000 0.200905 0.000000 0.000000 0.199368 0.000000 0.000000 0.195864 0.000000 0.000000 0.189875 0.000000 0.000000 0.181218 0.000000 0.000000 0.169883 0.000000 0.000000 0.156015 0.000000 0.000000 0.139654 0.000000 0.000000 0.119231 0.000000 0.065908 0.092903 0.000000 0.114002 0.056151 0.000000 0.958053 0.758245 1.000000 0.963655 0.746067 1.000000 0.975925 0.720372 1.000000 0.992314 0.679781 1.000000 1.000000 0.626220 1.000000 1.000000 0.562123 0.996410 1.000000 0.488927 0.889623 1.000000 0.406370 0.781666 1.000000 0.310808 0.674676 0.985861 0.186315 0.570201 0.960873 0.000000 0.469368 0.864192 0.764036 1.000000 0.877165 0.751607 1.000000 0.901277 0.724859 1.000000 0.930692 0.683277 1.000000 0.957552 0.629311 1.000000 0.977037 0.565349 0.959415 0.987277 0.492778 0.859365 0.988138 0.411397 0.756751 0.980295 0.317996 0.654021 0.964743 0.199182 0.552975 0.942403 0.000000 0.454892 0.715993 0.756677 1.000000 0.735493 0.744985 1.000000 0.772088 0.719734 1.000000 0.817419 0.680142 1.000000 0.860296 0.628396 0.962959 0.894063 0.566827 0.883258 0.916331 0.496932 0.795570 0.927096 0.418836 0.703297 0.927377 0.330219 0.609132 0.918510 0.221590 0.515171 0.901547 0.000635 0.422840 0.550124 0.725060 0.960023 0.573265 0.715386 0.949600 0.617360 0.694129 0.926360 0.673636 0.659970 0.888239 0.729444 0.614181 0.836168 0.776637 0.558571 0.772396 0.811752 0.494590 0.699721 0.834220 0.422737 0.620950 0.844875 0.341738 0.538613 0.844944 0.245661 0.454761 0.835425 0.102275 0.370871 0.384049 0.673055 0.782076 0.409725 0.665527 0.774593 0.458414 0.648738 0.757724 0.520951 0.621130 0.729526 0.584571 0.583120 0.690033 0.640944 0.535781 0.640283 0.686081 0.480221 0.581963 0.718795 0.417050 0.517060 0.739282 0.345633 0.447490 0.748272 0.262465 0.374938 0.746660 0.153132 0.300907 0.204689 0.607894 0.606543 0.238114 0.602181 0.601034 0.296494 0.589306 0.588541 0.366765 0.567765 0.567430 0.436478 0.537457 0.537399 0.498831 0.498831 0.498831 0.550568 0.452498 0.452603 0.590440 0.398851 0.399899 0.618301 0.337603 0.342044 0.634566 0.266830 0.280380 0.639894 0.179406 0.216215 0.000000 0.535271 0.438763 0.000000 0.530964 0.434602 0.087313 0.521179 0.425122 0.200661 0.504590 0.408975 0.285001 0.480825 0.385732 0.354381 0.449913 0.355420 0.411001 0.412085 0.318437 0.455461 0.367545 0.275449 0.488145 0.316158 0.227274 0.509506 0.256888 0.174758 0.520073 0.186035 0.118517 0.000000 0.457241 0.279128 0.000000 0.453964 0.275854 0.000000 0.446480 0.268370 0.000000 0.433674 0.255545 0.098677 0.415100 0.236909 0.200217 0.390581 0.212277 0.266721 0.360123 0.181675 0.316097 0.323780 0.145186 0.352394 0.281476 0.102584 0.376305 0.231902 0.050956 0.388515 0.172354 0.000000 0.000000 0.373844 0.121611 0.000000 0.371332 0.118721 0.000000 0.365581 0.112063 0.000000 0.355683 0.100435 0.000000 0.341133 0.082845 0.000000 0.321453 0.057552 0.095768 0.296396 0.022116 0.165170 0.265825 0.000000 0.209324 0.229653 0.000000 0.239788 0.187708 0.000000 0.260369 0.139057 0.000000 0.000000 0.279343 0.000000 0.000000 0.277263 0.000000 0.000000 0.272491 0.000000 0.000000 0.264264 0.000000 0.000000 0.252201 0.000000 0.000000 0.236084 0.000000 0.000000 0.215836 0.000000 0.022189 0.191506 0.000000 0.100172 0.163229 0.000000 0.142083 0.131048 0.000000 0.171010 0.094423 0.000000 0.000000 0.196997 0.000000 0.000000 0.195491 0.000000 0.000000 0.192057 0.000000 0.000000 0.186189 0.000000 0.000000 0.177707 0.000000 0.000000 0.166607 0.000000 0.000000 0.153038 0.000000 0.000000 0.136799 0.000000 0.000000 0.116307 0.000000 0.040241 0.089772 0.000000 0.098810 0.052284 0.000000 0.929340 0.710855 1.000000 0.934068 0.701018 1.000000 0.943804 0.679084 1.000000 0.957443 0.643357 1.000000 0.971522 0.594827 1.000000 0.982468 0.535097 1.000000 0.987834 0.465223 0.969676 0.986460 0.384662 0.864346 0.978122 0.288991 0.758550 0.963153 0.157036 0.654100 0.942008 0.000000 0.552261 0.852554 0.716494 1.000000 0.861476 0.706167 1.000000 0.879050 0.683501 1.000000 0.902259 0.647105 1.000000 0.925463 0.598267 1.000000 0.944113 0.538683 1.000000 0.955661 0.469433 0.940286 0.959177 0.390094 0.839793 0.954723 0.296809 0.737958 0.942872 0.172175 0.636765 0.924194 0.000000 0.537563 0.718346 0.713352 1.000000 0.732557 0.703387 1.000000 0.760402 0.681494 1.000000 0.797102 0.646302 1.000000 0.834288 0.599048 1.000000 0.865716 0.541422 0.963426 0.888225 0.474602 0.877900 0.900890 0.398498 0.786881 0.904008 0.310297 0.693076 0.898409 0.198029 0.598632 0.884758 0.000000 0.504974 0.555345 0.689844 1.000000 0.573783 0.681217 1.000000 0.610033 0.662080 1.000000 0.658367 0.630864 0.968222 0.708626 0.588288 0.917561 0.753169 0.535677 0.855044 0.787994 0.474171 0.783241 0.811781 0.404032 0.704819 0.824717 0.323608 0.622240 0.827510 0.225539 0.537485 0.820850 0.059105 0.452029 0.383694 0.646115 0.864556 0.406085 0.639144 0.857187 0.449466 0.623519 0.840555 0.506773 0.597614 0.812694 0.566795 0.561587 0.773548 0.621515 0.516223 0.724035 0.666645 0.462391 0.665724 0.700523 0.400481 0.600495 0.722887 0.329539 0.530165 0.734135 0.245285 0.456387 0.734928 0.128501 0.380652 0.191570 0.587793 0.690306 0.224458 0.582356 0.684793 0.281821 0.570072 0.672282 0.351032 0.549431 0.651115 0.420157 0.520222 0.620953 0.482616 0.482737 0.582113 0.535143 0.437432 0.535416 0.576355 0.384543 0.481996 0.605935 0.323572 0.423146 0.624134 0.252134 0.360210 0.631463 0.161184 0.294533 0.000000 0.520296 0.521503 0.000000 0.516112 0.517294 0.036380 0.506597 0.507705 0.179501 0.490427 0.491362 0.269113 0.467190 0.467817 0.341052 0.436848 0.437082 0.399544 0.399544 0.399544 0.445708 0.355380 0.355882 0.480079 0.304085 0.306986 0.503109 0.244327 0.253872 0.515289 0.171487 0.197663 0.000000 0.445877 0.360180 0.000000 0.442646 0.356875 0.000000 0.435269 0.349329 0.000000 0.422635 0.336409 0.076818 0.404276 0.317661 0.190303 0.379996 0.292960 0.260065 0.349753 0.262453 0.311387 0.313542 0.226513 0.349159 0.271163 0.185653 0.374076 0.220937 0.139598 0.386573 0.160121 0.088679 0.000000 0.365039 0.205882 0.000000 0.362541 0.203202 0.000000 0.356820 0.197065 0.000000 0.346954 0.186494 0.000000 0.332357 0.170902 0.000000 0.312622 0.149893 0.098516 0.287511 0.123201 0.167008 0.256925 0.090534 0.208762 0.220872 0.050824 0.236770 0.179087 0.000000 0.255649 0.130340 0.000000 0.000000 0.272064 0.043156 0.000000 0.270011 0.040153 0.000000 0.265298 0.032985 0.000000 0.257175 0.020474 0.000000 0.245264 0.001782 0.000000 0.229343 0.000000 0.000000 0.209343 0.000000 0.002934 0.185327 0.000000 0.086959 0.157425 0.000000 0.130847 0.125653 0.000000 0.160438 0.089388 0.000000 0.000000 0.191888 0.000000 0.000000 0.190420 0.000000 0.000000 0.187071 0.000000 0.000000 0.181350 0.000000 0.000000 0.173083 0.000000 0.000000 0.162268 0.000000 0.000000 0.149051 0.000000 0.000000 0.132818 0.000000 0.000000 0.112175 0.000000 0.009991 0.085246 0.000000 0.081414 0.046441 0.000000 0.895920 0.662291 1.000000 0.899774 0.653755 1.000000 0.907914 0.634603 1.000000 0.919772 0.602843 1.000000 0.932861 0.558694 1.000000 0.944187 0.503017 1.000000 0.951304 0.436325 1.000000 0.952730 0.357521 0.939684 0.947862 0.260754 0.836100 0.936706 0.113219 0.732676 0.919387 0.000000 0.630755 0.831319 0.667513 1.000000 0.837944 0.658712 1.000000 0.851472 0.639106 1.000000 0.870384 0.606908 1.000000 0.890693 0.562569 1.000000 0.908482 0.507082 1.000000 0.921009 0.441046 1.000000 0.926842 0.363568 0.915747 0.925539 0.269596 0.815787 0.917275 0.133425 0.715399 0.902247 0.000000 0.615964 0.710685 0.667512 1.000000 0.721428 0.658892 1.000000 0.743156 0.639718 1.000000 0.773230 0.608296 1.000000 0.805536 0.565139 1.000000 0.834627 0.511306 1.000000 0.857070 0.447538 0.952827 0.871311 0.373341 0.863926 0.877047 0.285036 0.771371 0.874605 0.165991 0.677295 0.864266 0.000000 0.583106 0.553716 0.650959 1.000000 0.568597 0.643247 1.000000 0.598594 0.626001 1.000000 0.640107 0.597512 1.000000 0.685158 0.558037 0.991246 0.726895 0.508436 0.930575 0.761111 0.449470 0.860324 0.785927 0.381061 0.782980 0.800954 0.300935 0.700890 0.806390 0.199339 0.615929 0.802618 0.000000 0.529556 0.379103 0.615259 0.939889 0.398688 0.608822 0.932707 0.437342 0.594330 0.916476 0.489715 0.570122 0.889220 0.546093 0.536121 0.850790 0.598938 0.492828 0.801970 0.643816 0.440841 0.744187 0.678661 0.380248 0.679165 0.702796 0.309656 0.608604 0.716302 0.223577 0.534096 0.719594 0.092925 0.457076 0.170928 0.564177 0.768612 0.204355 0.559028 0.763143 0.261857 0.547362 0.750721 0.330746 0.527676 0.729677 0.399628 0.499643 0.699617 0.462331 0.463401 0.660794 0.515702 0.419227 0.613944 0.558303 0.367162 0.560122 0.589687 0.306424 0.500553 0.609958 0.233946 0.436521 0.619493 0.137413 0.369312 0.000000 0.502427 0.600017 0.000000 0.498379 0.595783 0.000000 0.489162 0.586132 0.145020 0.473462 0.569669 0.245542 0.450823 0.545918 0.321798 0.421130 0.514854 0.383008 0.384429 0.476825 0.431401 0.340697 0.432469 0.467850 0.289472 0.382636 0.492912 0.229020 0.328318 0.507024 0.153213 0.270578 0.000000 0.432243 0.437099 0.000000 0.429071 0.433749 0.000000 0.421821 0.426096 0.000000 0.409390 0.412986 0.015934 0.391295 0.393952 0.170070 0.367303 0.368853 0.246518 0.337322 0.337831 0.301272 0.301272 0.301272 0.341335 0.258709 0.259611 0.367933 0.207664 0.212653 0.382018 0.144868 0.161494 0.000000 0.354509 0.281128 0.000000 0.352022 0.278425 0.000000 0.346322 0.272239 0.000000 0.336424 0.261540 0.000000 0.321791 0.245813 0.000000 0.302026 0.224756 0.087963 0.276882 0.198293 0.162484 0.246251 0.166622 0.206658 0.210080 0.130218 0.234724 0.168136 0.089797 0.251590 0.119010 0.038208 0.000000 0.263269 0.127037 0.000000 0.261226 0.124699 0.000000 0.256544 0.119341 0.000000 0.248474 0.110103 0.000000 0.236655 0.096551 0.000000 0.220905 0.078386 0.000000 0.201188 0.054201 0.000000 0.177537 0.014466 0.074412 0.150056 0.000000 0.119851 0.118717 0.000000 0.149788 0.082736 0.000000 0.000000 0.185545 0.000000 0.000000 0.184120 0.000000 0.000000 0.180871 0.000000 0.000000 0.175322 0.000000 0.000000 0.167304 0.000000 0.000000 0.156820 0.000000 0.000000 0.143904 0.000000 0.000000 0.127559 0.000000 0.000000 0.106645 0.000000 0.000000 0.079046 0.000000 0.061821 0.037971 0.000000 0.857154 0.612022 1.000000 0.860523 0.604491 1.000000 0.867726 0.587472 1.000000 0.878516 0.558877 1.000000 0.891042 0.518397 1.000000 0.902811 0.466247 1.000000 0.911514 0.402297 1.000000 0.915515 0.324580 1.000000 0.913932 0.224625 0.907517 0.906475 0.027159 0.805943 0.892927 0.000000 0.704739 0.801735 0.617170 1.000000 0.807014 0.609485 1.000000 0.818063 0.592198 1.000000 0.834155 0.563347 1.000000 0.852428 0.522801 1.000000 0.869612 0.470917 1.000000 0.883036 0.407707 1.000000 0.890993 0.331517 0.984962 0.892675 0.235146 0.887652 0.887886 0.069440 0.788863 0.876461 0.000000 0.689971 0.693094 0.619686 1.000000 0.701589 0.612112 1.000000 0.719182 0.595109 1.000000 0.744474 0.566826 1.000000 0.772986 0.527242 1.000000 0.800119 0.476843 1.000000 0.822484 0.415853 1.000000 0.838157 0.343167 0.934648 0.846373 0.253611 0.844072 0.846983 0.120275 0.751091 0.839929 0.000000 0.657096 0.543321 0.609135 1.000000 0.555645 0.602199 1.000000 0.580978 0.586591 1.000000 0.617112 0.560529 1.000000 0.657792 0.523905 1.000000 0.697018 0.477146 0.999309 0.730624 0.420598 0.931141 0.756371 0.353709 0.855480 0.773376 0.273215 0.774503 0.781394 0.164958 0.689972 0.780524 0.000000 0.603326 0.367028 0.581049 1.000000 0.384507 0.575107 1.000000 0.419488 0.561673 0.985727 0.467849 0.539080 0.959287 0.521134 0.507048 0.921872 0.572336 0.465806 0.874127 0.617010 0.415645 0.817314 0.652796 0.356272 0.752974 0.678687 0.285687 0.682689 0.694479 0.196385 0.607973 0.700354 0.029520 0.530196 0.134011 0.537376 0.841418 0.171460 0.532510 0.836034 0.232563 0.521460 0.823797 0.303355 0.502731 0.803028 0.373248 0.475894 0.773289 0.436874 0.440926 0.734755 0.491470 0.397911 0.688070 0.535700 0.346654 0.634200 0.569073 0.285982 0.574284 0.591597 0.211797 0.509531 0.603540 0.105469 0.441150 0.000000 0.481801 0.674208 0.000000 0.477897 0.669976 0.000000 0.468996 0.660323 0.079651 0.453797 0.643838 0.210410 0.431797 0.620017 0.294700 0.402802 0.588796 0.360174 0.366749 0.550469 0.411638 0.323461 0.505619 0.450725 0.272229 0.455035 0.478248 0.210734 0.399643 0.494529 0.130198 0.340256 0.000000 0.416389 0.510434 0.000000 0.413282 0.507046 0.000000 0.406176 0.499305 0.000000 0.393978 0.486036 0.000000 0.376183 0.466747 0.133125 0.352522 0.441273 0.223662 0.322840 0.409722 0.284239 0.286967 0.372446 0.327708 0.244041 0.329563 0.356819 0.191964 0.281042 0.373191 0.126338 0.228135 0.000000 0.342205 0.352141 0.000000 0.339720 0.349369 0.000000 0.334008 0.343008 0.000000 0.324094 0.332010 0.000000 0.309450 0.315858 0.000000 0.289686 0.294250 0.053727 0.264542 0.267123 0.147824 0.233890 0.234720 0.197595 0.197595 0.197595 0.228976 0.155198 0.156671 0.247600 0.104711 0.113268 0.000000 0.253183 0.195414 0.000000 0.251152 0.193078 0.000000 0.246497 0.187732 0.000000 0.238475 0.178547 0.000000 0.226732 0.165160 0.000000 0.211081 0.147441 0.000000 0.191499 0.125484 0.000000 0.168094 0.099609 0.063217 0.141032 0.069285 0.109457 0.110118 0.019128 0.139512 0.073907 0.000000 0.000000 0.177970 0.041099 0.000000 0.176596 0.038054 0.000000 0.173461 0.031026 0.000000 0.168106 0.018865 0.000000 0.160373 0.000957 0.000000 0.150262 0.000000 0.000000 0.137414 0.000000 0.000000 0.120861 0.000000 0.000000 0.099499 0.000000 0.000000 0.070810 0.000000 0.039391 0.026997 0.000000 0.812873 0.560069 1.000000 0.815987 0.553301 1.000000 0.822703 0.537920 1.000000 0.832982 0.511809 1.000000 0.845381 0.474278 1.000000 0.857798 0.424970 1.000000 0.868096 0.363006 1.000000 0.874582 0.285061 1.000000 0.876181 0.177274 0.973100 0.872326 0.000000 0.874011 0.862494 0.000000 0.774186 0.764396 0.565400 1.000000 0.768896 0.558549 1.000000 0.778476 0.543028 1.000000 0.792853 0.516807 1.000000 0.809900 0.479334 1.000000 0.826889 0.430403 1.000000 0.841317 0.369336 1.000000 0.851359 0.293307 1.000000 0.855952 0.190875 0.953813 0.854547 0.000000 0.857240 0.846684 0.000000 0.759539 0.665789 0.570139 1.000000 0.672859 0.563376 1.000000 0.687755 0.548090 1.000000 0.709804 0.522359 1.000000 0.735651 0.485767 1.000000 0.761432 0.438286 1.000000 0.783946 0.379552 1.000000 0.801084 0.307559 0.999383 0.811742 0.214406 0.911350 0.815342 0.035736 0.820045 0.811559 0.000000 0.726865 0.522918 0.564887 1.000000 0.533503 0.558600 1.000000 0.555582 0.544376 1.000000 0.587828 0.520406 1.000000 0.625244 0.486293 1.000000 0.662596 0.442069 1.000000 0.695882 0.387623 0.996006 0.722663 0.321754 0.922499 0.741647 0.239559 0.843134 0.752249 0.117060 0.759574 0.754316 0.000000 0.673227 0.344362 0.543980 1.000000 0.360563 0.538481 1.000000 0.393252 0.526006 1.000000 0.439025 0.504893 1.000000 0.490300 0.474692 0.987071 0.540553 0.435370 0.940700 0.585404 0.386870 0.885201 0.622331 0.328436 0.821939 0.650106 0.257162 0.752374 0.668290 0.161852 0.677926 0.676867 0.000000 0.599901 0.047666 0.507721 0.908844 0.109763 0.503125 0.903579 0.186326 0.492663 0.891599 0.264723 0.474853 0.871234 0.338500 0.449175 0.841998 0.404609 0.415442 0.803987 0.461327 0.373533 0.757755 0.507735 0.322958 0.704167 0.543467 0.262006 0.644271 0.568527 0.184893 0.579191 0.583133 0.057008 0.510066 0.000000 0.458597 0.744033 0.000000 0.454839 0.739828 0.000000 0.446258 0.730229 0.000000 0.431567 0.713820 0.154147 0.410220 0.690068 0.256329 0.381939 0.658869 0.329164 0.346534 0.620461 0.385189 0.303649 0.575365 0.427786 0.252239 0.524302 0.458371 0.189114 0.468132 0.477073 0.100375 0.407192 0.000000 0.398401 0.580274 0.000000 0.395367 0.576868 0.000000 0.388419 0.569076 0.000000 0.376470 0.555705 0.000000 0.359005 0.536250 0.052893 0.335704 0.510511 0.186928 0.306344 0.478558 0.258047 0.270603 0.440658 0.306788 0.227095 0.396511 0.339600 0.173687 0.346537 0.359114 0.103754 0.291960 0.000000 0.328075 0.419904 0.000000 0.325587 0.417051 0.000000 0.319880 0.410518 0.000000 0.309979 0.399223 0.000000 0.295363 0.382639 0.000000 0.275643 0.360459 0.000000 0.250552 0.332614 0.117318 0.219919 0.299340 0.178961 0.183494 0.261178 0.216167 0.140500 0.219026 0.238831 0.087778 0.174155 0.000000 0.241899 0.259313 0.000000 0.239886 0.256926 0.000000 0.235273 0.251462 0.000000 0.227327 0.242075 0.000000 0.215692 0.228396 0.000000 0.200187 0.210296 0.000000 0.180770 0.187882 0.000000 0.157522 0.161519 0.046831 0.130546 0.131834 0.099735 0.099735 0.099735 0.129978 0.062147 0.061552 0.000000 0.169281 0.112308 0.000000 0.167951 0.110530 0.000000 0.164921 0.106469 0.000000 0.159756 0.099512 0.000000 0.152318 0.089417 0.000000 0.142453 0.075318 0.000000 0.129434 0.054008 0.000000 0.112534 0.020272 0.000000 0.090444 0.000000 0.000000 0.059968 0.000000 0.017038 0.013945 0.000000 0.762585 0.506286 1.000000 0.765609 0.500090 1.000000 0.772182 0.485943 1.000000 0.782409 0.461711 1.000000 0.795118 0.426388 1.000000 0.808486 0.379056 1.000000 0.820528 0.317880 1.000000 0.829543 0.237156 1.000000 0.834325 0.107863 1.000000 0.834011 0.000000 0.937065 0.827867 0.000000 0.839139 0.719325 0.512038 1.000000 0.723412 0.505809 1.000000 0.732218 0.491617 1.000000 0.745730 0.467400 1.000000 0.762295 0.432270 1.000000 0.779587 0.385479 1.000000 0.795282 0.325488 1.000000 0.807518 0.247479 1.000000 0.815049 0.129341 1.000000 0.816990 0.000000 0.920686 0.812679 0.000000 0.824695 0.628682 0.518932 1.000000 0.634897 0.512795 1.000000 0.648165 0.498843 1.000000 0.668242 0.475126 1.000000 0.692516 0.440910 1.000000 0.717687 0.395685 1.000000 0.740772 0.338383 1.000000 0.759587 0.265578 1.000000 0.772754 0.163482 0.973440 0.779361 0.000000 0.884260 0.778879 0.000000 0.792403 0.491244 0.518540 1.000000 0.500771 0.512784 1.000000 0.520848 0.499701 1.000000 0.550680 0.477475 1.000000 0.586111 0.445471 1.000000 0.622495 0.403347 1.000000 0.656030 0.350467 1.000000 0.684143 0.284673 0.984282 0.705263 0.198116 0.906908 0.718555 0.028521 0.824759 0.723653 0.000000 0.739196 0.307028 0.504445 1.000000 0.323029 0.499334 1.000000 0.355288 0.487701 1.000000 0.400528 0.467898 1.000000 0.451510 0.439323 1.000000 0.502029 0.401676 1.000000 0.547844 0.354516 0.948030 0.586417 0.296491 0.886159 0.616405 0.223224 0.817677 0.637215 0.115299 0.743904 0.648693 0.000000 0.666083 0.000000 0.475519 0.971100 0.000000 0.471170 0.965978 0.097414 0.461249 0.954312 0.206129 0.444289 0.934449 0.291000 0.419682 0.905859 0.362922 0.387080 0.868565 0.423562 0.346127 0.823025 0.473212 0.295967 0.770002 0.511987 0.234093 0.710452 0.540060 0.151687 0.645412 0.557692 0.000000 0.575947 0.000000 0.433016 0.809509 0.000000 0.429399 0.805351 0.000000 0.421131 0.795856 0.000000 0.406937 0.779608 0.016293 0.386229 0.756052 0.198846 0.358636 0.725037 0.286611 0.323832 0.686751 0.350104 0.281232 0.641650 0.397729 0.229332 0.590386 0.432295 0.163532 0.533738 0.453818 0.057748 0.471341 0.000000 0.378405 0.646598 0.000000 0.375445 0.643187 0.000000 0.368661 0.635382 0.000000 0.356978 0.621980 0.000000 0.339855 0.602453 0.000000 0.316927 0.576575 0.123183 0.287890 0.544380 0.218339 0.252031 0.505797 0.276286 0.207812 0.460588 0.314743 0.152588 0.409447 0.338630 0.075355 0.353562 0.000000 0.312162 0.484569 0.000000 0.309689 0.481657 0.000000 0.304003 0.474972 0.000000 0.294156 0.463434 0.000000 0.279613 0.446488 0.000000 0.259994 0.423830 0.000000 0.235021 0.395395 0.048560 0.204457 0.361405 0.145764 0.167895 0.322385 0.193737 0.124098 0.279198 0.222794 0.067884 0.233053 0.000000 0.229578 0.320599 0.000000 0.227593 0.318163 0.000000 0.223043 0.312589 0.000000 0.215205 0.303010 0.000000 0.203728 0.289044 0.000000 0.188427 0.270552 0.000000 0.169241 0.247625 0.000000 0.146207 0.220616 0.001043 0.119321 0.190126 0.080516 0.088204 0.157028 0.120084 0.046338 0.122701 0.000000 0.160119 0.170940 0.000000 0.158829 0.169133 0.000000 0.155882 0.164998 0.000000 0.150860 0.157925 0.000000 0.143510 0.147690 0.000000 0.133343 0.134302 0.000000 0.119867 0.118003 0.000000 0.102312 0.099298 0.000000 0.079059 0.077187 0.000000 0.045504 0.044758 0.000000 0.000000 0.000000 ValveSoftware-gamescope-eb620ab/looks/invertbrightness_pq.cube000066400000000000000000001062221502457270500250270ustar00rootroot00000000000000TITLE "Generated by Foundry::LUT" LUT_3D_SIZE 11 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 0.906954 0.909745 1.000000 0.747482 0.690013 1.000000 0.611111 0.496536 1.000000 0.460548 0.323009 1.000000 0.281951 0.162104 1.000000 0.008906 0.002244 1.000000 0.008906 0.002244 1.000000 0.008906 0.002244 1.000000 0.008906 0.002244 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 0.906954 0.909745 1.000000 0.747482 0.690013 1.000000 0.611111 0.496536 1.000000 0.460548 0.323009 1.000000 0.281951 0.162104 1.000000 0.008906 0.002244 1.000000 0.008906 0.002244 1.000000 0.008906 0.002244 1.000000 0.008906 0.002244 0.746746 0.918942 0.746177 0.746746 0.918942 0.746177 0.874363 0.863697 0.702367 1.000000 0.749460 0.586937 1.000000 0.615776 0.439590 1.000000 0.468678 0.289772 1.000000 0.297429 0.133945 0.964122 0.086344 0.000000 0.964122 0.086344 0.000000 0.964122 0.086344 0.000000 0.964122 0.086344 0.000000 0.524649 0.793917 0.451622 0.524649 0.793917 0.451622 0.591950 0.770632 0.435870 0.721728 0.700904 0.383271 0.817814 0.597166 0.294936 0.860678 0.471649 0.178005 0.861175 0.324680 0.000000 0.840402 0.175419 0.000000 0.840402 0.175419 0.000000 0.840402 0.175419 0.000000 0.840402 0.175419 0.000000 0.244423 0.648310 0.183583 0.244423 0.648310 0.183583 0.300713 0.636455 0.173112 0.420164 0.596754 0.134862 0.530798 0.528843 0.046341 0.605079 0.437502 0.000000 0.640657 0.324725 0.000000 0.646332 0.216044 0.000000 0.646332 0.216044 0.000000 0.646332 0.216044 0.000000 0.646332 0.216044 0.000000 0.000000 0.496411 0.000000 0.000000 0.496411 0.000000 0.000000 0.489755 0.000000 0.000000 0.466491 0.000000 0.228877 0.423662 0.000000 0.333009 0.361313 0.000000 0.395851 0.279748 0.000000 0.420876 0.199916 0.000000 0.420876 0.199916 0.000000 0.420876 0.199916 0.000000 0.420876 0.199916 0.000000 0.000000 0.333036 0.000000 0.000000 0.333036 0.000000 0.000000 0.328769 0.000000 0.000000 0.313507 0.000000 0.000000 0.284252 0.000000 0.098382 0.239749 0.000000 0.187711 0.180102 0.000000 0.225735 0.123433 0.000000 0.225735 0.123433 0.000000 0.225735 0.123433 0.000000 0.225735 0.123433 0.000000 0.000000 0.206917 0.000000 0.000000 0.206917 0.000000 0.000000 0.203928 0.000000 0.000000 0.193453 0.000000 0.000000 0.174150 0.000000 0.000000 0.146495 0.000000 0.093561 0.107589 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.000000 0.206917 0.000000 0.000000 0.206917 0.000000 0.000000 0.203928 0.000000 0.000000 0.193453 0.000000 0.000000 0.174150 0.000000 0.000000 0.146495 0.000000 0.093561 0.107589 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.000000 0.206917 0.000000 0.000000 0.206917 0.000000 0.000000 0.203928 0.000000 0.000000 0.193453 0.000000 0.000000 0.174150 0.000000 0.000000 0.146495 0.000000 0.093561 0.107589 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.000000 0.206917 0.000000 0.000000 0.206917 0.000000 0.000000 0.203928 0.000000 0.000000 0.193453 0.000000 0.000000 0.174150 0.000000 0.000000 0.146495 0.000000 0.093561 0.107589 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 0.906954 0.909745 1.000000 0.747482 0.690013 1.000000 0.611111 0.496536 1.000000 0.460548 0.323009 1.000000 0.281951 0.162104 1.000000 0.008906 0.002244 1.000000 0.008906 0.002244 1.000000 0.008906 0.002244 1.000000 0.008906 0.002244 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 0.906954 0.909745 1.000000 0.747482 0.690013 1.000000 0.611111 0.496536 1.000000 0.460548 0.323009 1.000000 0.281951 0.162104 1.000000 0.008906 0.002244 1.000000 0.008906 0.002244 1.000000 0.008906 0.002244 1.000000 0.008906 0.002244 0.746746 0.918942 0.746177 0.746746 0.918942 0.746177 0.874363 0.863697 0.702367 1.000000 0.749460 0.586937 1.000000 0.615776 0.439590 1.000000 0.468678 0.289772 1.000000 0.297429 0.133945 0.964122 0.086344 0.000000 0.964122 0.086344 0.000000 0.964122 0.086344 0.000000 0.964122 0.086344 0.000000 0.524649 0.793917 0.451622 0.524649 0.793917 0.451622 0.591950 0.770632 0.435870 0.721728 0.700904 0.383271 0.817814 0.597166 0.294936 0.860678 0.471649 0.178005 0.861175 0.324680 0.000000 0.840402 0.175419 0.000000 0.840402 0.175419 0.000000 0.840402 0.175419 0.000000 0.840402 0.175419 0.000000 0.244423 0.648310 0.183583 0.244423 0.648310 0.183583 0.300713 0.636455 0.173112 0.420164 0.596754 0.134862 0.530798 0.528843 0.046341 0.605079 0.437502 0.000000 0.640657 0.324725 0.000000 0.646332 0.216044 0.000000 0.646332 0.216044 0.000000 0.646332 0.216044 0.000000 0.646332 0.216044 0.000000 0.000000 0.496411 0.000000 0.000000 0.496411 0.000000 0.000000 0.489755 0.000000 0.000000 0.466491 0.000000 0.228877 0.423662 0.000000 0.333009 0.361313 0.000000 0.395851 0.279748 0.000000 0.420876 0.199916 0.000000 0.420876 0.199916 0.000000 0.420876 0.199916 0.000000 0.420876 0.199916 0.000000 0.000000 0.333036 0.000000 0.000000 0.333036 0.000000 0.000000 0.328769 0.000000 0.000000 0.313507 0.000000 0.000000 0.284252 0.000000 0.098382 0.239749 0.000000 0.187711 0.180102 0.000000 0.225735 0.123433 0.000000 0.225735 0.123433 0.000000 0.225735 0.123433 0.000000 0.225735 0.123433 0.000000 0.000000 0.206917 0.000000 0.000000 0.206917 0.000000 0.000000 0.203928 0.000000 0.000000 0.193453 0.000000 0.000000 0.174150 0.000000 0.000000 0.146495 0.000000 0.093561 0.107589 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.000000 0.206917 0.000000 0.000000 0.206917 0.000000 0.000000 0.203928 0.000000 0.000000 0.193453 0.000000 0.000000 0.174150 0.000000 0.000000 0.146495 0.000000 0.093561 0.107589 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.000000 0.206917 0.000000 0.000000 0.206917 0.000000 0.000000 0.203928 0.000000 0.000000 0.193453 0.000000 0.000000 0.174150 0.000000 0.000000 0.146495 0.000000 0.093561 0.107589 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.000000 0.206917 0.000000 0.000000 0.206917 0.000000 0.000000 0.203928 0.000000 0.000000 0.193453 0.000000 0.000000 0.174150 0.000000 0.000000 0.146495 0.000000 0.093561 0.107589 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.145058 0.060662 0.000000 0.969316 0.967716 1.000000 0.969316 0.967716 1.000000 1.000000 0.878451 1.000000 1.000000 0.741947 0.839872 1.000000 0.608631 0.639947 1.000000 0.458604 0.451198 1.000000 0.279112 0.275366 0.998536 0.000000 0.145689 0.998536 0.000000 0.145689 0.998536 0.000000 0.145689 0.998536 0.000000 0.145689 0.969316 0.967716 1.000000 0.969316 0.967716 1.000000 1.000000 0.878451 1.000000 1.000000 0.741947 0.839872 1.000000 0.608631 0.639947 1.000000 0.458604 0.451198 1.000000 0.279112 0.275366 0.998536 0.000000 0.145689 0.998536 0.000000 0.145689 0.998536 0.000000 0.145689 0.998536 0.000000 0.145689 0.744522 0.897394 0.893706 0.744522 0.897394 0.893706 0.850127 0.850127 0.850127 0.979946 0.743556 0.731572 1.000000 0.612709 0.574754 1.000000 0.466250 0.408760 1.000000 0.294413 0.245178 0.960749 0.077333 0.118837 0.960749 0.077333 0.118837 0.960749 0.077333 0.118837 0.960749 0.077333 0.118837 0.505782 0.786452 0.571472 0.505782 0.786452 0.571472 0.573046 0.764025 0.554021 0.705138 0.696025 0.496246 0.805850 0.593590 0.400878 0.853395 0.468661 0.280537 0.857510 0.321543 0.143000 0.838593 0.170658 0.000006 0.838593 0.170658 0.000006 0.838593 0.170658 0.000006 0.838593 0.170658 0.000006 0.234345 0.644396 0.276705 0.234345 0.644396 0.276705 0.292440 0.632670 0.266894 0.414385 0.593324 0.232576 0.527068 0.525815 0.168593 0.603121 0.434728 0.062361 0.639792 0.321960 0.000000 0.645008 0.213104 0.000000 0.645008 0.213104 0.000000 0.645008 0.213104 0.000000 0.645008 0.213104 0.000000 0.000000 0.494012 0.000000 0.000000 0.494012 0.000000 0.000000 0.487374 0.000000 0.000235 0.464183 0.000000 0.229859 0.421508 0.000000 0.332372 0.359346 0.000000 0.394451 0.277934 0.000000 0.419028 0.198065 0.000000 0.419028 0.198065 0.000000 0.419028 0.198065 0.000000 0.419028 0.198065 0.000000 0.000000 0.331716 0.000000 0.000000 0.331716 0.000000 0.000000 0.327450 0.000000 0.000000 0.312211 0.000000 0.000000 0.283008 0.000000 0.091252 0.238606 0.000000 0.183453 0.179126 0.000000 0.221995 0.122635 0.000000 0.221995 0.122635 0.000000 0.221995 0.122635 0.000000 0.221995 0.122635 0.000000 0.000000 0.206121 0.000000 0.000000 0.206121 0.000000 0.000000 0.203150 0.000000 0.000000 0.192729 0.000000 0.000000 0.173535 0.000000 0.000000 0.146042 0.000000 0.085648 0.107167 0.000000 0.139907 0.060218 0.000000 0.139907 0.060218 0.000000 0.139907 0.060218 0.000000 0.139907 0.060218 0.000000 0.000000 0.206121 0.000000 0.000000 0.206121 0.000000 0.000000 0.203150 0.000000 0.000000 0.192729 0.000000 0.000000 0.173535 0.000000 0.000000 0.146042 0.000000 0.085648 0.107167 0.000000 0.139907 0.060218 0.000000 0.139907 0.060218 0.000000 0.139907 0.060218 0.000000 0.139907 0.060218 0.000000 0.000000 0.206121 0.000000 0.000000 0.206121 0.000000 0.000000 0.203150 0.000000 0.000000 0.192729 0.000000 0.000000 0.173535 0.000000 0.000000 0.146042 0.000000 0.085648 0.107167 0.000000 0.139907 0.060218 0.000000 0.139907 0.060218 0.000000 0.139907 0.060218 0.000000 0.139907 0.060218 0.000000 0.000000 0.206121 0.000000 0.000000 0.206121 0.000000 0.000000 0.203150 0.000000 0.000000 0.192729 0.000000 0.000000 0.173535 0.000000 0.000000 0.146042 0.000000 0.085648 0.107167 0.000000 0.139907 0.060218 0.000000 0.139907 0.060218 0.000000 0.139907 0.060218 0.000000 0.139907 0.060218 0.000000 1.000000 0.854330 1.000000 1.000000 0.854330 1.000000 1.000000 0.808680 1.000000 1.000000 0.715309 1.000000 1.000000 0.592572 0.834607 1.000000 0.446766 0.637339 1.000000 0.266486 0.449612 0.984969 0.000000 0.314959 0.984969 0.000000 0.314959 0.984969 0.000000 0.314959 0.984969 0.000000 0.314959 1.000000 0.854330 1.000000 1.000000 0.854330 1.000000 1.000000 0.808680 1.000000 1.000000 0.715309 1.000000 1.000000 0.592572 0.834607 1.000000 0.446766 0.637339 1.000000 0.266486 0.449612 0.984969 0.000000 0.314959 0.984969 0.000000 0.314959 0.984969 0.000000 0.314959 0.984969 0.000000 0.314959 0.779859 0.837797 1.000000 0.779859 0.837797 1.000000 0.836982 0.805518 1.000000 0.934805 0.717393 0.924503 0.993134 0.596445 0.765099 1.000000 0.454181 0.590336 0.982868 0.282055 0.416645 0.948666 0.035707 0.289214 0.948666 0.035707 0.289214 0.948666 0.035707 0.289214 0.948666 0.035707 0.289214 0.497184 0.759199 0.755493 0.497184 0.759199 0.755493 0.554724 0.739250 0.737253 0.676769 0.676768 0.676768 0.778780 0.579198 0.576499 0.833174 0.457065 0.450251 0.844902 0.310143 0.312346 0.830932 0.153504 0.204241 0.830932 0.153504 0.204241 0.830932 0.153504 0.204241 0.830932 0.153504 0.204241 0.214482 0.630313 0.441342 0.214482 0.630313 0.441342 0.274657 0.619037 0.431381 0.399319 0.580961 0.396987 0.515175 0.514977 0.335500 0.595140 0.424971 0.250287 0.635670 0.312219 0.145655 0.642274 0.201714 0.045133 0.642274 0.201714 0.045133 0.642274 0.201714 0.045133 0.642274 0.201714 0.045133 0.000000 0.485653 0.146588 0.000000 0.485653 0.146588 0.000000 0.479085 0.139500 0.022750 0.456110 0.113747 0.234704 0.413752 0.059166 0.333709 0.352017 0.000000 0.392210 0.271063 0.000000 0.414740 0.190904 0.000000 0.414740 0.190904 0.000000 0.414740 0.190904 0.000000 0.414740 0.190904 0.000000 0.000000 0.326770 0.000000 0.000000 0.326770 0.000000 0.000000 0.322525 0.000000 0.000000 0.307353 0.000000 0.000000 0.278321 0.000000 0.071682 0.234251 0.000000 0.171681 0.175302 0.000000 0.211289 0.119334 0.000000 0.211289 0.119334 0.000000 0.211289 0.119334 0.000000 0.211289 0.119334 0.000000 0.000000 0.203083 0.000000 0.000000 0.203083 0.000000 0.000000 0.200164 0.000000 0.000000 0.189937 0.000000 0.000000 0.171101 0.000000 0.000000 0.144113 0.000000 0.058385 0.105256 0.000000 0.123686 0.058039 0.000000 0.123686 0.058039 0.000000 0.123686 0.058039 0.000000 0.123686 0.058039 0.000000 0.000000 0.203083 0.000000 0.000000 0.203083 0.000000 0.000000 0.200164 0.000000 0.000000 0.189937 0.000000 0.000000 0.171101 0.000000 0.000000 0.144113 0.000000 0.058385 0.105256 0.000000 0.123686 0.058039 0.000000 0.123686 0.058039 0.000000 0.123686 0.058039 0.000000 0.123686 0.058039 0.000000 0.000000 0.203083 0.000000 0.000000 0.203083 0.000000 0.000000 0.200164 0.000000 0.000000 0.189937 0.000000 0.000000 0.171101 0.000000 0.000000 0.144113 0.000000 0.058385 0.105256 0.000000 0.123686 0.058039 0.000000 0.123686 0.058039 0.000000 0.123686 0.058039 0.000000 0.123686 0.058039 0.000000 0.000000 0.203083 0.000000 0.000000 0.203083 0.000000 0.000000 0.200164 0.000000 0.000000 0.189937 0.000000 0.000000 0.171101 0.000000 0.000000 0.144113 0.000000 0.058385 0.105256 0.000000 0.123686 0.058039 0.000000 0.123686 0.058039 0.000000 0.123686 0.058039 0.000000 0.123686 0.058039 0.000000 0.956808 0.756093 1.000000 0.956808 0.756093 1.000000 0.967242 0.734038 1.000000 0.994996 0.665739 1.000000 1.000000 0.557951 0.995301 1.000000 0.419975 0.803456 0.992742 0.238309 0.613137 0.960109 0.000000 0.473167 0.960109 0.000000 0.473167 0.960109 0.000000 0.473167 0.960109 0.000000 0.473167 0.956808 0.756093 1.000000 0.956808 0.756093 1.000000 0.967242 0.734038 1.000000 0.994996 0.665739 1.000000 1.000000 0.557951 0.995301 1.000000 0.419975 0.803456 0.992742 0.238309 0.613137 0.960109 0.000000 0.473167 0.960109 0.000000 0.473167 0.960109 0.000000 0.473167 0.960109 0.000000 0.473167 0.799713 0.761381 1.000000 0.799713 0.761381 1.000000 0.828233 0.738802 1.000000 0.891691 0.669391 1.000000 0.943161 0.562639 0.928519 0.962668 0.428003 0.756044 0.951491 0.255574 0.578920 0.925791 0.000000 0.446198 0.925791 0.000000 0.446198 0.925791 0.000000 0.446198 0.925791 0.000000 0.446198 0.507891 0.712021 0.918472 0.507891 0.712021 0.918472 0.550000 0.695161 0.900663 0.649512 0.640385 0.841155 0.744099 0.550692 0.741044 0.802166 0.433800 0.612778 0.821765 0.287588 0.470511 0.814161 0.116973 0.358254 0.814161 0.116973 0.358254 0.814161 0.116973 0.358254 0.814161 0.116973 0.358254 0.194807 0.603964 0.602708 0.194807 0.603964 0.602708 0.254239 0.593435 0.592520 0.377480 0.557544 0.557324 0.494350 0.494350 0.494350 0.578121 0.406514 0.407297 0.623843 0.293995 0.303021 0.634479 0.179394 0.215734 0.634479 0.179394 0.215734 0.634479 0.179394 0.215734 0.634479 0.179394 0.215734 0.000000 0.470109 0.308970 0.000000 0.470109 0.308970 0.000000 0.463679 0.302594 0.002495 0.441139 0.280191 0.230567 0.399395 0.238617 0.332368 0.338136 0.177908 0.391611 0.257115 0.098947 0.410906 0.175699 0.017014 0.410906 0.175699 0.017014 0.410906 0.175699 0.017014 0.410906 0.175699 0.017014 0.000000 0.316722 0.007323 0.000000 0.316722 0.007323 0.000000 0.312501 0.001854 0.000000 0.297464 0.000000 0.000000 0.268744 0.000000 0.050295 0.225253 0.000000 0.157328 0.167157 0.000000 0.197089 0.111867 0.000000 0.197089 0.111867 0.000000 0.197089 0.111867 0.000000 0.197089 0.111867 0.000000 0.000000 0.196794 0.000000 0.000000 0.196794 0.000000 0.000000 0.193970 0.000000 0.000000 0.184081 0.000000 0.000000 0.165881 0.000000 0.000000 0.139606 0.000000 0.007187 0.100516 0.000000 0.098079 0.052070 0.000000 0.098079 0.052070 0.000000 0.098079 0.052070 0.000000 0.098079 0.052070 0.000000 0.000000 0.196794 0.000000 0.000000 0.196794 0.000000 0.000000 0.193970 0.000000 0.000000 0.184081 0.000000 0.000000 0.165881 0.000000 0.000000 0.139606 0.000000 0.007187 0.100516 0.000000 0.098079 0.052070 0.000000 0.098079 0.052070 0.000000 0.098079 0.052070 0.000000 0.098079 0.052070 0.000000 0.000000 0.196794 0.000000 0.000000 0.196794 0.000000 0.000000 0.193970 0.000000 0.000000 0.184081 0.000000 0.000000 0.165881 0.000000 0.000000 0.139606 0.000000 0.007187 0.100516 0.000000 0.098079 0.052070 0.000000 0.098079 0.052070 0.000000 0.098079 0.052070 0.000000 0.098079 0.052070 0.000000 0.000000 0.196794 0.000000 0.000000 0.196794 0.000000 0.000000 0.193970 0.000000 0.000000 0.184081 0.000000 0.000000 0.165881 0.000000 0.000000 0.139606 0.000000 0.007187 0.100516 0.000000 0.098079 0.052070 0.000000 0.098079 0.052070 0.000000 0.098079 0.052070 0.000000 0.098079 0.052070 0.000000 0.901853 0.670494 1.000000 0.901853 0.670494 1.000000 0.909140 0.654187 1.000000 0.929670 0.599520 1.000000 0.951173 0.505914 1.000000 0.958855 0.376538 0.944851 0.946540 0.187764 0.758954 0.923413 0.000000 0.618007 0.923413 0.000000 0.618007 0.923413 0.000000 0.618007 0.923413 0.000000 0.618007 0.901853 0.670494 1.000000 0.901853 0.670494 1.000000 0.909140 0.654187 1.000000 0.929670 0.599520 1.000000 0.951173 0.505914 1.000000 0.958855 0.376538 0.944851 0.946540 0.187764 0.758954 0.923413 0.000000 0.618007 0.923413 0.000000 0.618007 0.923413 0.000000 0.618007 0.923413 0.000000 0.618007 0.784393 0.677095 1.000000 0.784393 0.677095 1.000000 0.800503 0.660279 1.000000 0.842553 0.604961 1.000000 0.885665 0.512184 1.000000 0.909441 0.386185 0.899130 0.908318 0.209835 0.724792 0.891294 0.000000 0.590466 0.891294 0.000000 0.590466 0.891294 0.000000 0.590466 0.891294 0.000000 0.590466 0.511237 0.649919 1.000000 0.511237 0.649919 1.000000 0.541030 0.635863 1.000000 0.618389 0.588786 0.980628 0.701824 0.508041 0.884410 0.760733 0.397653 0.758396 0.787319 0.251183 0.615382 0.786445 0.029919 0.500084 0.786445 0.029919 0.500084 0.786445 0.029919 0.500084 0.786445 0.029919 0.500084 0.163397 0.565726 0.748318 0.163397 0.565726 0.748318 0.223384 0.556096 0.738171 0.345353 0.522927 0.703009 0.462431 0.463381 0.639647 0.549691 0.378526 0.551139 0.601198 0.266084 0.443791 0.616923 0.142989 0.352881 0.616923 0.142989 0.352881 0.616923 0.142989 0.352881 0.616923 0.142989 0.352881 0.000000 0.446991 0.451152 0.000000 0.446991 0.451152 0.000000 0.440768 0.444637 0.000000 0.418882 0.421737 0.205135 0.378087 0.379272 0.317588 0.317588 0.317588 0.382512 0.235922 0.239336 0.404721 0.150599 0.168702 0.404721 0.150599 0.168702 0.404721 0.150599 0.168702 0.404721 0.150599 0.168702 0.000000 0.301206 0.173240 0.000000 0.301206 0.173240 0.000000 0.296990 0.168565 0.000000 0.281995 0.151983 0.000000 0.253487 0.120606 0.042706 0.210620 0.073429 0.145285 0.153599 0.000000 0.182942 0.098816 0.000000 0.182942 0.098816 0.000000 0.182942 0.098816 0.000000 0.182942 0.098816 0.000000 0.000000 0.186686 0.000000 0.000000 0.186686 0.000000 0.000000 0.183998 0.000000 0.000000 0.174585 0.000000 0.000000 0.157276 0.000000 0.000000 0.131523 0.000000 0.000000 0.091582 0.000000 0.065255 0.039601 0.000000 0.065255 0.039601 0.000000 0.065255 0.039601 0.000000 0.065255 0.039601 0.000000 0.000000 0.186686 0.000000 0.000000 0.186686 0.000000 0.000000 0.183998 0.000000 0.000000 0.174585 0.000000 0.000000 0.157276 0.000000 0.000000 0.131523 0.000000 0.000000 0.091582 0.000000 0.065255 0.039601 0.000000 0.065255 0.039601 0.000000 0.065255 0.039601 0.000000 0.065255 0.039601 0.000000 0.000000 0.186686 0.000000 0.000000 0.186686 0.000000 0.000000 0.183998 0.000000 0.000000 0.174585 0.000000 0.000000 0.157276 0.000000 0.000000 0.131523 0.000000 0.000000 0.091582 0.000000 0.065255 0.039601 0.000000 0.065255 0.039601 0.000000 0.065255 0.039601 0.000000 0.065255 0.039601 0.000000 0.000000 0.186686 0.000000 0.000000 0.186686 0.000000 0.000000 0.183998 0.000000 0.000000 0.174585 0.000000 0.000000 0.157276 0.000000 0.000000 0.131523 0.000000 0.000000 0.091582 0.000000 0.065255 0.039601 0.000000 0.065255 0.039601 0.000000 0.065255 0.039601 0.000000 0.065255 0.039601 0.000000 0.830259 0.579883 1.000000 0.830259 0.579883 1.000000 0.836150 0.566708 1.000000 0.853783 0.521175 1.000000 0.875755 0.438552 1.000000 0.889905 0.315061 1.000000 0.888337 0.090777 0.886793 0.874460 0.000000 0.748504 0.874460 0.000000 0.748504 0.874460 0.000000 0.748504 0.874460 0.000000 0.748504 0.830259 0.579883 1.000000 0.830259 0.579883 1.000000 0.836150 0.566708 1.000000 0.853783 0.521175 1.000000 0.875755 0.438552 1.000000 0.889905 0.315061 1.000000 0.888337 0.090777 0.886793 0.874460 0.000000 0.748504 0.874460 0.000000 0.748504 0.874460 0.000000 0.748504 0.874460 0.000000 0.748504 0.738131 0.587703 1.000000 0.738131 0.587703 1.000000 0.748992 0.574342 1.000000 0.780142 0.528670 1.000000 0.817823 0.447122 1.000000 0.845038 0.327739 1.000000 0.853105 0.131230 0.853616 0.844552 0.000000 0.721086 0.844552 0.000000 0.721086 0.844552 0.000000 0.721086 0.844552 0.000000 0.721086 0.489493 0.577425 1.000000 0.489493 0.577425 1.000000 0.511886 0.565596 1.000000 0.574000 0.525043 1.000000 0.648170 0.452574 1.000000 0.707448 0.348172 0.886257 0.740502 0.196351 0.745971 0.746447 0.000000 0.630307 0.746447 0.000000 0.630307 0.746447 0.000000 0.630307 0.746447 0.000000 0.630307 0.068296 0.517174 0.876603 0.068296 0.517174 0.876603 0.155309 0.508448 0.866745 0.291877 0.478075 0.832451 0.414183 0.422405 0.770128 0.506720 0.340663 0.681995 0.565102 0.226899 0.573464 0.586840 0.081020 0.480079 0.586840 0.081020 0.480079 0.586840 0.081020 0.480079 0.586840 0.081020 0.480079 0.000000 0.416461 0.581336 0.000000 0.416461 0.581336 0.000000 0.410493 0.574724 0.000000 0.389428 0.551437 0.136325 0.349833 0.508067 0.281387 0.290277 0.444622 0.357383 0.207089 0.362605 0.385478 0.114438 0.287640 0.385478 0.114438 0.287640 0.385478 0.114438 0.287640 0.385478 0.114438 0.287640 0.000000 0.280797 0.295035 0.000000 0.280797 0.295035 0.000000 0.276613 0.290210 0.000000 0.261739 0.273133 0.000000 0.233484 0.241037 0.000214 0.190937 0.193809 0.134092 0.134093 0.134092 0.170598 0.078653 0.082738 0.170598 0.078653 0.082738 0.170598 0.078653 0.082738 0.170598 0.078653 0.082738 0.000000 0.172653 0.089225 0.000000 0.172653 0.089225 0.000000 0.170136 0.085885 0.000000 0.161320 0.073433 0.000000 0.145067 0.043342 0.000000 0.118973 0.000000 0.000000 0.076900 0.000000 0.024974 0.019097 0.000000 0.024974 0.019097 0.000000 0.024974 0.019097 0.000000 0.024974 0.019097 0.000000 0.000000 0.172653 0.089225 0.000000 0.172653 0.089225 0.000000 0.170136 0.085885 0.000000 0.161320 0.073433 0.000000 0.145067 0.043342 0.000000 0.118973 0.000000 0.000000 0.076900 0.000000 0.024974 0.019097 0.000000 0.024974 0.019097 0.000000 0.024974 0.019097 0.000000 0.024974 0.019097 0.000000 0.000000 0.172653 0.089225 0.000000 0.172653 0.089225 0.000000 0.170136 0.085885 0.000000 0.161320 0.073433 0.000000 0.145067 0.043342 0.000000 0.118973 0.000000 0.000000 0.076900 0.000000 0.024974 0.019097 0.000000 0.024974 0.019097 0.000000 0.024974 0.019097 0.000000 0.024974 0.019097 0.000000 0.000000 0.172653 0.089225 0.000000 0.172653 0.089225 0.000000 0.170136 0.085885 0.000000 0.161320 0.073433 0.000000 0.145067 0.043342 0.000000 0.118973 0.000000 0.000000 0.076900 0.000000 0.024974 0.019097 0.000000 0.024974 0.019097 0.000000 0.024974 0.019097 0.000000 0.024974 0.019097 0.000000 0.762585 0.506286 1.000000 0.762585 0.506286 1.000000 0.768187 0.494657 1.000000 0.785489 0.453763 1.000000 0.809067 0.376636 1.000000 0.828311 0.252495 1.000000 0.834780 0.000000 0.973632 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.762585 0.506286 1.000000 0.762585 0.506286 1.000000 0.768187 0.494657 1.000000 0.785489 0.453763 1.000000 0.809067 0.376636 1.000000 0.828311 0.252495 1.000000 0.834780 0.000000 0.973632 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.683312 0.515564 1.000000 0.683312 0.515564 1.000000 0.692439 0.503910 1.000000 0.719827 0.463256 1.000000 0.756071 0.387703 1.000000 0.786585 0.269416 1.000000 0.801652 0.000000 0.941621 0.799554 0.000000 0.812181 0.799554 0.000000 0.812181 0.799554 0.000000 0.812181 0.799554 0.000000 0.812181 0.449197 0.516367 1.000000 0.449197 0.516367 1.000000 0.468775 0.505846 1.000000 0.524790 0.469253 1.000000 0.595457 0.401932 1.000000 0.656364 0.300308 0.973157 0.694790 0.132677 0.836704 0.706233 0.000000 0.722296 0.706233 0.000000 0.722296 0.706233 0.000000 0.722296 0.706233 0.000000 0.722296 0.000000 0.473898 0.963957 0.000000 0.473898 0.963957 0.000000 0.465800 0.954441 0.219243 0.437396 0.921227 0.359264 0.384465 0.860468 0.460308 0.304607 0.773719 0.525966 0.187062 0.665648 0.553320 0.000000 0.571579 0.553320 0.000000 0.571579 0.553320 0.000000 0.571579 0.553320 0.000000 0.571579 0.000000 0.388270 0.673651 0.000000 0.388270 0.673651 0.000000 0.382516 0.667035 0.000000 0.362135 0.643703 0.000000 0.323540 0.600095 0.229625 0.264679 0.535931 0.322515 0.179145 0.451258 0.358170 0.074793 0.373678 0.358170 0.074793 0.373678 0.358170 0.074793 0.373678 0.358170 0.074793 0.373678 0.000000 0.262055 0.381839 0.000000 0.262055 0.381839 0.000000 0.257928 0.376875 0.000000 0.243263 0.359300 0.000000 0.215402 0.326246 0.000000 0.173323 0.277512 0.103289 0.116312 0.215595 0.155728 0.057781 0.162253 0.155728 0.057781 0.162253 0.155728 0.057781 0.162253 0.155728 0.057781 0.162253 0.000000 0.160119 0.170940 0.000000 0.160119 0.170940 0.000000 0.157697 0.167546 0.000000 0.149225 0.155614 0.000000 0.132816 0.133633 0.000000 0.105569 0.102551 0.000000 0.059977 0.058831 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.160119 0.170940 0.000000 0.160119 0.170940 0.000000 0.157697 0.167546 0.000000 0.149225 0.155614 0.000000 0.132816 0.133633 0.000000 0.105569 0.102551 0.000000 0.059977 0.058831 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.160119 0.170940 0.000000 0.160119 0.170940 0.000000 0.157697 0.167546 0.000000 0.149225 0.155614 0.000000 0.132816 0.133633 0.000000 0.105569 0.102551 0.000000 0.059977 0.058831 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.160119 0.170940 0.000000 0.160119 0.170940 0.000000 0.157697 0.167546 0.000000 0.149225 0.155614 0.000000 0.132816 0.133633 0.000000 0.105569 0.102551 0.000000 0.059977 0.058831 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.762585 0.506286 1.000000 0.762585 0.506286 1.000000 0.768187 0.494657 1.000000 0.785489 0.453763 1.000000 0.809067 0.376636 1.000000 0.828311 0.252495 1.000000 0.834780 0.000000 0.973632 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.762585 0.506286 1.000000 0.762585 0.506286 1.000000 0.768187 0.494657 1.000000 0.785489 0.453763 1.000000 0.809067 0.376636 1.000000 0.828311 0.252495 1.000000 0.834780 0.000000 0.973632 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.683312 0.515564 1.000000 0.683312 0.515564 1.000000 0.692439 0.503910 1.000000 0.719827 0.463256 1.000000 0.756071 0.387703 1.000000 0.786585 0.269416 1.000000 0.801652 0.000000 0.941621 0.799554 0.000000 0.812181 0.799554 0.000000 0.812181 0.799554 0.000000 0.812181 0.799554 0.000000 0.812181 0.449197 0.516367 1.000000 0.449197 0.516367 1.000000 0.468775 0.505846 1.000000 0.524790 0.469253 1.000000 0.595457 0.401932 1.000000 0.656364 0.300308 0.973157 0.694790 0.132677 0.836704 0.706233 0.000000 0.722296 0.706233 0.000000 0.722296 0.706233 0.000000 0.722296 0.706233 0.000000 0.722296 0.000000 0.473898 0.963957 0.000000 0.473898 0.963957 0.000000 0.465800 0.954441 0.219243 0.437396 0.921227 0.359264 0.384465 0.860468 0.460308 0.304607 0.773719 0.525966 0.187062 0.665648 0.553320 0.000000 0.571579 0.553320 0.000000 0.571579 0.553320 0.000000 0.571579 0.553320 0.000000 0.571579 0.000000 0.388270 0.673651 0.000000 0.388270 0.673651 0.000000 0.382516 0.667035 0.000000 0.362135 0.643703 0.000000 0.323540 0.600095 0.229625 0.264679 0.535931 0.322515 0.179145 0.451258 0.358170 0.074793 0.373678 0.358170 0.074793 0.373678 0.358170 0.074793 0.373678 0.358170 0.074793 0.373678 0.000000 0.262055 0.381839 0.000000 0.262055 0.381839 0.000000 0.257928 0.376875 0.000000 0.243263 0.359300 0.000000 0.215402 0.326246 0.000000 0.173323 0.277512 0.103289 0.116312 0.215595 0.155728 0.057781 0.162253 0.155728 0.057781 0.162253 0.155728 0.057781 0.162253 0.155728 0.057781 0.162253 0.000000 0.160119 0.170940 0.000000 0.160119 0.170940 0.000000 0.157697 0.167546 0.000000 0.149225 0.155614 0.000000 0.132816 0.133633 0.000000 0.105569 0.102551 0.000000 0.059977 0.058831 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.160119 0.170940 0.000000 0.160119 0.170940 0.000000 0.157697 0.167546 0.000000 0.149225 0.155614 0.000000 0.132816 0.133633 0.000000 0.105569 0.102551 0.000000 0.059977 0.058831 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.160119 0.170940 0.000000 0.160119 0.170940 0.000000 0.157697 0.167546 0.000000 0.149225 0.155614 0.000000 0.132816 0.133633 0.000000 0.105569 0.102551 0.000000 0.059977 0.058831 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.160119 0.170940 0.000000 0.160119 0.170940 0.000000 0.157697 0.167546 0.000000 0.149225 0.155614 0.000000 0.132816 0.133633 0.000000 0.105569 0.102551 0.000000 0.059977 0.058831 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.762585 0.506286 1.000000 0.762585 0.506286 1.000000 0.768187 0.494657 1.000000 0.785489 0.453763 1.000000 0.809067 0.376636 1.000000 0.828311 0.252495 1.000000 0.834780 0.000000 0.973632 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.762585 0.506286 1.000000 0.762585 0.506286 1.000000 0.768187 0.494657 1.000000 0.785489 0.453763 1.000000 0.809067 0.376636 1.000000 0.828311 0.252495 1.000000 0.834780 0.000000 0.973632 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.683312 0.515564 1.000000 0.683312 0.515564 1.000000 0.692439 0.503910 1.000000 0.719827 0.463256 1.000000 0.756071 0.387703 1.000000 0.786585 0.269416 1.000000 0.801652 0.000000 0.941621 0.799554 0.000000 0.812181 0.799554 0.000000 0.812181 0.799554 0.000000 0.812181 0.799554 0.000000 0.812181 0.449197 0.516367 1.000000 0.449197 0.516367 1.000000 0.468775 0.505846 1.000000 0.524790 0.469253 1.000000 0.595457 0.401932 1.000000 0.656364 0.300308 0.973157 0.694790 0.132677 0.836704 0.706233 0.000000 0.722296 0.706233 0.000000 0.722296 0.706233 0.000000 0.722296 0.706233 0.000000 0.722296 0.000000 0.473898 0.963957 0.000000 0.473898 0.963957 0.000000 0.465800 0.954441 0.219243 0.437396 0.921227 0.359264 0.384465 0.860468 0.460308 0.304607 0.773719 0.525966 0.187062 0.665648 0.553320 0.000000 0.571579 0.553320 0.000000 0.571579 0.553320 0.000000 0.571579 0.553320 0.000000 0.571579 0.000000 0.388270 0.673651 0.000000 0.388270 0.673651 0.000000 0.382516 0.667035 0.000000 0.362135 0.643703 0.000000 0.323540 0.600095 0.229625 0.264679 0.535931 0.322515 0.179145 0.451258 0.358170 0.074793 0.373678 0.358170 0.074793 0.373678 0.358170 0.074793 0.373678 0.358170 0.074793 0.373678 0.000000 0.262055 0.381839 0.000000 0.262055 0.381839 0.000000 0.257928 0.376875 0.000000 0.243263 0.359300 0.000000 0.215402 0.326246 0.000000 0.173323 0.277512 0.103289 0.116312 0.215595 0.155728 0.057781 0.162253 0.155728 0.057781 0.162253 0.155728 0.057781 0.162253 0.155728 0.057781 0.162253 0.000000 0.160119 0.170940 0.000000 0.160119 0.170940 0.000000 0.157697 0.167546 0.000000 0.149225 0.155614 0.000000 0.132816 0.133633 0.000000 0.105569 0.102551 0.000000 0.059977 0.058831 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.160119 0.170940 0.000000 0.160119 0.170940 0.000000 0.157697 0.167546 0.000000 0.149225 0.155614 0.000000 0.132816 0.133633 0.000000 0.105569 0.102551 0.000000 0.059977 0.058831 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.160119 0.170940 0.000000 0.160119 0.170940 0.000000 0.157697 0.167546 0.000000 0.149225 0.155614 0.000000 0.132816 0.133633 0.000000 0.105569 0.102551 0.000000 0.059977 0.058831 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.160119 0.170940 0.000000 0.160119 0.170940 0.000000 0.157697 0.167546 0.000000 0.149225 0.155614 0.000000 0.132816 0.133633 0.000000 0.105569 0.102551 0.000000 0.059977 0.058831 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.762585 0.506286 1.000000 0.762585 0.506286 1.000000 0.768187 0.494657 1.000000 0.785489 0.453763 1.000000 0.809067 0.376636 1.000000 0.828311 0.252495 1.000000 0.834780 0.000000 0.973632 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.762585 0.506286 1.000000 0.762585 0.506286 1.000000 0.768187 0.494657 1.000000 0.785489 0.453763 1.000000 0.809067 0.376636 1.000000 0.828311 0.252495 1.000000 0.834780 0.000000 0.973632 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.827867 0.000000 0.839139 0.683312 0.515564 1.000000 0.683312 0.515564 1.000000 0.692439 0.503910 1.000000 0.719827 0.463256 1.000000 0.756071 0.387703 1.000000 0.786585 0.269416 1.000000 0.801652 0.000000 0.941621 0.799554 0.000000 0.812181 0.799554 0.000000 0.812181 0.799554 0.000000 0.812181 0.799554 0.000000 0.812181 0.449197 0.516367 1.000000 0.449197 0.516367 1.000000 0.468775 0.505846 1.000000 0.524790 0.469253 1.000000 0.595457 0.401932 1.000000 0.656364 0.300308 0.973157 0.694790 0.132677 0.836704 0.706233 0.000000 0.722296 0.706233 0.000000 0.722296 0.706233 0.000000 0.722296 0.706233 0.000000 0.722296 0.000000 0.473898 0.963957 0.000000 0.473898 0.963957 0.000000 0.465800 0.954441 0.219243 0.437396 0.921227 0.359264 0.384465 0.860468 0.460308 0.304607 0.773719 0.525966 0.187062 0.665648 0.553320 0.000000 0.571579 0.553320 0.000000 0.571579 0.553320 0.000000 0.571579 0.553320 0.000000 0.571579 0.000000 0.388270 0.673651 0.000000 0.388270 0.673651 0.000000 0.382516 0.667035 0.000000 0.362135 0.643703 0.000000 0.323540 0.600095 0.229625 0.264679 0.535931 0.322515 0.179145 0.451258 0.358170 0.074793 0.373678 0.358170 0.074793 0.373678 0.358170 0.074793 0.373678 0.358170 0.074793 0.373678 0.000000 0.262055 0.381839 0.000000 0.262055 0.381839 0.000000 0.257928 0.376875 0.000000 0.243263 0.359300 0.000000 0.215402 0.326246 0.000000 0.173323 0.277512 0.103289 0.116312 0.215595 0.155728 0.057781 0.162253 0.155728 0.057781 0.162253 0.155728 0.057781 0.162253 0.155728 0.057781 0.162253 0.000000 0.160119 0.170940 0.000000 0.160119 0.170940 0.000000 0.157697 0.167546 0.000000 0.149225 0.155614 0.000000 0.132816 0.133633 0.000000 0.105569 0.102551 0.000000 0.059977 0.058831 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.160119 0.170940 0.000000 0.160119 0.170940 0.000000 0.157697 0.167546 0.000000 0.149225 0.155614 0.000000 0.132816 0.133633 0.000000 0.105569 0.102551 0.000000 0.059977 0.058831 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.160119 0.170940 0.000000 0.160119 0.170940 0.000000 0.157697 0.167546 0.000000 0.149225 0.155614 0.000000 0.132816 0.133633 0.000000 0.105569 0.102551 0.000000 0.059977 0.058831 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.160119 0.170940 0.000000 0.160119 0.170940 0.000000 0.157697 0.167546 0.000000 0.149225 0.155614 0.000000 0.132816 0.133633 0.000000 0.105569 0.102551 0.000000 0.059977 0.058831 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ValveSoftware-gamescope-eb620ab/looks/invertcolors_g22.cube000066400000000000000000000065371502457270500241420ustar00rootroot00000000000000TITLE "Generated by Foundry::LUT" LUT_3D_SIZE 5 1.000000 1.000000 1.000000 0.750000 1.000000 1.000000 0.500000 1.000000 1.000000 0.250000 1.000000 1.000000 0.000000 1.000000 1.000000 1.000000 0.750000 1.000000 0.750000 0.750000 1.000000 0.500000 0.750000 1.000000 0.250000 0.750000 1.000000 0.000000 0.750000 1.000000 1.000000 0.500000 1.000000 0.750000 0.500000 1.000000 0.500000 0.500000 1.000000 0.250000 0.500000 1.000000 0.000000 0.500000 1.000000 1.000000 0.250000 1.000000 0.750000 0.250000 1.000000 0.500000 0.250000 1.000000 0.250000 0.250000 1.000000 0.000000 0.250000 1.000000 1.000000 0.000000 1.000000 0.750000 0.000000 1.000000 0.500000 0.000000 1.000000 0.250000 0.000000 1.000000 0.000000 0.000000 1.000000 1.000000 1.000000 0.750000 0.750000 1.000000 0.750000 0.500000 1.000000 0.750000 0.250000 1.000000 0.750000 0.000000 1.000000 0.750000 1.000000 0.750000 0.750000 0.750000 0.750000 0.750000 0.500000 0.750000 0.750000 0.250000 0.750000 0.750000 0.000000 0.750000 0.750000 1.000000 0.500000 0.750000 0.750000 0.500000 0.750000 0.500000 0.500000 0.750000 0.250000 0.500000 0.750000 0.000000 0.500000 0.750000 1.000000 0.250000 0.750000 0.750000 0.250000 0.750000 0.500000 0.250000 0.750000 0.250000 0.250000 0.750000 0.000000 0.250000 0.750000 1.000000 0.000000 0.750000 0.750000 0.000000 0.750000 0.500000 0.000000 0.750000 0.250000 0.000000 0.750000 0.000000 0.000000 0.750000 1.000000 1.000000 0.500000 0.750000 1.000000 0.500000 0.500000 1.000000 0.500000 0.250000 1.000000 0.500000 0.000000 1.000000 0.500000 1.000000 0.750000 0.500000 0.750000 0.750000 0.500000 0.500000 0.750000 0.500000 0.250000 0.750000 0.500000 0.000000 0.750000 0.500000 1.000000 0.500000 0.500000 0.750000 0.500000 0.500000 0.500000 0.500000 0.500000 0.250000 0.500000 0.500000 0.000000 0.500000 0.500000 1.000000 0.250000 0.500000 0.750000 0.250000 0.500000 0.500000 0.250000 0.500000 0.250000 0.250000 0.500000 0.000000 0.250000 0.500000 1.000000 0.000000 0.500000 0.750000 0.000000 0.500000 0.500000 0.000000 0.500000 0.250000 0.000000 0.500000 0.000000 0.000000 0.500000 1.000000 1.000000 0.250000 0.750000 1.000000 0.250000 0.500000 1.000000 0.250000 0.250000 1.000000 0.250000 0.000000 1.000000 0.250000 1.000000 0.750000 0.250000 0.750000 0.750000 0.250000 0.500000 0.750000 0.250000 0.250000 0.750000 0.250000 0.000000 0.750000 0.250000 1.000000 0.500000 0.250000 0.750000 0.500000 0.250000 0.500000 0.500000 0.250000 0.250000 0.500000 0.250000 0.000000 0.500000 0.250000 1.000000 0.250000 0.250000 0.750000 0.250000 0.250000 0.500000 0.250000 0.250000 0.250000 0.250000 0.250000 0.000000 0.250000 0.250000 1.000000 0.000000 0.250000 0.750000 0.000000 0.250000 0.500000 0.000000 0.250000 0.250000 0.000000 0.250000 0.000000 0.000000 0.250000 1.000000 1.000000 0.000000 0.750000 1.000000 0.000000 0.500000 1.000000 0.000000 0.250000 1.000000 0.000000 0.000000 1.000000 0.000000 1.000000 0.750000 0.000000 0.750000 0.750000 0.000000 0.500000 0.750000 0.000000 0.250000 0.750000 0.000000 0.000000 0.750000 0.000000 1.000000 0.500000 0.000000 0.750000 0.500000 0.000000 0.500000 0.500000 0.000000 0.250000 0.500000 0.000000 0.000000 0.500000 0.000000 1.000000 0.250000 0.000000 0.750000 0.250000 0.000000 0.500000 0.250000 0.000000 0.250000 0.250000 0.000000 0.000000 0.250000 0.000000 1.000000 0.000000 0.000000 0.750000 0.000000 0.000000 0.500000 0.000000 0.000000 0.250000 0.000000 0.000000 0.000000 0.000000 0.000000 ValveSoftware-gamescope-eb620ab/looks/invertcolors_pq.cube000066400000000000000000001062221502457270500241600ustar00rootroot00000000000000TITLE "Generated by Foundry::LUT" LUT_3D_SIZE 11 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 0.853310 1.000000 1.000000 0.674419 1.000000 1.000000 0.495528 1.000000 1.000000 0.316637 1.000000 1.000000 0.137746 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 0.853310 1.000000 1.000000 0.674419 1.000000 1.000000 0.495528 1.000000 1.000000 0.316637 1.000000 1.000000 0.137746 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 1.000000 0.853310 1.000000 1.000000 0.853310 1.000000 0.853310 0.853310 1.000000 0.674419 0.853310 1.000000 0.495528 0.853310 1.000000 0.316637 0.853310 1.000000 0.137746 0.853310 1.000000 0.000000 0.853310 1.000000 0.000000 0.853310 1.000000 0.000000 0.853310 1.000000 0.000000 0.853310 1.000000 1.000000 0.674419 1.000000 1.000000 0.674419 1.000000 0.853310 0.674419 1.000000 0.674419 0.674419 1.000000 0.495528 0.674419 1.000000 0.316637 0.674419 1.000000 0.137746 0.674419 1.000000 0.000000 0.674419 1.000000 0.000000 0.674419 1.000000 0.000000 0.674419 1.000000 0.000000 0.674419 1.000000 1.000000 0.495528 1.000000 1.000000 0.495528 1.000000 0.853310 0.495528 1.000000 0.674419 0.495528 1.000000 0.495528 0.495528 1.000000 0.316637 0.495528 1.000000 0.137746 0.495528 1.000000 0.000000 0.495528 1.000000 0.000000 0.495528 1.000000 0.000000 0.495528 1.000000 0.000000 0.495528 1.000000 1.000000 0.316637 1.000000 1.000000 0.316637 1.000000 0.853310 0.316637 1.000000 0.674419 0.316637 1.000000 0.495528 0.316637 1.000000 0.316637 0.316637 1.000000 0.137746 0.316637 1.000000 0.000000 0.316637 1.000000 0.000000 0.316637 1.000000 0.000000 0.316637 1.000000 0.000000 0.316637 1.000000 1.000000 0.137746 1.000000 1.000000 0.137746 1.000000 0.853310 0.137746 1.000000 0.674419 0.137746 1.000000 0.495528 0.137746 1.000000 0.316637 0.137746 1.000000 0.137746 0.137746 1.000000 0.000000 0.137746 1.000000 0.000000 0.137746 1.000000 0.000000 0.137746 1.000000 0.000000 0.137746 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 0.853310 0.000000 1.000000 0.674419 0.000000 1.000000 0.495528 0.000000 1.000000 0.316637 0.000000 1.000000 0.137746 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 0.853310 0.000000 1.000000 0.674419 0.000000 1.000000 0.495528 0.000000 1.000000 0.316637 0.000000 1.000000 0.137746 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 0.853310 0.000000 1.000000 0.674419 0.000000 1.000000 0.495528 0.000000 1.000000 0.316637 0.000000 1.000000 0.137746 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 0.853310 0.000000 1.000000 0.674419 0.000000 1.000000 0.495528 0.000000 1.000000 0.316637 0.000000 1.000000 0.137746 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 0.853310 1.000000 1.000000 0.674419 1.000000 1.000000 0.495528 1.000000 1.000000 0.316637 1.000000 1.000000 0.137746 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 0.853310 1.000000 1.000000 0.674419 1.000000 1.000000 0.495528 1.000000 1.000000 0.316637 1.000000 1.000000 0.137746 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 1.000000 0.853310 1.000000 1.000000 0.853310 1.000000 0.853310 0.853310 1.000000 0.674419 0.853310 1.000000 0.495528 0.853310 1.000000 0.316637 0.853310 1.000000 0.137746 0.853310 1.000000 0.000000 0.853310 1.000000 0.000000 0.853310 1.000000 0.000000 0.853310 1.000000 0.000000 0.853310 1.000000 1.000000 0.674419 1.000000 1.000000 0.674419 1.000000 0.853310 0.674419 1.000000 0.674419 0.674419 1.000000 0.495528 0.674419 1.000000 0.316637 0.674419 1.000000 0.137746 0.674419 1.000000 0.000000 0.674419 1.000000 0.000000 0.674419 1.000000 0.000000 0.674419 1.000000 0.000000 0.674419 1.000000 1.000000 0.495528 1.000000 1.000000 0.495528 1.000000 0.853310 0.495528 1.000000 0.674419 0.495528 1.000000 0.495528 0.495528 1.000000 0.316637 0.495528 1.000000 0.137746 0.495528 1.000000 0.000000 0.495528 1.000000 0.000000 0.495528 1.000000 0.000000 0.495528 1.000000 0.000000 0.495528 1.000000 1.000000 0.316637 1.000000 1.000000 0.316637 1.000000 0.853310 0.316637 1.000000 0.674419 0.316637 1.000000 0.495528 0.316637 1.000000 0.316637 0.316637 1.000000 0.137746 0.316637 1.000000 0.000000 0.316637 1.000000 0.000000 0.316637 1.000000 0.000000 0.316637 1.000000 0.000000 0.316637 1.000000 1.000000 0.137746 1.000000 1.000000 0.137746 1.000000 0.853310 0.137746 1.000000 0.674419 0.137746 1.000000 0.495528 0.137746 1.000000 0.316637 0.137746 1.000000 0.137746 0.137746 1.000000 0.000000 0.137746 1.000000 0.000000 0.137746 1.000000 0.000000 0.137746 1.000000 0.000000 0.137746 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 0.853310 0.000000 1.000000 0.674419 0.000000 1.000000 0.495528 0.000000 1.000000 0.316637 0.000000 1.000000 0.137746 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 0.853310 0.000000 1.000000 0.674419 0.000000 1.000000 0.495528 0.000000 1.000000 0.316637 0.000000 1.000000 0.137746 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 0.853310 0.000000 1.000000 0.674419 0.000000 1.000000 0.495528 0.000000 1.000000 0.316637 0.000000 1.000000 0.137746 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 0.853310 0.000000 1.000000 0.674419 0.000000 1.000000 0.495528 0.000000 1.000000 0.316637 0.000000 1.000000 0.137746 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 1.000000 1.000000 0.853310 1.000000 1.000000 0.853310 0.853310 1.000000 0.853310 0.674419 1.000000 0.853310 0.495528 1.000000 0.853310 0.316637 1.000000 0.853310 0.137746 1.000000 0.853310 0.000000 1.000000 0.853310 0.000000 1.000000 0.853310 0.000000 1.000000 0.853310 0.000000 1.000000 0.853310 1.000000 1.000000 0.853310 1.000000 1.000000 0.853310 0.853310 1.000000 0.853310 0.674419 1.000000 0.853310 0.495528 1.000000 0.853310 0.316637 1.000000 0.853310 0.137746 1.000000 0.853310 0.000000 1.000000 0.853310 0.000000 1.000000 0.853310 0.000000 1.000000 0.853310 0.000000 1.000000 0.853310 1.000000 0.853310 0.853310 1.000000 0.853310 0.853310 0.853310 0.853310 0.853310 0.674419 0.853310 0.853310 0.495528 0.853310 0.853310 0.316637 0.853310 0.853310 0.137746 0.853310 0.853310 0.000000 0.853310 0.853310 0.000000 0.853310 0.853310 0.000000 0.853310 0.853310 0.000000 0.853310 0.853310 1.000000 0.674419 0.853310 1.000000 0.674419 0.853310 0.853310 0.674419 0.853310 0.674419 0.674419 0.853310 0.495528 0.674419 0.853310 0.316637 0.674419 0.853310 0.137746 0.674419 0.853310 0.000000 0.674419 0.853310 0.000000 0.674419 0.853310 0.000000 0.674419 0.853310 0.000000 0.674419 0.853310 1.000000 0.495528 0.853310 1.000000 0.495528 0.853310 0.853310 0.495528 0.853310 0.674419 0.495528 0.853310 0.495528 0.495528 0.853310 0.316637 0.495528 0.853310 0.137746 0.495528 0.853310 0.000000 0.495528 0.853310 0.000000 0.495528 0.853310 0.000000 0.495528 0.853310 0.000000 0.495528 0.853310 1.000000 0.316637 0.853310 1.000000 0.316637 0.853310 0.853310 0.316637 0.853310 0.674419 0.316637 0.853310 0.495528 0.316637 0.853310 0.316637 0.316637 0.853310 0.137746 0.316637 0.853310 0.000000 0.316637 0.853310 0.000000 0.316637 0.853310 0.000000 0.316637 0.853310 0.000000 0.316637 0.853310 1.000000 0.137746 0.853310 1.000000 0.137746 0.853310 0.853310 0.137746 0.853310 0.674419 0.137746 0.853310 0.495528 0.137746 0.853310 0.316637 0.137746 0.853310 0.137746 0.137746 0.853310 0.000000 0.137746 0.853310 0.000000 0.137746 0.853310 0.000000 0.137746 0.853310 0.000000 0.137746 0.853310 1.000000 0.000000 0.853310 1.000000 0.000000 0.853310 0.853310 0.000000 0.853310 0.674419 0.000000 0.853310 0.495528 0.000000 0.853310 0.316637 0.000000 0.853310 0.137746 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 1.000000 0.000000 0.853310 1.000000 0.000000 0.853310 0.853310 0.000000 0.853310 0.674419 0.000000 0.853310 0.495528 0.000000 0.853310 0.316637 0.000000 0.853310 0.137746 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 1.000000 0.000000 0.853310 1.000000 0.000000 0.853310 0.853310 0.000000 0.853310 0.674419 0.000000 0.853310 0.495528 0.000000 0.853310 0.316637 0.000000 0.853310 0.137746 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 1.000000 0.000000 0.853310 1.000000 0.000000 0.853310 0.853310 0.000000 0.853310 0.674419 0.000000 0.853310 0.495528 0.000000 0.853310 0.316637 0.000000 0.853310 0.137746 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 1.000000 1.000000 0.674419 1.000000 1.000000 0.674419 0.853310 1.000000 0.674419 0.674419 1.000000 0.674419 0.495528 1.000000 0.674419 0.316637 1.000000 0.674419 0.137746 1.000000 0.674419 0.000000 1.000000 0.674419 0.000000 1.000000 0.674419 0.000000 1.000000 0.674419 0.000000 1.000000 0.674419 1.000000 1.000000 0.674419 1.000000 1.000000 0.674419 0.853310 1.000000 0.674419 0.674419 1.000000 0.674419 0.495528 1.000000 0.674419 0.316637 1.000000 0.674419 0.137746 1.000000 0.674419 0.000000 1.000000 0.674419 0.000000 1.000000 0.674419 0.000000 1.000000 0.674419 0.000000 1.000000 0.674419 1.000000 0.853310 0.674419 1.000000 0.853310 0.674419 0.853310 0.853310 0.674419 0.674419 0.853310 0.674419 0.495528 0.853310 0.674419 0.316637 0.853310 0.674419 0.137746 0.853310 0.674419 0.000000 0.853310 0.674419 0.000000 0.853310 0.674419 0.000000 0.853310 0.674419 0.000000 0.853310 0.674419 1.000000 0.674419 0.674419 1.000000 0.674419 0.674419 0.853310 0.674419 0.674419 0.674419 0.674419 0.674419 0.495528 0.674419 0.674419 0.316637 0.674419 0.674419 0.137746 0.674419 0.674419 0.000000 0.674419 0.674419 0.000000 0.674419 0.674419 0.000000 0.674419 0.674419 0.000000 0.674419 0.674419 1.000000 0.495528 0.674419 1.000000 0.495528 0.674419 0.853310 0.495528 0.674419 0.674419 0.495528 0.674419 0.495528 0.495528 0.674419 0.316637 0.495528 0.674419 0.137746 0.495528 0.674419 0.000000 0.495528 0.674419 0.000000 0.495528 0.674419 0.000000 0.495528 0.674419 0.000000 0.495528 0.674419 1.000000 0.316637 0.674419 1.000000 0.316637 0.674419 0.853310 0.316637 0.674419 0.674419 0.316637 0.674419 0.495528 0.316637 0.674419 0.316637 0.316637 0.674419 0.137746 0.316637 0.674419 0.000000 0.316637 0.674419 0.000000 0.316637 0.674419 0.000000 0.316637 0.674419 0.000000 0.316637 0.674419 1.000000 0.137746 0.674419 1.000000 0.137746 0.674419 0.853310 0.137746 0.674419 0.674419 0.137746 0.674419 0.495528 0.137746 0.674419 0.316637 0.137746 0.674419 0.137746 0.137746 0.674419 0.000000 0.137746 0.674419 0.000000 0.137746 0.674419 0.000000 0.137746 0.674419 0.000000 0.137746 0.674419 1.000000 0.000000 0.674419 1.000000 0.000000 0.674419 0.853310 0.000000 0.674419 0.674419 0.000000 0.674419 0.495528 0.000000 0.674419 0.316637 0.000000 0.674419 0.137746 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 1.000000 0.000000 0.674419 1.000000 0.000000 0.674419 0.853310 0.000000 0.674419 0.674419 0.000000 0.674419 0.495528 0.000000 0.674419 0.316637 0.000000 0.674419 0.137746 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 1.000000 0.000000 0.674419 1.000000 0.000000 0.674419 0.853310 0.000000 0.674419 0.674419 0.000000 0.674419 0.495528 0.000000 0.674419 0.316637 0.000000 0.674419 0.137746 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 1.000000 0.000000 0.674419 1.000000 0.000000 0.674419 0.853310 0.000000 0.674419 0.674419 0.000000 0.674419 0.495528 0.000000 0.674419 0.316637 0.000000 0.674419 0.137746 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 1.000000 1.000000 0.495528 1.000000 1.000000 0.495528 0.853310 1.000000 0.495528 0.674419 1.000000 0.495528 0.495528 1.000000 0.495528 0.316637 1.000000 0.495528 0.137746 1.000000 0.495528 0.000000 1.000000 0.495528 0.000000 1.000000 0.495528 0.000000 1.000000 0.495528 0.000000 1.000000 0.495528 1.000000 1.000000 0.495528 1.000000 1.000000 0.495528 0.853310 1.000000 0.495528 0.674419 1.000000 0.495528 0.495528 1.000000 0.495528 0.316637 1.000000 0.495528 0.137746 1.000000 0.495528 0.000000 1.000000 0.495528 0.000000 1.000000 0.495528 0.000000 1.000000 0.495528 0.000000 1.000000 0.495528 1.000000 0.853310 0.495528 1.000000 0.853310 0.495528 0.853310 0.853310 0.495528 0.674419 0.853310 0.495528 0.495528 0.853310 0.495528 0.316637 0.853310 0.495528 0.137746 0.853310 0.495528 0.000000 0.853310 0.495528 0.000000 0.853310 0.495528 0.000000 0.853310 0.495528 0.000000 0.853310 0.495528 1.000000 0.674419 0.495528 1.000000 0.674419 0.495528 0.853310 0.674419 0.495528 0.674419 0.674419 0.495528 0.495528 0.674419 0.495528 0.316637 0.674419 0.495528 0.137746 0.674419 0.495528 0.000000 0.674419 0.495528 0.000000 0.674419 0.495528 0.000000 0.674419 0.495528 0.000000 0.674419 0.495528 1.000000 0.495528 0.495528 1.000000 0.495528 0.495528 0.853310 0.495528 0.495528 0.674419 0.495528 0.495528 0.495528 0.495528 0.495528 0.316637 0.495528 0.495528 0.137746 0.495528 0.495528 0.000000 0.495528 0.495528 0.000000 0.495528 0.495528 0.000000 0.495528 0.495528 0.000000 0.495528 0.495528 1.000000 0.316637 0.495528 1.000000 0.316637 0.495528 0.853310 0.316637 0.495528 0.674419 0.316637 0.495528 0.495528 0.316637 0.495528 0.316637 0.316637 0.495528 0.137746 0.316637 0.495528 0.000000 0.316637 0.495528 0.000000 0.316637 0.495528 0.000000 0.316637 0.495528 0.000000 0.316637 0.495528 1.000000 0.137746 0.495528 1.000000 0.137746 0.495528 0.853310 0.137746 0.495528 0.674419 0.137746 0.495528 0.495528 0.137746 0.495528 0.316637 0.137746 0.495528 0.137746 0.137746 0.495528 0.000000 0.137746 0.495528 0.000000 0.137746 0.495528 0.000000 0.137746 0.495528 0.000000 0.137746 0.495528 1.000000 0.000000 0.495528 1.000000 0.000000 0.495528 0.853310 0.000000 0.495528 0.674419 0.000000 0.495528 0.495528 0.000000 0.495528 0.316637 0.000000 0.495528 0.137746 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 1.000000 0.000000 0.495528 1.000000 0.000000 0.495528 0.853310 0.000000 0.495528 0.674419 0.000000 0.495528 0.495528 0.000000 0.495528 0.316637 0.000000 0.495528 0.137746 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 1.000000 0.000000 0.495528 1.000000 0.000000 0.495528 0.853310 0.000000 0.495528 0.674419 0.000000 0.495528 0.495528 0.000000 0.495528 0.316637 0.000000 0.495528 0.137746 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 1.000000 0.000000 0.495528 1.000000 0.000000 0.495528 0.853310 0.000000 0.495528 0.674419 0.000000 0.495528 0.495528 0.000000 0.495528 0.316637 0.000000 0.495528 0.137746 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 1.000000 1.000000 0.316637 1.000000 1.000000 0.316637 0.853310 1.000000 0.316637 0.674419 1.000000 0.316637 0.495528 1.000000 0.316637 0.316637 1.000000 0.316637 0.137746 1.000000 0.316637 0.000000 1.000000 0.316637 0.000000 1.000000 0.316637 0.000000 1.000000 0.316637 0.000000 1.000000 0.316637 1.000000 1.000000 0.316637 1.000000 1.000000 0.316637 0.853310 1.000000 0.316637 0.674419 1.000000 0.316637 0.495528 1.000000 0.316637 0.316637 1.000000 0.316637 0.137746 1.000000 0.316637 0.000000 1.000000 0.316637 0.000000 1.000000 0.316637 0.000000 1.000000 0.316637 0.000000 1.000000 0.316637 1.000000 0.853310 0.316637 1.000000 0.853310 0.316637 0.853310 0.853310 0.316637 0.674419 0.853310 0.316637 0.495528 0.853310 0.316637 0.316637 0.853310 0.316637 0.137746 0.853310 0.316637 0.000000 0.853310 0.316637 0.000000 0.853310 0.316637 0.000000 0.853310 0.316637 0.000000 0.853310 0.316637 1.000000 0.674419 0.316637 1.000000 0.674419 0.316637 0.853310 0.674419 0.316637 0.674419 0.674419 0.316637 0.495528 0.674419 0.316637 0.316637 0.674419 0.316637 0.137746 0.674419 0.316637 0.000000 0.674419 0.316637 0.000000 0.674419 0.316637 0.000000 0.674419 0.316637 0.000000 0.674419 0.316637 1.000000 0.495528 0.316637 1.000000 0.495528 0.316637 0.853310 0.495528 0.316637 0.674419 0.495528 0.316637 0.495528 0.495528 0.316637 0.316637 0.495528 0.316637 0.137746 0.495528 0.316637 0.000000 0.495528 0.316637 0.000000 0.495528 0.316637 0.000000 0.495528 0.316637 0.000000 0.495528 0.316637 1.000000 0.316637 0.316637 1.000000 0.316637 0.316637 0.853310 0.316637 0.316637 0.674419 0.316637 0.316637 0.495528 0.316637 0.316637 0.316637 0.316637 0.316637 0.137746 0.316637 0.316637 0.000000 0.316637 0.316637 0.000000 0.316637 0.316637 0.000000 0.316637 0.316637 0.000000 0.316637 0.316637 1.000000 0.137746 0.316637 1.000000 0.137746 0.316637 0.853310 0.137746 0.316637 0.674419 0.137746 0.316637 0.495528 0.137746 0.316637 0.316637 0.137746 0.316637 0.137746 0.137746 0.316637 0.000000 0.137746 0.316637 0.000000 0.137746 0.316637 0.000000 0.137746 0.316637 0.000000 0.137746 0.316637 1.000000 0.000000 0.316637 1.000000 0.000000 0.316637 0.853310 0.000000 0.316637 0.674419 0.000000 0.316637 0.495528 0.000000 0.316637 0.316637 0.000000 0.316637 0.137746 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 1.000000 0.000000 0.316637 1.000000 0.000000 0.316637 0.853310 0.000000 0.316637 0.674419 0.000000 0.316637 0.495528 0.000000 0.316637 0.316637 0.000000 0.316637 0.137746 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 1.000000 0.000000 0.316637 1.000000 0.000000 0.316637 0.853310 0.000000 0.316637 0.674419 0.000000 0.316637 0.495528 0.000000 0.316637 0.316637 0.000000 0.316637 0.137746 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 1.000000 0.000000 0.316637 1.000000 0.000000 0.316637 0.853310 0.000000 0.316637 0.674419 0.000000 0.316637 0.495528 0.000000 0.316637 0.316637 0.000000 0.316637 0.137746 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 1.000000 1.000000 0.137746 1.000000 1.000000 0.137746 0.853310 1.000000 0.137746 0.674419 1.000000 0.137746 0.495528 1.000000 0.137746 0.316637 1.000000 0.137746 0.137746 1.000000 0.137746 0.000000 1.000000 0.137746 0.000000 1.000000 0.137746 0.000000 1.000000 0.137746 0.000000 1.000000 0.137746 1.000000 1.000000 0.137746 1.000000 1.000000 0.137746 0.853310 1.000000 0.137746 0.674419 1.000000 0.137746 0.495528 1.000000 0.137746 0.316637 1.000000 0.137746 0.137746 1.000000 0.137746 0.000000 1.000000 0.137746 0.000000 1.000000 0.137746 0.000000 1.000000 0.137746 0.000000 1.000000 0.137746 1.000000 0.853310 0.137746 1.000000 0.853310 0.137746 0.853310 0.853310 0.137746 0.674419 0.853310 0.137746 0.495528 0.853310 0.137746 0.316637 0.853310 0.137746 0.137746 0.853310 0.137746 0.000000 0.853310 0.137746 0.000000 0.853310 0.137746 0.000000 0.853310 0.137746 0.000000 0.853310 0.137746 1.000000 0.674419 0.137746 1.000000 0.674419 0.137746 0.853310 0.674419 0.137746 0.674419 0.674419 0.137746 0.495528 0.674419 0.137746 0.316637 0.674419 0.137746 0.137746 0.674419 0.137746 0.000000 0.674419 0.137746 0.000000 0.674419 0.137746 0.000000 0.674419 0.137746 0.000000 0.674419 0.137746 1.000000 0.495528 0.137746 1.000000 0.495528 0.137746 0.853310 0.495528 0.137746 0.674419 0.495528 0.137746 0.495528 0.495528 0.137746 0.316637 0.495528 0.137746 0.137746 0.495528 0.137746 0.000000 0.495528 0.137746 0.000000 0.495528 0.137746 0.000000 0.495528 0.137746 0.000000 0.495528 0.137746 1.000000 0.316637 0.137746 1.000000 0.316637 0.137746 0.853310 0.316637 0.137746 0.674419 0.316637 0.137746 0.495528 0.316637 0.137746 0.316637 0.316637 0.137746 0.137746 0.316637 0.137746 0.000000 0.316637 0.137746 0.000000 0.316637 0.137746 0.000000 0.316637 0.137746 0.000000 0.316637 0.137746 1.000000 0.137746 0.137746 1.000000 0.137746 0.137746 0.853310 0.137746 0.137746 0.674419 0.137746 0.137746 0.495528 0.137746 0.137746 0.316637 0.137746 0.137746 0.137746 0.137746 0.137746 0.000000 0.137746 0.137746 0.000000 0.137746 0.137746 0.000000 0.137746 0.137746 0.000000 0.137746 0.137746 1.000000 0.000000 0.137746 1.000000 0.000000 0.137746 0.853310 0.000000 0.137746 0.674419 0.000000 0.137746 0.495528 0.000000 0.137746 0.316637 0.000000 0.137746 0.137746 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 1.000000 0.000000 0.137746 1.000000 0.000000 0.137746 0.853310 0.000000 0.137746 0.674419 0.000000 0.137746 0.495528 0.000000 0.137746 0.316637 0.000000 0.137746 0.137746 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 1.000000 0.000000 0.137746 1.000000 0.000000 0.137746 0.853310 0.000000 0.137746 0.674419 0.000000 0.137746 0.495528 0.000000 0.137746 0.316637 0.000000 0.137746 0.137746 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 1.000000 0.000000 0.137746 1.000000 0.000000 0.137746 0.853310 0.000000 0.137746 0.674419 0.000000 0.137746 0.495528 0.000000 0.137746 0.316637 0.000000 0.137746 0.137746 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 0.853310 1.000000 0.000000 0.674419 1.000000 0.000000 0.495528 1.000000 0.000000 0.316637 1.000000 0.000000 0.137746 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 0.853310 1.000000 0.000000 0.674419 1.000000 0.000000 0.495528 1.000000 0.000000 0.316637 1.000000 0.000000 0.137746 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 1.000000 0.853310 0.000000 1.000000 0.853310 0.000000 0.853310 0.853310 0.000000 0.674419 0.853310 0.000000 0.495528 0.853310 0.000000 0.316637 0.853310 0.000000 0.137746 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 1.000000 0.674419 0.000000 1.000000 0.674419 0.000000 0.853310 0.674419 0.000000 0.674419 0.674419 0.000000 0.495528 0.674419 0.000000 0.316637 0.674419 0.000000 0.137746 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 1.000000 0.495528 0.000000 1.000000 0.495528 0.000000 0.853310 0.495528 0.000000 0.674419 0.495528 0.000000 0.495528 0.495528 0.000000 0.316637 0.495528 0.000000 0.137746 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 1.000000 0.316637 0.000000 1.000000 0.316637 0.000000 0.853310 0.316637 0.000000 0.674419 0.316637 0.000000 0.495528 0.316637 0.000000 0.316637 0.316637 0.000000 0.137746 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 1.000000 0.137746 0.000000 1.000000 0.137746 0.000000 0.853310 0.137746 0.000000 0.674419 0.137746 0.000000 0.495528 0.137746 0.000000 0.316637 0.137746 0.000000 0.137746 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.853310 0.000000 0.000000 0.674419 0.000000 0.000000 0.495528 0.000000 0.000000 0.316637 0.000000 0.000000 0.137746 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.853310 0.000000 0.000000 0.674419 0.000000 0.000000 0.495528 0.000000 0.000000 0.316637 0.000000 0.000000 0.137746 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.853310 0.000000 0.000000 0.674419 0.000000 0.000000 0.495528 0.000000 0.000000 0.316637 0.000000 0.000000 0.137746 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.853310 0.000000 0.000000 0.674419 0.000000 0.000000 0.495528 0.000000 0.000000 0.316637 0.000000 0.000000 0.137746 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 0.853310 1.000000 0.000000 0.674419 1.000000 0.000000 0.495528 1.000000 0.000000 0.316637 1.000000 0.000000 0.137746 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 0.853310 1.000000 0.000000 0.674419 1.000000 0.000000 0.495528 1.000000 0.000000 0.316637 1.000000 0.000000 0.137746 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 1.000000 0.853310 0.000000 1.000000 0.853310 0.000000 0.853310 0.853310 0.000000 0.674419 0.853310 0.000000 0.495528 0.853310 0.000000 0.316637 0.853310 0.000000 0.137746 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 1.000000 0.674419 0.000000 1.000000 0.674419 0.000000 0.853310 0.674419 0.000000 0.674419 0.674419 0.000000 0.495528 0.674419 0.000000 0.316637 0.674419 0.000000 0.137746 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 1.000000 0.495528 0.000000 1.000000 0.495528 0.000000 0.853310 0.495528 0.000000 0.674419 0.495528 0.000000 0.495528 0.495528 0.000000 0.316637 0.495528 0.000000 0.137746 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 1.000000 0.316637 0.000000 1.000000 0.316637 0.000000 0.853310 0.316637 0.000000 0.674419 0.316637 0.000000 0.495528 0.316637 0.000000 0.316637 0.316637 0.000000 0.137746 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 1.000000 0.137746 0.000000 1.000000 0.137746 0.000000 0.853310 0.137746 0.000000 0.674419 0.137746 0.000000 0.495528 0.137746 0.000000 0.316637 0.137746 0.000000 0.137746 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.853310 0.000000 0.000000 0.674419 0.000000 0.000000 0.495528 0.000000 0.000000 0.316637 0.000000 0.000000 0.137746 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.853310 0.000000 0.000000 0.674419 0.000000 0.000000 0.495528 0.000000 0.000000 0.316637 0.000000 0.000000 0.137746 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.853310 0.000000 0.000000 0.674419 0.000000 0.000000 0.495528 0.000000 0.000000 0.316637 0.000000 0.000000 0.137746 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.853310 0.000000 0.000000 0.674419 0.000000 0.000000 0.495528 0.000000 0.000000 0.316637 0.000000 0.000000 0.137746 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 0.853310 1.000000 0.000000 0.674419 1.000000 0.000000 0.495528 1.000000 0.000000 0.316637 1.000000 0.000000 0.137746 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 0.853310 1.000000 0.000000 0.674419 1.000000 0.000000 0.495528 1.000000 0.000000 0.316637 1.000000 0.000000 0.137746 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 1.000000 0.853310 0.000000 1.000000 0.853310 0.000000 0.853310 0.853310 0.000000 0.674419 0.853310 0.000000 0.495528 0.853310 0.000000 0.316637 0.853310 0.000000 0.137746 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 1.000000 0.674419 0.000000 1.000000 0.674419 0.000000 0.853310 0.674419 0.000000 0.674419 0.674419 0.000000 0.495528 0.674419 0.000000 0.316637 0.674419 0.000000 0.137746 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 1.000000 0.495528 0.000000 1.000000 0.495528 0.000000 0.853310 0.495528 0.000000 0.674419 0.495528 0.000000 0.495528 0.495528 0.000000 0.316637 0.495528 0.000000 0.137746 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 1.000000 0.316637 0.000000 1.000000 0.316637 0.000000 0.853310 0.316637 0.000000 0.674419 0.316637 0.000000 0.495528 0.316637 0.000000 0.316637 0.316637 0.000000 0.137746 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 1.000000 0.137746 0.000000 1.000000 0.137746 0.000000 0.853310 0.137746 0.000000 0.674419 0.137746 0.000000 0.495528 0.137746 0.000000 0.316637 0.137746 0.000000 0.137746 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.853310 0.000000 0.000000 0.674419 0.000000 0.000000 0.495528 0.000000 0.000000 0.316637 0.000000 0.000000 0.137746 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.853310 0.000000 0.000000 0.674419 0.000000 0.000000 0.495528 0.000000 0.000000 0.316637 0.000000 0.000000 0.137746 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.853310 0.000000 0.000000 0.674419 0.000000 0.000000 0.495528 0.000000 0.000000 0.316637 0.000000 0.000000 0.137746 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.853310 0.000000 0.000000 0.674419 0.000000 0.000000 0.495528 0.000000 0.000000 0.316637 0.000000 0.000000 0.137746 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 0.853310 1.000000 0.000000 0.674419 1.000000 0.000000 0.495528 1.000000 0.000000 0.316637 1.000000 0.000000 0.137746 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 1.000000 1.000000 0.000000 1.000000 1.000000 0.000000 0.853310 1.000000 0.000000 0.674419 1.000000 0.000000 0.495528 1.000000 0.000000 0.316637 1.000000 0.000000 0.137746 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 1.000000 0.853310 0.000000 1.000000 0.853310 0.000000 0.853310 0.853310 0.000000 0.674419 0.853310 0.000000 0.495528 0.853310 0.000000 0.316637 0.853310 0.000000 0.137746 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 0.000000 0.853310 0.000000 1.000000 0.674419 0.000000 1.000000 0.674419 0.000000 0.853310 0.674419 0.000000 0.674419 0.674419 0.000000 0.495528 0.674419 0.000000 0.316637 0.674419 0.000000 0.137746 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 0.000000 0.674419 0.000000 1.000000 0.495528 0.000000 1.000000 0.495528 0.000000 0.853310 0.495528 0.000000 0.674419 0.495528 0.000000 0.495528 0.495528 0.000000 0.316637 0.495528 0.000000 0.137746 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 0.000000 0.495528 0.000000 1.000000 0.316637 0.000000 1.000000 0.316637 0.000000 0.853310 0.316637 0.000000 0.674419 0.316637 0.000000 0.495528 0.316637 0.000000 0.316637 0.316637 0.000000 0.137746 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 0.000000 0.316637 0.000000 1.000000 0.137746 0.000000 1.000000 0.137746 0.000000 0.853310 0.137746 0.000000 0.674419 0.137746 0.000000 0.495528 0.137746 0.000000 0.316637 0.137746 0.000000 0.137746 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 0.000000 0.137746 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.853310 0.000000 0.000000 0.674419 0.000000 0.000000 0.495528 0.000000 0.000000 0.316637 0.000000 0.000000 0.137746 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.853310 0.000000 0.000000 0.674419 0.000000 0.000000 0.495528 0.000000 0.000000 0.316637 0.000000 0.000000 0.137746 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.853310 0.000000 0.000000 0.674419 0.000000 0.000000 0.495528 0.000000 0.000000 0.316637 0.000000 0.000000 0.137746 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.853310 0.000000 0.000000 0.674419 0.000000 0.000000 0.495528 0.000000 0.000000 0.316637 0.000000 0.000000 0.137746 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ValveSoftware-gamescope-eb620ab/meson.build000066400000000000000000000065611502457270500211070ustar00rootroot00000000000000project( 'gamescope', 'c', 'cpp', meson_version: '>=0.58.0', default_options: [ 'cpp_std=c++20', 'warning_level=2', 'force_fallback_for=wlroots,libliftoff,vkroots', ], ) fallbacks = get_option('force_fallback_for') if not (fallbacks.contains('wlroots') and fallbacks.contains('libliftoff') and fallbacks.contains('vkroots')) error('!!!"force_fallback_for" is missing entries!!!\n\tPlease do not remove entries from force_fallback_for if you are packaging the project.\n\tWe pull in these projects at specific commits/forks/builds for a reason.\n\tIf you are not packaging, remove this line to continue.') endif add_project_arguments([ '-DWLR_USE_UNSTABLE', ], language: 'cpp') cppc = meson.get_compiler('cpp') data_dir = get_option('datadir') prefix = get_option('prefix') lib_dir = get_option('libdir') add_project_arguments(cppc.get_supported_arguments([ '-Wno-unused-parameter', '-Wno-missing-field-initializers', '-Wno-c99-designator', '-Wno-invalid-offsetof', '-Wno-unused-const-variable', '-Wno-volatile', # glm warning '-Wno-deprecated-volatile', '-Wno-ignored-qualifiers', # reshade warning '-Wno-missing-braces', ]), language: 'cpp') add_project_arguments(cppc.get_supported_arguments([ '-fno-exceptions', '-ffast-math', ]), language: 'cpp') pipewire_dep = dependency('libpipewire-0.3', required: get_option('pipewire')) librt_dep = cppc.find_library('rt', required : get_option('pipewire')) hwdata_dep = dependency('hwdata', required : false) dep_x11 = dependency('x11') dep_wayland = dependency('wayland-client') vulkan_dep = dependency('vulkan') glm_proj = subproject('glm') glm_dep = glm_proj.get_variable('glm_dep') stb_proj = subproject('stb') stb_dep = stb_proj.get_variable('stb_dep') if get_option('enable_openvr_support') openvr_dep = dependency('openvr', version: '>= 2.7', required : false) if not openvr_dep.found() cmake = import('cmake') openvr_var = cmake.subproject_options() openvr_var.add_cmake_defines({'USE_LIBCXX': false, #HACK: remove me when openvr supports CMake 4.0 'CMAKE_POLICY_VERSION_MINIMUM': '3.5'}) #ENDHACK openvr_var.set_override_option('warning_level', '0') openvr_proj = cmake.subproject('openvr', options : openvr_var) openvr_dep = openvr_proj.dependency('openvr_api') endif else # null dep openvr_dep = dependency('', required : false) endif add_project_arguments( '-DHAVE_PIPEWIRE=@0@'.format(pipewire_dep.found().to_int()), '-DHAVE_OPENVR=@0@'.format(openvr_dep.found().to_int()), '-DSCRIPT_DIR="@0@"'.format(prefix / data_dir / 'gamescope/scripts'), language: 'cpp', ) if hwdata_dep.found() add_project_arguments( '-DHWDATA_PNP_IDS="@0@"'.format(hwdata_dep.get_variable('pkgdatadir') / 'pnp.ids'), language: 'cpp', ) else warning('Building without hwdata pnp id support.') endif # Vulkan headers are installed separately from the loader (which ships the # pkg-config file) if not cppc.check_header('vulkan/vulkan.h', dependencies: vulkan_dep) error('Missing vulkan-headers') endif subdir('protocol') if get_option('enable_gamescope_wsi_layer') subdir('layer') endif if get_option('enable_gamescope') subdir('src') endif # Handle default script/config/looks stuff meson.add_install_script('default_extras_install.sh') devenv = environment() devenv.set('GAMESCOPE_SCRIPT_PATH', join_paths(meson.current_source_dir(), 'scripts')) meson.add_devenv(devenv) ValveSoftware-gamescope-eb620ab/meson_options.txt000066400000000000000000000017541502457270500224010ustar00rootroot00000000000000option('pipewire', type: 'feature', description: 'Screen capture via PipeWire') option('rt_cap', type: 'feature', description: 'Support for creating real-time threads + compute queues') option('drm_backend', type: 'feature', description: 'DRM Atomic Backend') option('sdl2_backend', type: 'feature', description: 'SDL2 Window Backend') option('avif_screenshots', type: 'feature', description: 'Support for saving .AVIF HDR screenshots') option('input_emulation' , type: 'feature', description: 'Support for XTest/Input Emulation with libei') option('enable_gamescope', type : 'boolean', value : true, description: 'Build Gamescope executable') option('enable_gamescope_wsi_layer', type : 'boolean', value : true, description: 'Build Gamescope layer') option('enable_openvr_support', type : 'boolean', value : true, description: 'OpenVR Integrations') option('benchmark', type: 'feature', description: 'Benchmark tools') ValveSoftware-gamescope-eb620ab/protocol/000077500000000000000000000000001502457270500205765ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/protocol/color-management-v1.xml000066400000000000000000002221321502457270500250760ustar00rootroot00000000000000 Copyright 2019 Sebastian Wick Copyright 2019 Erwin Burema Copyright 2020 AMD Copyright 2020-2024 Collabora, Ltd. Copyright 2024 Xaver Hugl Copyright 2022-2025 Red Hat, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. The aim of the color management extension is to allow clients to know the color properties of outputs, and to tell the compositor about the color properties of their content on surfaces. Doing this enables a compositor to perform automatic color management of content for different outputs according to how content is intended to look like. The color properties are represented as an image description object which is immutable after it has been created. A wl_output always has an associated image description that clients can observe. A wl_surface always has an associated preferred image description as a hint chosen by the compositor that clients can also observe. Clients can set an image description on a wl_surface to denote the color characteristics of the surface contents. An image description includes SDR and HDR colorimetry and encoding, HDR metadata, and viewing environment parameters. An image description does not include the properties set through color-representation extension. It is expected that the color-representation extension is used in conjunction with the color management extension when necessary, particularly with the YUV family of pixel formats. Recommendation ITU-T H.273 "Coding-independent code points for video signal type identification" shall be referred to as simply H.273 here. The color-and-hdr repository (https://gitlab.freedesktop.org/pq/color-and-hdr) contains background information on the protocol design and legacy color management. It also contains a glossary, learning resources for digital color, tools, samples and more. The terminology used in this protocol is based on common color science and color encoding terminology where possible. The glossary in the color-and-hdr repository shall be the authority on the definition of terms in this protocol. Warning! The protocol described in this file is currently in the testing phase. Backward compatible changes may be added together with the corresponding interface version bump. Backward incompatible changes can only be done by creating a new major version of the extension. A singleton global interface used for getting color management extensions for wl_surface and wl_output objects, and for creating client defined image description objects. The extension interfaces allow getting the image description of outputs and setting the image description of surfaces. Compositors should never remove this global. Destroy the wp_color_manager_v1 object. This does not affect any other objects in any way. See the ICC.1:2022 specification from the International Color Consortium for more details about rendering intents. The principles of ICC defined rendering intents apply with all types of image descriptions, not only those with ICC file profiles. Compositors must support the perceptual rendering intent. Other rendering intents are optional. The compositor supports set_mastering_display_primaries request with a target color volume fully contained inside the primary color volume. The compositor additionally supports target color volumes that extend outside of the primary color volume. This can only be advertised if feature set_mastering_display_primaries is supported as well. Named color primaries used to encode well-known sets of primaries. H.273 is the authority, when it comes to the exact values of primaries and authoritative specifications, where an equivalent code point exists. A value of 0 is invalid and will never be present in the list of enums. Descriptions do list the specifications for convenience. Color primaries as defined by - Rec. ITU-R BT.709-6 - Rec. ITU-R BT.1361-0 conventional colour gamut system and extended colour gamut system (historical) - IEC 61966-2-1 sRGB or sYCC - IEC 61966-2-4 - Society of Motion Picture and Television Engineers (SMPTE) RP 177 (1993) Annex B Equivalent to H.273 ColourPrimaries code point 1. Color primaries as defined by - Rec. ITU-R BT.470-6 System M (historical) - United States National Television System Committee 1953 Recommendation for transmission standards for color television - United States Federal Communications Commission (2003) Title 47 Code of Federal Regulations 73.682 (a)(20) Equivalent to H.273 ColourPrimaries code point 4. Color primaries as defined by - Rec. ITU-R BT.470-6 System B, G (historical) - Rec. ITU-R BT.601-7 625 - Rec. ITU-R BT.1358-0 625 (historical) - Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM Equivalent to H.273 ColourPrimaries code point 5. Color primaries as defined by - Rec. ITU-R BT.601-7 525 - Rec. ITU-R BT.1358-1 525 or 625 (historical) - Rec. ITU-R BT.1700-0 NTSC - SMPTE 170M (2004) - SMPTE 240M (1999) (historical) Equivalent to H.273 ColourPrimaries code point 6 and 7. Color primaries as defined by H.273 for generic film. Equivalent to H.273 ColourPrimaries code point 8. Color primaries as defined by - Rec. ITU-R BT.2020-2 - Rec. ITU-R BT.2100-0 Equivalent to H.273 ColourPrimaries code point 9. Color primaries as defined as the maximum of the CIE 1931 XYZ color space by - SMPTE ST 428-1 - (CIE 1931 XYZ as in ISO 11664-1) Equivalent to H.273 ColourPrimaries code point 10. Color primaries as defined by Digital Cinema System and published in SMPTE RP 431-2 (2011). Equivalent to H.273 ColourPrimaries code point 11. Color primaries as defined by Digital Cinema System and published in SMPTE EG 432-1 (2010). Equivalent to H.273 ColourPrimaries code point 12. Color primaries as defined by Adobe as "Adobe RGB" and later published by ISO 12640-4 (2011). Named transfer functions used to represent well-known transfer characteristics. H.273 is the authority, when it comes to the exact formulas and authoritative specifications, where an equivalent code point exists. A value of 0 is invalid and will never be present in the list of enums. Descriptions do list the specifications for convenience. Rec. ITU-R BT.1886 is the display transfer characteristic assumed by - Rec. ITU-R BT.601-7 525 and 625 - Rec. ITU-R BT.709-6 - Rec. ITU-R BT.2020-2 These recommendations are referred to by H.273 TransferCharacteristics code points 1, 6, 14, and 15, which are all equivalent. This TF implies these default luminances from Rec. ITU-R BT.2035: - primary color volume minimum: 0.01 cd/m² - primary color volume maximum: 100 cd/m² - reference white: 100 cd/m² Transfer characteristics as defined by - Rec. ITU-R BT.470-6 System M (historical) - United States National Television System Committee 1953 Recommendation for transmission standards for color television - United States Federal Communications Commission (2003) Title 47 Code of Federal Regulations 73.682 (a) (20) - Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM Equivalent to H.273 TransferCharacteristics code point 4. Transfer characteristics as defined by - Rec. ITU-R BT.470-6 System B, G (historical) Equivalent to H.273 TransferCharacteristics code point 5. Transfer characteristics as defined by - SMPTE ST 240 (1999) Equivalent to H.273 TransferCharacteristics code point 7. Linear transfer function defined over all real numbers. Normalised electrical values are equal the normalised optical values. The differences to H.273 TransferCharacteristics code point 8 are the definition over all real numbers. Logarithmic transfer characteristic (100:1 range). Equivalent to H.273 TransferCharacteristics code point 9. Logarithmic transfer characteristic (100 * Sqrt(10) : 1 range). Equivalent to H.273 TransferCharacteristics code point 10. Transfer characteristics as defined by - IEC 61966-2-4 Equivalent to H.273 TransferCharacteristics code point 11. Transfer characteristics as defined by - IEC 61966-2-1 sRGB Equivalent to H.273 TransferCharacteristics code point 13 with MatrixCoefficients set to 0. Transfer characteristics as defined by - IEC 61966-2-1 sYCC Equivalent to H.273 TransferCharacteristics code point 13 with MatrixCoefficients set to anything but 0. Transfer characteristics as defined by - SMPTE ST 2084 (2014) for 10-, 12-, 14- and 16-bit systems - Rec. ITU-R BT.2100-2 perceptual quantization (PQ) system Equivalent to H.273 TransferCharacteristics code point 16. This TF implies these default luminances - primary color volume minimum: 0.005 cd/m² - primary color volume maximum: 10000 cd/m² - reference white: 203 cd/m² The difference between the primary color volume minimum and maximum must be approximately 10000 cd/m² as that is the swing of the EOTF defined by ST 2084 and BT.2100. The default value for the reference white is a protocol addition: it is suggested by Report ITU-R BT.2408-7 and is not part of ST 2084 or BT.2100. Transfer characteristics as defined by - SMPTE ST 428-1 (2019) Equivalent to H.273 TransferCharacteristics code point 17. Transfer characteristics as defined by - ARIB STD-B67 (2015) - Rec. ITU-R BT.2100-2 hybrid log-gamma (HLG) system Equivalent to H.273 TransferCharacteristics code point 18. This TF implies these default luminances - primary color volume minimum: 0.005 cd/m² - primary color volume maximum: 1000 cd/m² - reference white: 203 cd/m² HLG is a relative display-referred signal with a specified non-linear mapping to the display peak luminance (the HLG OOTF). All absolute luminance values used here for HLG assume a 1000 cd/m² peak display. The default value for the reference white is a protocol addition: it is suggested by Report ITU-R BT.2408-7 and is not part of ARIB STD-B67 or BT.2100. This creates a new wp_color_management_output_v1 object for the given wl_output. See the wp_color_management_output_v1 interface for more details. If a wp_color_management_surface_v1 object already exists for the given wl_surface, the protocol error surface_exists is raised. This creates a new color wp_color_management_surface_v1 object for the given wl_surface. See the wp_color_management_surface_v1 interface for more details. This creates a new color wp_color_management_surface_feedback_v1 object for the given wl_surface. See the wp_color_management_surface_feedback_v1 interface for more details. Makes a new ICC-based image description creator object with all properties initially unset. The client can then use the object's interface to define all the required properties for an image description and finally create a wp_image_description_v1 object. This request can be used when the compositor advertises wp_color_manager_v1.feature.icc_v2_v4. Otherwise this request raises the protocol error unsupported_feature. Makes a new parametric image description creator object with all properties initially unset. The client can then use the object's interface to define all the required properties for an image description and finally create a wp_image_description_v1 object. This request can be used when the compositor advertises wp_color_manager_v1.feature.parametric. Otherwise this request raises the protocol error unsupported_feature. This creates a pre-defined image description for the so-called Windows-scRGB stimulus encoding. This comes from the Windows 10 handling of its own definition of an scRGB color space for an HDR screen driven in BT.2100/PQ signalling mode. Windows-scRGB uses sRGB (BT.709) color primaries and white point. The transfer characteristic is extended linear. The nominal color channel value range is extended, meaning it includes negative and greater than 1.0 values. Negative values are used to escape the sRGB color gamut boundaries. To make use of the extended range, the client needs to use a pixel format that can represent those values, e.g. floating-point 16 bits per channel. Nominal color value R=G=B=0.0 corresponds to BT.2100/PQ system 0 cd/m², and R=G=B=1.0 corresponds to BT.2100/PQ system 80 cd/m². The maximum is R=G=B=125.0 corresponding to 10k cd/m². Windows-scRGB is displayed by Windows 10 by converting it to BT.2100/PQ, maintaining the CIE 1931 chromaticity and mapping the luminance as above. No adjustment is made to the signal to account for the viewing conditions. The reference white level of Windows-scRGB is unknown. If a reference white level must be assumed for compositor processing, it should be R=G=B=2.5375 corresponding to 203 cd/m² of Report ITU-R BT.2408-7. The target color volume of Windows-scRGB is unknown. The color gamut may be anything between sRGB and BT.2100. Note: EGL_EXT_gl_colorspace_scrgb_linear definition differs from Windows-scRGB by using R=G=B=1.0 as the reference white level, while Windows-scRGB reference white level is unknown or varies. However, it seems probable that Windows implements both EGL_EXT_gl_colorspace_scrgb_linear and Vulkan VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT as Windows-scRGB. This request can be used when the compositor advertises wp_color_manager_v1.feature.windows_scrgb. Otherwise this request raises the protocol error unsupported_feature. The resulting image description object does not allow get_information request. The wp_image_description_v1.ready event shall be sent. When this object is created, it shall immediately send this event once for each rendering intent the compositor supports. When this object is created, it shall immediately send this event once for each compositor supported feature listed in the enumeration. When this object is created, it shall immediately send this event once for each named transfer function the compositor supports with the parametric image description creator. When this object is created, it shall immediately send this event once for each named set of primaries the compositor supports with the parametric image description creator. This event is sent when all supported rendering intents, features, transfer functions and named primaries have been sent. A wp_color_management_output_v1 describes the color properties of an output. The wp_color_management_output_v1 is associated with the wl_output global underlying the wl_output object. Therefore the client destroying the wl_output object has no impact, but the compositor removing the output global makes the wp_color_management_output_v1 object inert. Destroy the color wp_color_management_output_v1 object. This does not affect any remaining protocol objects. This event is sent whenever the image description of the output changed, followed by one wl_output.done event common to output events across all extensions. If the client wants to use the updated image description, it needs to do get_image_description again, because image description objects are immutable. This creates a new wp_image_description_v1 object for the current image description of the output. There always is exactly one image description active for an output so the client should destroy the image description created by earlier invocations of this request. This request is usually sent as a reaction to the image_description_changed event or when creating a wp_color_management_output_v1 object. The image description of an output represents the color encoding the output expects. There might be performance and power advantages, as well as improved color reproduction, if a content update matches the image description of the output it is being shown on. If a content update is shown on any other output than the one it matches the image description of, then the color reproduction on those outputs might be considerably worse. The created wp_image_description_v1 object preserves the image description of the output from the time the object was created. The resulting image description object allows get_information request. If this protocol object is inert, the resulting image description object shall immediately deliver the wp_image_description_v1.failed event with the no_output cause. If the interface version is inadequate for the output's image description, meaning that the client does not support all the events needed to deliver the crucial information, the resulting image description object shall immediately deliver the wp_image_description_v1.failed event with the low_version cause. Otherwise the object shall immediately deliver the ready event. A wp_color_management_surface_v1 allows the client to set the color space and HDR properties of a surface. If the wl_surface associated with the wp_color_management_surface_v1 is destroyed, the wp_color_management_surface_v1 object becomes inert. Destroy the wp_color_management_surface_v1 object and do the same as unset_image_description. If this protocol object is inert, the protocol error inert is raised. Set the image description of the underlying surface. The image description and rendering intent are double-buffered state, see wl_surface.commit. It is the client's responsibility to understand the image description it sets on a surface, and to provide content that matches that image description. Compositors might convert images to match their own or any other image descriptions. Image descriptions which are not ready (see wp_image_description_v1) are forbidden in this request, and in such case the protocol error image_description is raised. All image descriptions which are ready (see wp_image_description_v1) are allowed and must always be accepted by the compositor. A rendering intent provides the client's preference on how content colors should be mapped to each output. The render_intent value must be one advertised by the compositor with wp_color_manager_v1.render_intent event, otherwise the protocol error render_intent is raised. When an image description is set on a surface, the Transfer Characteristics of the image description defines the valid range of the nominal (real-valued) color channel values. The processing of out-of-range color channel values is undefined, but compositors are recommended to clamp the values to the valid range when possible. By default, a surface does not have an associated image description nor a rendering intent. The handling of color on such surfaces is compositor implementation defined. Compositors should handle such surfaces as sRGB, but may handle them differently if they have specific requirements. Setting the image description has copy semantics; after this request, the image description can be immediately destroyed without affecting the pending state of the surface. If this protocol object is inert, the protocol error inert is raised. This request removes any image description from the surface. See set_image_description for how a compositor handles a surface without an image description. This is double-buffered state, see wl_surface.commit. A wp_color_management_surface_feedback_v1 allows the client to get the preferred image description of a surface. If the wl_surface associated with this object is destroyed, the wp_color_management_surface_feedback_v1 object becomes inert. Destroy the wp_color_management_surface_feedback_v1 object. The preferred image description is the one which likely has the most performance and/or quality benefits for the compositor if used by the client for its wl_surface contents. This event is sent whenever the compositor changes the wl_surface's preferred image description. This event sends the identity of the new preferred state as the argument, so clients who are aware of the image description already can reuse it. Otherwise, if the client client wants to know what the preferred image description is, it shall use the get_preferred request. The preferred image description is not automatically used for anything. It is only a hint, and clients may set any valid image description with set_image_description, but there might be performance and color accuracy improvements by providing the wl_surface contents in the preferred image description. Therefore clients that can, should render according to the preferred image description If this protocol object is inert, the protocol error inert is raised. The preferred image description represents the compositor's preferred color encoding for this wl_surface at the current time. There might be performance and power advantages, as well as improved color reproduction, if the image description of a content update matches the preferred image description. This creates a new wp_image_description_v1 object for the currently preferred image description for the wl_surface. The client should stop using and destroy the image descriptions created by earlier invocations of this request for the associated wl_surface. This request is usually sent as a reaction to the preferred_changed event or when creating a wp_color_management_surface_feedback_v1 object if the client is capable of adapting to image descriptions. The created wp_image_description_v1 object preserves the preferred image description of the wl_surface from the time the object was created. The resulting image description object allows get_information request. If the image description is parametric, the client should set it on its wl_surface only if the image description is an exact match with the client content. Particularly if everything else matches, but the target color volume is greater than what the client needs, the client should create its own parameric image description with its exact parameters. If the interface version is inadequate for the preferred image description, meaning that the client does not support all the events needed to deliver the crucial information, the resulting image description object shall immediately deliver the wp_image_description_v1.failed event with the low_version cause, otherwise the object shall immediately deliver the ready event. The same description as for get_preferred applies, except the returned image description is guaranteed to be parametric. This is meant for clients that can only deal with parametric image descriptions. If the compositor doesn't support parametric image descriptions, the unsupported_feature error is emitted. This type of object is used for collecting all the information required to create a wp_image_description_v1 object from an ICC file. A complete set of required parameters consists of these properties: - ICC file Each required property must be set exactly once if the client is to create an image description. The set requests verify that a property was not already set. The create request verifies that all required properties are set. There may be several alternative requests for setting each property, and in that case the client must choose one of them. Once all properties have been set, the create request must be used to create the image description object, destroying the creator in the process. Create an image description object based on the ICC information previously set on this object. A compositor must parse the ICC data in some undefined but finite amount of time. The completeness of the parameter set is verified. If the set is not complete, the protocol error incomplete_set is raised. For the definition of a complete set, see the description of this interface. If the particular combination of the information is not supported by the compositor, the resulting image description object shall immediately deliver the wp_image_description_v1.failed event with the 'unsupported' cause. If a valid image description was created from the information, the wp_image_description_v1.ready event will eventually be sent instead. This request destroys the wp_image_description_creator_icc_v1 object. The resulting image description object does not allow get_information request. Sets the ICC profile file to be used as the basis of the image description. The data shall be found through the given fd at the given offset, having the given length. The fd must be seekable and readable. Violating these requirements raises the bad_fd protocol error. If reading the data fails due to an error independent of the client, the compositor shall send the wp_image_description_v1.failed event on the created wp_image_description_v1 with the 'operating_system' cause. The maximum size of the ICC profile is 32 MB. If length is greater than that or zero, the protocol error bad_size is raised. If offset + length exceeds the file size, the protocol error out_of_file is raised. A compositor may read the file at any time starting from this request and only until whichever happens first: - If create request was issued, the wp_image_description_v1 object delivers either failed or ready event; or - if create request was not issued, this wp_image_description_creator_icc_v1 object is destroyed. A compositor shall not modify the contents of the file, and the fd may be sealed for writes and size changes. The client must ensure to its best ability that the data does not change while the compositor is reading it. The data must represent a valid ICC profile. The ICC profile version must be 2 or 4, it must be a 3 channel profile and the class must be Display or ColorSpace. Violating these requirements will not result in a protocol error, but will eventually send the wp_image_description_v1.failed event on the created wp_image_description_v1 with the 'unsupported' cause. See the International Color Consortium specification ICC.1:2022 for more details about ICC profiles. If ICC file has already been set on this object, the protocol error already_set is raised. This type of object is used for collecting all the parameters required to create a wp_image_description_v1 object. A complete set of required parameters consists of these properties: - transfer characteristic function (tf) - chromaticities of primaries and white point (primary color volume) The following properties are optional and have a well-defined default if not explicitly set: - primary color volume luminance range - reference white luminance level - mastering display primaries and white point (target color volume) - mastering luminance range The following properties are optional and will be ignored if not explicitly set: - maximum content light level - maximum frame-average light level Each required property must be set exactly once if the client is to create an image description. The set requests verify that a property was not already set. The create request verifies that all required properties are set. There may be several alternative requests for setting each property, and in that case the client must choose one of them. Once all properties have been set, the create request must be used to create the image description object, destroying the creator in the process. Create an image description object based on the parameters previously set on this object. The completeness of the parameter set is verified. If the set is not complete, the protocol error incomplete_set is raised. For the definition of a complete set, see the description of this interface. The protocol error invalid_luminance is raised if any of the following requirements is not met: - When max_cll is set, it must be greater than min L and less or equal to max L of the mastering luminance range. - When max_fall is set, it must be greater than min L and less or equal to max L of the mastering luminance range. - When both max_cll and max_fall are set, max_fall must be less or equal to max_cll. If the particular combination of the parameter set is not supported by the compositor, the resulting image description object shall immediately deliver the wp_image_description_v1.failed event with the 'unsupported' cause. If a valid image description was created from the parameter set, the wp_image_description_v1.ready event will eventually be sent instead. This request destroys the wp_image_description_creator_params_v1 object. The resulting image description object does not allow get_information request. Sets the transfer characteristic using explicitly enumerated named functions. When the resulting image description is attached to an image, the content should be encoded and decoded according to the industry standard practices for the transfer characteristic. Only names advertised with wp_color_manager_v1 event supported_tf_named are allowed. Other values shall raise the protocol error invalid_tf. If transfer characteristic has already been set on this object, the protocol error already_set is raised. Sets the color component transfer characteristic to a power curve with the given exponent. Negative values are handled by mirroring the positive half of the curve through the origin. The valid domain and range of the curve are all finite real numbers. This curve represents the conversion from electrical to optical color channel values. When the resulting image description is attached to an image, the content should be encoded with the inverse of the power curve. The curve exponent shall be multiplied by 10000 to get the argument eexp value to carry the precision of 4 decimals. The curve exponent must be at least 1.0 and at most 10.0. Otherwise the protocol error invalid_tf is raised. If transfer characteristic has already been set on this object, the protocol error already_set is raised. This request can be used when the compositor advertises wp_color_manager_v1.feature.set_tf_power. Otherwise this request raises the protocol error unsupported_feature. Sets the color primaries and white point using explicitly named sets. This describes the primary color volume which is the basis for color value encoding. Only names advertised with wp_color_manager_v1 event supported_primaries_named are allowed. Other values shall raise the protocol error invalid_primaries_named. If primaries have already been set on this object, the protocol error already_set is raised. Sets the color primaries and white point using CIE 1931 xy chromaticity coordinates. This describes the primary color volume which is the basis for color value encoding. Each coordinate value is multiplied by 1 million to get the argument value to carry precision of 6 decimals. If primaries have already been set on this object, the protocol error already_set is raised. This request can be used if the compositor advertises wp_color_manager_v1.feature.set_primaries. Otherwise this request raises the protocol error unsupported_feature. Sets the primary color volume luminance range and the reference white luminance level. These values include the minimum display emission and ambient flare luminances, assumed to be optically additive and have the chromaticity of the primary color volume white point. The default luminances from https://www.color.org/chardata/rgb/srgb.xalter are - primary color volume minimum: 0.2 cd/m² - primary color volume maximum: 80 cd/m² - reference white: 80 cd/m² Setting a named transfer characteristic can imply other default luminances. The default luminances get overwritten when this request is used. With transfer_function.st2084_pq the given 'max_lum' value is ignored, and 'max_lum' is taken as 'min_lum' + 10000 cd/m². 'min_lum' and 'max_lum' specify the minimum and maximum luminances of the primary color volume as reproduced by the targeted display. 'reference_lum' specifies the luminance of the reference white as reproduced by the targeted display, and reflects the targeted viewing environment. Compositors should make sure that all content is anchored, meaning that an input signal level of 'reference_lum' on one image description and another input signal level of 'reference_lum' on another image description should produce the same output level, even though the 'reference_lum' on both image representations can be different. 'reference_lum' may be higher than 'max_lum'. In that case reaching the reference white output level in image content requires the 'extended_target_volume' feature support. If 'max_lum' or 'reference_lum' are less than or equal to 'min_lum', the protocol error invalid_luminance is raised. The minimum luminance is multiplied by 10000 to get the argument 'min_lum' value and carries precision of 4 decimals. The maximum luminance and reference white luminance values are unscaled. If the primary color volume luminance range and the reference white luminance level have already been set on this object, the protocol error already_set is raised. This request can be used if the compositor advertises wp_color_manager_v1.feature.set_luminances. Otherwise this request raises the protocol error unsupported_feature. Provides the color primaries and white point of the mastering display using CIE 1931 xy chromaticity coordinates. This is compatible with the SMPTE ST 2086 definition of HDR static metadata. The mastering display primaries and mastering display luminances define the target color volume. If mastering display primaries are not explicitly set, the target color volume is assumed to have the same primaries as the primary color volume. The target color volume is defined by all tristimulus values between 0.0 and 1.0 (inclusive) of the color space defined by the given mastering display primaries and white point. The colorimetry is identical between the container color space and the mastering display color space, including that no chromatic adaptation is applied even if the white points differ. The target color volume can exceed the primary color volume to allow for a greater color volume with an existing color space definition (for example scRGB). It can be smaller than the primary color volume to minimize gamut and tone mapping distances for big color spaces (HDR metadata). To make use of the entire target color volume a suitable pixel format has to be chosen (e.g. floating point to exceed the primary color volume, or abusing limited quantization range as with xvYCC). Each coordinate value is multiplied by 1 million to get the argument value to carry precision of 6 decimals. If mastering display primaries have already been set on this object, the protocol error already_set is raised. This request can be used if the compositor advertises wp_color_manager_v1.feature.set_mastering_display_primaries. Otherwise this request raises the protocol error unsupported_feature. The advertisement implies support only for target color volumes fully contained within the primary color volume. If a compositor additionally supports target color volume exceeding the primary color volume, it must advertise wp_color_manager_v1.feature.extended_target_volume. If a client uses target color volume exceeding the primary color volume and the compositor does not support it, the result is implementation defined. Compositors are recommended to detect this case and fail the image description gracefully, but it may as well result in color artifacts. Sets the luminance range that was used during the content mastering process as the minimum and maximum absolute luminance L. These values include the minimum display emission and ambient flare luminances, assumed to be optically additive and have the chromaticity of the primary color volume white point. This should be compatible with the SMPTE ST 2086 definition of HDR static metadata. The mastering display primaries and mastering display luminances define the target color volume. If mastering luminances are not explicitly set, the target color volume is assumed to have the same min and max luminances as the primary color volume. If max L is less than or equal to min L, the protocol error invalid_luminance is raised. Min L value is multiplied by 10000 to get the argument min_lum value and carry precision of 4 decimals. Max L value is unscaled for max_lum. This request can be used if the compositor advertises wp_color_manager_v1.feature.set_mastering_display_primaries. Otherwise this request raises the protocol error unsupported_feature. The advertisement implies support only for target color volumes fully contained within the primary color volume. If a compositor additionally supports target color volume exceeding the primary color volume, it must advertise wp_color_manager_v1.feature.extended_target_volume. If a client uses target color volume exceeding the primary color volume and the compositor does not support it, the result is implementation defined. Compositors are recommended to detect this case and fail the image description gracefully, but it may as well result in color artifacts. Sets the maximum content light level (max_cll) as defined by CTA-861-H. max_cll is undefined by default. Sets the maximum frame-average light level (max_fall) as defined by CTA-861-H. max_fall is undefined by default. An image description carries information about the color encoding used on a surface when attached to a wl_surface via wp_color_management_surface_v1.set_image_description. A compositor can use this information to decode pixel values into colorimetrically meaningful quantities. Note, that the wp_image_description_v1 object is not ready to be used immediately after creation. The object eventually delivers either the 'ready' or the 'failed' event, specified in all requests creating it. The object is deemed "ready" after receiving the 'ready' event. An object which is not ready is illegal to use, it can only be destroyed. Any other request in this interface shall result in the 'not_ready' protocol error. Attempts to use an object which is not ready through other interfaces shall raise protocol errors defined there. Once created and regardless of how it was created, a wp_image_description_v1 object always refers to one fixed image description. It cannot change after creation. Destroy this object. It is safe to destroy an object which is not ready. Destroying a wp_image_description_v1 object has no side-effects, not even if a wp_color_management_surface_v1.set_image_description has not yet been followed by a wl_surface.commit. If creating a wp_image_description_v1 object fails for a reason that is not defined as a protocol error, this event is sent. The requests that create image description objects define whether and when this can occur. Only such creation requests can trigger this event. This event cannot be triggered after the image description was successfully formed. Once this event has been sent, the wp_image_description_v1 object will never become ready and it can only be destroyed. Once this event has been sent, the wp_image_description_v1 object is deemed "ready". Ready objects can be used to send requests and can be used through other interfaces. Every ready wp_image_description_v1 protocol object refers to an underlying image description record in the compositor. Multiple protocol objects may end up referring to the same record. Clients may identify these "copies" by comparing their id numbers: if the numbers from two protocol objects are identical, the protocol objects refer to the same image description record. Two different image description records cannot have the same id number simultaneously. The id number does not change during the lifetime of the image description record. The id number is valid only as long as the protocol object is alive. If all protocol objects referring to the same image description record are destroyed, the id number may be recycled for a different image description record. Image description id number is not a protocol object id. Zero is reserved as an invalid id number. It shall not be possible for a client to refer to an image description by its id number in protocol. The id numbers might not be portable between Wayland connections. A compositor shall not send an invalid id number. This identity allows clients to de-duplicate image description records and avoid get_information request if they already have the image description information. Creates a wp_image_description_info_v1 object which delivers the information that makes up the image description. Not all image description protocol objects allow get_information request. Whether it is allowed or not is defined by the request that created the object. If get_information is not allowed, the protocol error no_information is raised. Sends all matching events describing an image description object exactly once and finally sends the 'done' event. This means - if the image description is parametric, it must send - primaries - named_primaries, if applicable - at least one of tf_power and tf_named, as applicable - luminances - target_primaries - target_luminance - if the image description is parametric, it may send, if applicable, - target_max_cll - target_max_fall - if the image description contains an ICC profile, it must send the icc_file event Once a wp_image_description_info_v1 object has delivered a 'done' event it is automatically destroyed. Every wp_image_description_info_v1 created from the same wp_image_description_v1 shall always return the exact same data. Signals the end of information events and destroys the object. The icc argument provides a file descriptor to the client which may be memory-mapped to provide the ICC profile matching the image description. The fd is read-only, and if mapped then it must be mapped with MAP_PRIVATE by the client. The ICC profile version and other details are determined by the compositor. There is no provision for a client to ask for a specific kind of a profile. Delivers the primary color volume primaries and white point using CIE 1931 xy chromaticity coordinates. Each coordinate value is multiplied by 1 million to get the argument value to carry precision of 6 decimals. Delivers the primary color volume primaries and white point using an explicitly enumerated named set. The color component transfer characteristic of this image description is a pure power curve. This event provides the exponent of the power function. This curve represents the conversion from electrical to optical pixel or color values. The curve exponent has been multiplied by 10000 to get the argument eexp value to carry the precision of 4 decimals. Delivers the transfer characteristic using an explicitly enumerated named function. Delivers the primary color volume luminance range and the reference white luminance level. These values include the minimum display emission and ambient flare luminances, assumed to be optically additive and have the chromaticity of the primary color volume white point. The minimum luminance is multiplied by 10000 to get the argument 'min_lum' value and carries precision of 4 decimals. The maximum luminance and reference white luminance values are unscaled. Provides the color primaries and white point of the target color volume using CIE 1931 xy chromaticity coordinates. This is compatible with the SMPTE ST 2086 definition of HDR static metadata for mastering displays. While primary color volume is about how color is encoded, the target color volume is the actually displayable color volume. If target color volume is equal to the primary color volume, then this event is not sent. Each coordinate value is multiplied by 1 million to get the argument value to carry precision of 6 decimals. Provides the luminance range that the image description is targeting as the minimum and maximum absolute luminance L. These values include the minimum display emission and ambient flare luminances, assumed to be optically additive and have the chromaticity of the primary color volume white point. This should be compatible with the SMPTE ST 2086 definition of HDR static metadata. This luminance range is only theoretical and may not correspond to the luminance of light emitted on an actual display. Min L value is multiplied by 10000 to get the argument min_lum value and carry precision of 4 decimals. Max L value is unscaled for max_lum. Provides the targeted max_cll of the image description. max_cll is defined by CTA-861-H. This luminance is only theoretical and may not correspond to the luminance of light emitted on an actual display. Provides the targeted max_fall of the image description. max_fall is defined by CTA-861-H. This luminance is only theoretical and may not correspond to the luminance of light emitted on an actual display. ValveSoftware-gamescope-eb620ab/protocol/frog-color-management-v1.xml000066400000000000000000000372441502457270500260410ustar00rootroot00000000000000 Copyright © 2023 Joshua Ashton for Valve Software Copyright © 2023 Xaver Hugl Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. The aim of this color management extension is to get HDR games working quickly, and have an easy way to test implementations in the wild before the upstream protocol is ready to be merged. For that purpose it's intentionally limited and cut down and does not serve all uses cases. The color management factory singleton creates color managed surface objects. Interface for changing surface color management and HDR state. An implementation must: support every part of the version of the frog_color_managed_surface interface it exposes. Including all known enums associated with a given version. Destroying the color managed surface resets all known color state for the surface back to 'undefined' implementation-specific values. Extended information on the transfer functions described here can be found in the Khronos Data Format specification: https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.html Extended information on render intents described here can be found in ICC.1:2022: https://www.color.org/specification/ICC.1-2022-05.pdf NOTE: On a surface with "perceptual" (default) render intent, handling of the container's color volume is implementation-specific, and may differ between different transfer functions it is paired with: ie. sRGB + 709 rendering may have it's primaries widened to more of the available display's gamut to be be more pleasing for the viewer. Compared to scRGB Linear + 709 being treated faithfully as 709 (including utilizing negatives out of the 709 gamut triangle) Forwards HDR metadata from the client to the compositor. HDR Metadata Infoframe as per CTA 861.G spec. Usage of this HDR metadata is implementation specific and outside of the scope of this protocol. Mastering Red Color Primary X Coordinate of the Data. Coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. Mastering Red Color Primary Y Coordinate of the Data. Coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. Mastering Green Color Primary X Coordinate of the Data. Coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. Mastering Green Color Primary Y Coordinate of the Data. Coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. Mastering Blue Color Primary X Coordinate of the Data. Coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. Mastering Blue Color Primary Y Coordinate of the Data. Coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. Mastering White Point X Coordinate of the Data. These are coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. Mastering White Point Y Coordinate of the Data. These are coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. Max Mastering Display Luminance. This value is coded as an unsigned 16-bit value in units of 1 cd/m2, where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. Min Mastering Display Luminance. This value is coded as an unsigned 16-bit value in units of 0.0001 cd/m2, where 0x0001 represents 0.0001 cd/m2 and 0xFFFF represents 6.5535 cd/m2. Max Content Light Level. This value is coded as an unsigned 16-bit value in units of 1 cd/m2, where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. Max Frame Average Light Level. This value is coded as an unsigned 16-bit value in units of 1 cd/m2, where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. Current preferred metadata for a surface. The application should use this information to tone-map its buffers to this target before committing. This metadata does not necessarily correspond to any physical output, but rather what the compositor thinks would be best for a given surface. Specifies a known transfer function that corresponds to the output the surface is targeting. Output Red Color Primary X Coordinate of the Data. Coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. Output Red Color Primary Y Coordinate of the Data. Coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. Output Green Color Primary X Coordinate of the Data. Coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. Output Green Color Primary Y Coordinate of the Data. Coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. Output Blue Color Primary X Coordinate of the Data. Coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. Output Blue Color Primary Y Coordinate of the Data. Coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. Output White Point X Coordinate of the Data. These are coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. Output White Point Y Coordinate of the Data. These are coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. Max Output Luminance The max luminance in nits that the output is capable of rendering in small areas. Content should: not exceed this value to avoid clipping. This value is coded as an unsigned 16-bit value in units of 1 cd/m2, where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. Min Output Luminance The min luminance that the output is capable of rendering. Content should: not exceed this value to avoid clipping. This value is coded as an unsigned 16-bit value in units of 0.0001 cd/m2, where 0x0001 represents 0.0001 cd/m2 and 0xFFFF represents 6.5535 cd/m2. Max Full Frame Luminance The max luminance in nits that the output is capable of rendering for the full frame sustained. This value is coded as an unsigned 16-bit value in units of 1 cd/m2, where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. ValveSoftware-gamescope-eb620ab/protocol/gamescope-action-binding.xml000066400000000000000000000070371502457270500261550ustar00rootroot00000000000000 Copyright © 2024 Valve Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. This is a private Gamescope protocol. Regular Wayland clients must not use it. Flags that control how the action is armed. Flags that say how the action was triggered. ValveSoftware-gamescope-eb620ab/protocol/gamescope-control.xml000066400000000000000000000151611502457270500247450ustar00rootroot00000000000000 Copyright © 2023 Valve Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. This is a private Gamescope protocol. Regular Wayland clients must not use it. Enum of the features supported by Gamescope. Says whether a feature is supported and the version. ValveSoftware-gamescope-eb620ab/protocol/gamescope-input-method.xml000066400000000000000000000213611502457270500257010ustar00rootroot00000000000000 Copyright © 2008-2011 Kristian Høgsberg Copyright © 2010-2011 Intel Corporation Copyright © 2012-2013 Collabora, Ltd. Copyright © 2012, 2013 Intel Corporation Copyright © 2015, 2016 Jan Arne Petersen Copyright © 2017, 2018 Red Hat, Inc. Copyright © 2018 Purism SPC Copyright © 2021 Valve Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. This protocol allows applications to act as input methods for compositors. An input method context is used to manage the state of the input method. Text strings are UTF-8 encoded, their indices and lengths are in bytes. This iteration of the protocol is a subset of this work-in-progress proposal: https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/112 This is a private Gamescope protocol. Regular Wayland clients must not use it. The input method manager allows the client to become the input method on a chosen seat. No more than one input method must be associated with any seat at any given time. Destroys the gamescope_input_method_manager object. The gamescope_input_method objects originating from it remain valid. Create a new gamescope_input_method object associated with a given seat. An input method object allows for clients to compose text. The objects connects the client to a text input in an application, and lets the client to serve as an input method for a seat. There must be no more than one input method object per seat. The input method ceased to be available. The compositor must issue this event as the only event on the object if there was another input_method object associated with the same seat at the time of its creation. The compositor must issue this request when the object is no longer useable, e.g. due to seat removal. The input method context becomes inert and should be destroyed. Any further requests and events except for the destroy request must be ignored. Notify the client that the compositor state has changed. The serial is used to synchronize compositor state changes and client state changes. The serial allows the compositor to ignore requests meant for a previous text input. In the future, will be used to atomically apply compositor state changes sent to the client (but currently there's no such state). Destroys the gamescope_text_input object. Apply state changes from set_string and set_action requests. The state relating to these events is double-buffered, and each one modifies the pending state. This request replaces the current state with the pending state. The client must set the serial to the last received serial in the done event. Send the string text for insertion to the application. Inserts a string at current cursor position (see commit event sequence). The string to commit could be either just a single character after a key press or the result of some composing. Values set with this event are double-buffered. They must be applied and reset to initial on the next gamescope_text_input.commit request. The initial value of text is an empty string. A possible action to perform on a text input. The backspace and delete actions should be handled in a similar manner to backpace and delete keys being pressed on a keyboard. Set the action to be performed on commit. Values set with this event are double-buffered. They must be applied and reset to initial on the next gamescope_text_input.commit request. Describes the physical state of a button that produced the button event. ValveSoftware-gamescope-eb620ab/protocol/gamescope-pipewire.xml000066400000000000000000000037211502457270500251100ustar00rootroot00000000000000 Copyright © 2021 Valve Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. This is a private Gamescope protocol. Regular Wayland clients must not use it. This event advertises a PipeWire stream node identifier suitable for capturing the main output. A roundtrip after binding to the gamescope_pipewire global ensures this event has been received. ValveSoftware-gamescope-eb620ab/protocol/gamescope-private.xml000066400000000000000000000036621502457270500247420ustar00rootroot00000000000000 Copyright © 2023 Valve Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. This is a private Gamescope protocol, mainly for debugging. No stable ABI is guaranteed for this protocol, it is versioned with gamescope. ValveSoftware-gamescope-eb620ab/protocol/gamescope-reshade.xml000066400000000000000000000057011502457270500246770ustar00rootroot00000000000000 Copyright © 2024 Wayne Heaney Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. This protocol allows applications to load and interact with a reshade FX shader in gamescope. The effect will be disabled to allow an opportunity to set uniform variables before enabling it. This event alerts the client when an effect has been enabled. Enables the effect that was previously loaded by set_effect. Set the value of a uniform variable. Can be called before or after enabling the effect. Disables the effect that was previously enabled by enable_effect. ValveSoftware-gamescope-eb620ab/protocol/gamescope-swapchain.xml000066400000000000000000000240211502457270500252350ustar00rootroot00000000000000 Copyright © 2023 Joshua Ashton for Valve Software Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. This is a private Gamescope protocol. Regular Wayland clients must not use it. Xwayland creates a wl_surface for each X11 window. It sends a WL_SURFACE_ID client message to indicate the mapping between the X11 windows and the wl_surface objects. This request overrides this mapping for a given X11 window, allowing an X11 client to submit buffers via the Wayland protocol. The override only affects buffer submission. Everything else (e.g. input events) still uses Xwayland's WL_SURFACE_ID. x11_server is gotten by the GAMESCOPE_XWAYLAND_SERVER_ID property on the root window of the associated server. Provide swapchain feedback to the compositor. This is what the useless tearing protocol should have been. Absolutely not enough information in the final protocol to do what we want for SteamOS -- which is have the Allow Tearing toggle apply to *both* Mailbox + Immediate and NOT fifo, essentially acting as an override for tearing on/off for games. The upstream protocol is very useless for our usecase here. Provides image count ahead of time instead of needing to try and calculate it from an initial stall if we are doing low latency. Provides colorspace info for us to do HDR for both HDR10 PQ and scRGB. The upstream HDR efforts seem to have no interest in supporting scRGB but we *need* that so /shrug We can do it here now! Yipee! Swapchain feedback solves so many problems! :D Forward HDR metadata from Vulkan to the compositor. HDR Metadata Infoframe as per CTA 861.G spec. This is expected to match exactly with the spec. display_primary_*: Color Primaries of the Data. Specifies X and Y coordinates. These are coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. white_point_*: White Point of Colorspace Data. Specifies X and Y coordinates. These are coded as unsigned 16-bit values in units of 0.00002, where 0x0000 represents zero and 0xC350 represents 1.0000. max_display_mastering_luminance: Max Mastering Display Luminance. This value is coded as an unsigned 16-bit value in units of 1 cd/m2, where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. max_display_mastering_luminance: Min Mastering Display Luminance. This value is coded as an unsigned 16-bit value in units of 0.0001 cd/m2, where 0x0001 represents 0.0001 cd/m2 and 0xFFFF represents 6.5535 cd/m2. max_cll: Max Content Light Level. This value is coded as an unsigned 16-bit value in units of 1 cd/m2, where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. max_fall: Max Frame Average Light Level. This value is coded as an unsigned 16-bit value in units of 1 cd/m2, where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. Sets the display timing of the next commit. This gets reset to 0s in the compositor's state after a commit. Gives information on the past presentation timing Gives information on the refresh cycle for this swapchain ValveSoftware-gamescope-eb620ab/protocol/gamescope-xwayland.xml000066400000000000000000000047701502457270500251200ustar00rootroot00000000000000 Copyright © 2021 Valve Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. This is a private Gamescope protocol. Regular Wayland clients must not use it. This protocol has been superceded by the 'gamescope-swapchain' protocol. Xwayland creates a wl_surface for each X11 window. It sends a WL_SURFACE_ID client message to indicate the mapping between the X11 windows and the wl_surface objects. This request overrides this mapping for a given X11 window, allowing an X11 client to submit buffers via the Wayland protocol. The override only affects buffer submission. Everything else (e.g. input events) still uses Xwayland's WL_SURFACE_ID. x11_server is gotten by the GAMESCOPE_XWAYLAND_SERVER_ID property on the root window of the associated server. ValveSoftware-gamescope-eb620ab/protocol/meson.build000066400000000000000000000045311502457270500227430ustar00rootroot00000000000000fs = import('fs') wayland_scanner_dep = dependency('wayland-scanner', native: true) wayland_scanner_path = wayland_scanner_dep.get_variable(pkgconfig: 'wayland_scanner') wayland_scanner = find_program(wayland_scanner_path, native: true) wayland_protos = dependency('wayland-protocols', fallback: 'wayland-protocols', default_options: ['tests=false'], ) wl_protocol_dir = wayland_protos.get_variable('pkgdatadir') protocols = [ # Upstream protocols wl_protocol_dir / 'stable/linux-dmabuf/linux-dmabuf-v1.xml', wl_protocol_dir / 'stable/viewporter/viewporter.xml', wl_protocol_dir / 'stable/xdg-shell/xdg-shell.xml', wl_protocol_dir / 'stable/presentation-time/presentation-time.xml', wl_protocol_dir / 'staging/single-pixel-buffer/single-pixel-buffer-v1.xml', wl_protocol_dir / 'unstable/pointer-constraints/pointer-constraints-unstable-v1.xml', wl_protocol_dir / 'unstable/relative-pointer/relative-pointer-unstable-v1.xml', wl_protocol_dir / 'unstable/primary-selection/primary-selection-unstable-v1.xml', wl_protocol_dir / 'staging/fractional-scale/fractional-scale-v1.xml', wl_protocol_dir / 'staging/linux-drm-syncobj/linux-drm-syncobj-v1.xml', 'color-management-v1.xml', # Wayland Protocols not yet in a release. 'xdg-toplevel-icon-v1.xml', # Frog protocols 'frog-color-management-v1.xml', # Gamescope protocols 'gamescope-xwayland.xml', 'gamescope-pipewire.xml', 'gamescope-input-method.xml', 'gamescope-control.xml', 'gamescope-reshade.xml', 'gamescope-swapchain.xml', 'gamescope-private.xml', 'gamescope-action-binding.xml', # wlroots protocols 'wlr-layer-shell-unstable-v1.xml', ] protocols_client_src = [] protocols_server_src = [] foreach xml : protocols name = fs.name(xml).replace('.xml', '') code = custom_target( name + '-protocol.c', input: xml, output: '@BASENAME@-protocol.c', command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'], ) server_header = custom_target( name + '-protocol.h', input: xml, output: '@BASENAME@-protocol.h', command: [wayland_scanner, 'server-header', '@INPUT@', '@OUTPUT@'], ) client_header = custom_target( name + '-client-protocol.h', input: xml, output: '@BASENAME@-client-protocol.h', command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'], ) protocols_server_src += [code, server_header] protocols_client_src += [code, client_header] endforeach ValveSoftware-gamescope-eb620ab/protocol/wlr-layer-shell-unstable-v1.xml000066400000000000000000000440361502457270500265110ustar00rootroot00000000000000 Copyright © 2017 Drew DeVault Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of the copyright holders not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. The copyright holders make no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty. THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. Clients can use this interface to assign the surface_layer role to wl_surfaces. Such surfaces are assigned to a "layer" of the output and rendered with a defined z-depth respective to each other. They may also be anchored to the edges and corners of a screen and specify input handling semantics. This interface should be suitable for the implementation of many desktop shell components, and a broad number of other applications that interact with the desktop. Create a layer surface for an existing surface. This assigns the role of layer_surface, or raises a protocol error if another role is already assigned. Creating a layer surface from a wl_surface which has a buffer attached or committed is a client error, and any attempts by a client to attach or manipulate a buffer prior to the first layer_surface.configure call must also be treated as errors. After creating a layer_surface object and setting it up, the client must perform an initial commit without any buffer attached. The compositor will reply with a layer_surface.configure event. The client must acknowledge it and is then allowed to attach a buffer to map the surface. You may pass NULL for output to allow the compositor to decide which output to use. Generally this will be the one that the user most recently interacted with. Clients can specify a namespace that defines the purpose of the layer surface. These values indicate which layers a surface can be rendered in. They are ordered by z depth, bottom-most first. Traditional shell surfaces will typically be rendered between the bottom and top layers. Fullscreen shell surfaces are typically rendered at the top layer. Multiple surfaces can share a single layer, and ordering within a single layer is undefined. This request indicates that the client will not use the layer_shell object any more. Objects that have been created through this instance are not affected. An interface that may be implemented by a wl_surface, for surfaces that are designed to be rendered as a layer of a stacked desktop-like environment. Layer surface state (layer, size, anchor, exclusive zone, margin, interactivity) is double-buffered, and will be applied at the time wl_surface.commit of the corresponding wl_surface is called. Attaching a null buffer to a layer surface unmaps it. Unmapping a layer_surface means that the surface cannot be shown by the compositor until it is explicitly mapped again. The layer_surface returns to the state it had right after layer_shell.get_layer_surface. The client can re-map the surface by performing a commit without any buffer attached, waiting for a configure event and handling it as usual. Sets the size of the surface in surface-local coordinates. The compositor will display the surface centered with respect to its anchors. If you pass 0 for either value, the compositor will assign it and inform you of the assignment in the configure event. You must set your anchor to opposite edges in the dimensions you omit; not doing so is a protocol error. Both values are 0 by default. Size is double-buffered, see wl_surface.commit. Requests that the compositor anchor the surface to the specified edges and corners. If two orthogonal edges are specified (e.g. 'top' and 'left'), then the anchor point will be the intersection of the edges (e.g. the top left corner of the output); otherwise the anchor point will be centered on that edge, or in the center if none is specified. Anchor is double-buffered, see wl_surface.commit. Requests that the compositor avoids occluding an area with other surfaces. The compositor's use of this information is implementation-dependent - do not assume that this region will not actually be occluded. A positive value is only meaningful if the surface is anchored to one edge or an edge and both perpendicular edges. If the surface is not anchored, anchored to only two perpendicular edges (a corner), anchored to only two parallel edges or anchored to all edges, a positive value will be treated the same as zero. A positive zone is the distance from the edge in surface-local coordinates to consider exclusive. Surfaces that do not wish to have an exclusive zone may instead specify how they should interact with surfaces that do. If set to zero, the surface indicates that it would like to be moved to avoid occluding surfaces with a positive exclusive zone. If set to -1, the surface indicates that it would not like to be moved to accommodate for other surfaces, and the compositor should extend it all the way to the edges it is anchored to. For example, a panel might set its exclusive zone to 10, so that maximized shell surfaces are not shown on top of it. A notification might set its exclusive zone to 0, so that it is moved to avoid occluding the panel, but shell surfaces are shown underneath it. A wallpaper or lock screen might set their exclusive zone to -1, so that they stretch below or over the panel. The default value is 0. Exclusive zone is double-buffered, see wl_surface.commit. Requests that the surface be placed some distance away from the anchor point on the output, in surface-local coordinates. Setting this value for edges you are not anchored to has no effect. The exclusive zone includes the margin. Margin is double-buffered, see wl_surface.commit. Types of keyboard interaction possible for layer shell surfaces. The rationale for this is twofold: (1) some applications are not interested in keyboard events and not allowing them to be focused can improve the desktop experience; (2) some applications will want to take exclusive keyboard focus. This value indicates that this surface is not interested in keyboard events and the compositor should never assign it the keyboard focus. This is the default value, set for newly created layer shell surfaces. This is useful for e.g. desktop widgets that display information or only have interaction with non-keyboard input devices. Request exclusive keyboard focus if this surface is above the shell surface layer. For the top and overlay layers, the seat will always give exclusive keyboard focus to the top-most layer which has keyboard interactivity set to exclusive. If this layer contains multiple surfaces with keyboard interactivity set to exclusive, the compositor determines the one receiving keyboard events in an implementation- defined manner. In this case, no guarantee is made when this surface will receive keyboard focus (if ever). For the bottom and background layers, the compositor is allowed to use normal focus semantics. This setting is mainly intended for applications that need to ensure they receive all keyboard events, such as a lock screen or a password prompt. This requests the compositor to allow this surface to be focused and unfocused by the user in an implementation-defined manner. The user should be able to unfocus this surface even regardless of the layer it is on. Typically, the compositor will want to use its normal mechanism to manage keyboard focus between layer shell surfaces with this setting and regular toplevels on the desktop layer (e.g. click to focus). Nevertheless, it is possible for a compositor to require a special interaction to focus or unfocus layer shell surfaces (e.g. requiring a click even if focus follows the mouse normally, or providing a keybinding to switch focus between layers). This setting is mainly intended for desktop shell components (e.g. panels) that allow keyboard interaction. Using this option can allow implementing a desktop shell that can be fully usable without the mouse. Set how keyboard events are delivered to this surface. By default, layer shell surfaces do not receive keyboard events; this request can be used to change this. This setting is inherited by child surfaces set by the get_popup request. Layer surfaces receive pointer, touch, and tablet events normally. If you do not want to receive them, set the input region on your surface to an empty region. Keyboard interactivity is double-buffered, see wl_surface.commit. This assigns an xdg_popup's parent to this layer_surface. This popup should have been created via xdg_surface::get_popup with the parent set to NULL, and this request must be invoked before committing the popup's initial state. See the documentation of xdg_popup for more details about what an xdg_popup is and how it is used. When a configure event is received, if a client commits the surface in response to the configure event, then the client must make an ack_configure request sometime before the commit request, passing along the serial of the configure event. If the client receives multiple configure events before it can respond to one, it only has to ack the last configure event. A client is not required to commit immediately after sending an ack_configure request - it may even ack_configure several times before its next surface commit. A client may send multiple ack_configure requests before committing, but only the last request sent before a commit indicates which configure event the client really is responding to. This request destroys the layer surface. The configure event asks the client to resize its surface. Clients should arrange their surface for the new states, and then send an ack_configure request with the serial sent in this configure event at some point before committing the new surface. The client is free to dismiss all but the last configure event it received. The width and height arguments specify the size of the window in surface-local coordinates. The size is a hint, in the sense that the client is free to ignore it if it doesn't resize, pick a smaller size (to satisfy aspect ratio or resize in steps of NxM pixels). If the client picks a smaller size and is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the surface will be centered on this axis. If the width or height arguments are zero, it means the client should decide its own window dimension. The closed event is sent by the compositor when the surface will no longer be shown. The output may have been destroyed or the user may have asked for it to be removed. Further changes to the surface will be ignored. The client should destroy the resource after receiving this event, and create a new surface if they so choose. Change the layer that the surface is rendered on. Layer is double-buffered, see wl_surface.commit. ValveSoftware-gamescope-eb620ab/protocol/xdg-toplevel-icon-v1.xml000066400000000000000000000217441502457270500252140ustar00rootroot00000000000000 Copyright © 2023-2024 Matthias Klumpp Copyright © 2024 David Edmundson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. This protocol allows clients to set icons for their toplevel surfaces either via the XDG icon stock (using an icon name), or from pixel data. A toplevel icon represents the individual toplevel (unlike the application or launcher icon, which represents the application as a whole), and may be shown in window switchers, window overviews and taskbars that list individual windows. This document adheres to RFC 2119 when using words like "must", "should", "may", etc. Warning! The protocol described in this file is currently in the testing phase. Backward compatible changes may be added together with the corresponding interface version bump. Backward incompatible changes can only be done by creating a new major version of the extension. This interface allows clients to create toplevel window icons and set them on toplevel windows to be displayed to the user. Destroy the toplevel icon manager. This does not destroy objects created with the manager. Creates a new icon object. This icon can then be attached to a xdg_toplevel via the 'set_icon' request. This request assigns the icon 'icon' to 'toplevel', or clears the toplevel icon if 'icon' was null. This state is double-buffered and is applied on the next wl_surface.commit of the toplevel. After making this call, the xdg_toplevel_icon_v1 provided as 'icon' can be destroyed by the client without 'toplevel' losing its icon. The xdg_toplevel_icon_v1 is immutable from this point, and any future attempts to change it must raise the 'xdg_toplevel_icon_v1.immutable' protocol error. The compositor must set the toplevel icon from either the pixel data the icon provides, or by loading a stock icon using the icon name. See the description of 'xdg_toplevel_icon_v1' for details. If 'icon' is set to null, the icon of the respective toplevel is reset to its default icon (usually the icon of the application, derived from its desktop-entry file, or a placeholder icon). If this request is passed an icon with no pixel buffers or icon name assigned, the icon must be reset just like if 'icon' was null. This event indicates an icon size the compositor prefers to be available if the client has scalable icons and can render to any size. When the 'xdg_toplevel_icon_manager_v1' object is created, the compositor may send one or more 'icon_size' events to describe the list of preferred icon sizes. If the compositor has no size preference, it may not send any 'icon_size' event, and it is up to the client to decide a suitable icon size. A sequence of 'icon_size' events must be finished with a 'done' event. If the compositor has no size preferences, it must still send the 'done' event, without any preceding 'icon_size' events. This event is sent after all 'icon_size' events have been sent. This interface defines a toplevel icon. An icon can have a name, and multiple buffers. In order to be applied, the icon must have either a name, or at least one buffer assigned. Applying an empty icon (with no buffer or name) to a toplevel should reset its icon to the default icon. It is up to compositor policy whether to prefer using a buffer or loading an icon via its name. See 'set_name' and 'add_buffer' for details. Destroys the 'xdg_toplevel_icon_v1' object. The icon must still remain set on every toplevel it was assigned to, until the toplevel icon is reset explicitly. This request assigns an icon name to this icon. Any previously set name is overridden. The compositor must resolve 'icon_name' according to the lookup rules described in the XDG icon theme specification[1] using the environment's current icon theme. If the compositor does not support icon names or cannot resolve 'icon_name' according to the XDG icon theme specification it must fall back to using pixel buffer data instead. If this request is made after the icon has been assigned to a toplevel via 'set_icon', a 'immutable' error must be raised. [1]: https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html This request adds pixel data supplied as wl_buffer to the icon. The client should add pixel data for all icon sizes and scales that it can provide, or which are explicitly requested by the compositor via 'icon_size' events on xdg_toplevel_icon_manager_v1. The wl_buffer supplying pixel data as 'buffer' must be backed by wl_shm and must be a square (width and height being equal). If any of these buffer requirements are not fulfilled, a 'invalid_buffer' error must be raised. If this icon instance already has a buffer of the same size and scale from a previous 'add_buffer' request, data from the last request overrides the preexisting pixel data. The wl_buffer must be kept alive for as long as the xdg_toplevel_icon it is associated with is not destroyed. The buffer contents must not be modified after it was assigned to the icon. If this request is made after the icon has been assigned to a toplevel via 'set_icon', a 'immutable' error must be raised. ValveSoftware-gamescope-eb620ab/scripts/000077500000000000000000000000001502457270500204245ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/scripts/00-gamescope/000077500000000000000000000000001502457270500226045ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/scripts/00-gamescope/common/000077500000000000000000000000001502457270500240745ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/scripts/00-gamescope/common/inspect.lua000066400000000000000000000216541502457270500262540ustar00rootroot00000000000000-- Slightly modified from https://github.com/kikito/inspect.lua -- MIT license. local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local math = _tl_compat and _tl_compat.math or math; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table inspect = {Options = {}, } inspect._VERSION = 'inspect.lua 3.1.0' inspect._URL = 'http://github.com/kikito/inspect.lua' inspect._DESCRIPTION = 'human-readable representations of tables' inspect._LICENSE = [[ MIT LICENSE Copyright (c) 2022 Enrique García Cota Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] inspect.KEY = setmetatable({}, { __tostring = function() return 'inspect.KEY' end }) inspect.METATABLE = setmetatable({}, { __tostring = function() return 'inspect.METATABLE' end }) local tostring = tostring local rep = string.rep local match = string.match local char = string.char local gsub = string.gsub local fmt = string.format local _rawget if rawget then _rawget = rawget else _rawget = function(t, k) return t[k] end end local function rawpairs(t) return next, t, nil end local function smartQuote(str) if match(str, '"') and not match(str, "'") then return "'" .. str .. "'" end return '"' .. gsub(str, '"', '\\"') .. '"' end local shortControlCharEscapes = { ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v", ["\127"] = "\\127", } local longControlCharEscapes = { ["\127"] = "\127" } for i = 0, 31 do local ch = char(i) if not shortControlCharEscapes[ch] then shortControlCharEscapes[ch] = "\\" .. i longControlCharEscapes[ch] = fmt("\\%03d", i) end end local function escape(str) return (gsub(gsub(gsub(str, "\\", "\\\\"), "(%c)%f[0-9]", longControlCharEscapes), "%c", shortControlCharEscapes)) end local luaKeywords = { ['and'] = true, ['break'] = true, ['do'] = true, ['else'] = true, ['elseif'] = true, ['end'] = true, ['false'] = true, ['for'] = true, ['function'] = true, ['goto'] = true, ['if'] = true, ['in'] = true, ['local'] = true, ['nil'] = true, ['not'] = true, ['or'] = true, ['repeat'] = true, ['return'] = true, ['then'] = true, ['true'] = true, ['until'] = true, ['while'] = true, } local function isIdentifier(str) return type(str) == "string" and not not str:match("^[_%a][_%a%d]*$") and not luaKeywords[str] end local flr = math.floor local function isSequenceKey(k, sequenceLength) return type(k) == "number" and flr(k) == k and 1 <= (k) and k <= sequenceLength end local defaultTypeOrders = { ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, ['function'] = 5, ['userdata'] = 6, ['thread'] = 7, } local function sortKeys(a, b) local ta, tb = type(a), type(b) if ta == tb and (ta == 'string' or ta == 'number') then return (a) < (b) end local dta = defaultTypeOrders[ta] or 100 local dtb = defaultTypeOrders[tb] or 100 return dta == dtb and ta < tb or dta < dtb end local function getKeys(t) local seqLen = 1 while _rawget(t, seqLen) ~= nil do seqLen = seqLen + 1 end seqLen = seqLen - 1 local keys, keysLen = {}, 0 for k in rawpairs(t) do if not isSequenceKey(k, seqLen) then keysLen = keysLen + 1 keys[keysLen] = k end end table.sort(keys, sortKeys) return keys, keysLen, seqLen end local function countCycles(x, cycles) if type(x) == "table" then if cycles[x] then cycles[x] = cycles[x] + 1 else cycles[x] = 1 for k, v in rawpairs(x) do countCycles(k, cycles) countCycles(v, cycles) end countCycles(getmetatable(x), cycles) end end end local function makePath(path, a, b) local newPath = {} local len = #path for i = 1, len do newPath[i] = path[i] end newPath[len + 1] = a newPath[len + 2] = b return newPath end local function processRecursive(process, item, path, visited) if item == nil then return nil end if visited[item] then return visited[item] end local processed = process(item, path) if type(processed) == "table" then local processedCopy = {} visited[item] = processedCopy local processedKey for k, v in rawpairs(processed) do processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) if processedKey ~= nil then processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) end end local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) if type(mt) ~= 'table' then mt = nil end setmetatable(processedCopy, mt) processed = processedCopy end return processed end local function puts(buf, str) buf.n = buf.n + 1 buf[buf.n] = str end local Inspector = {} local Inspector_mt = { __index = Inspector } local function tabify(inspector) puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level)) end function Inspector:getId(v) local id = self.ids[v] local ids = self.ids if not id then local tv = type(v) id = (ids[tv] or 0) + 1 ids[v], ids[tv] = id, id end return tostring(id) end function Inspector:putValue(v) local buf = self.buf local tv = type(v) if tv == 'string' then puts(buf, smartQuote(escape(v))) elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or tv == 'cdata' or tv == 'ctype' then puts(buf, tostring(v)) elseif tv == 'table' and not self.ids[v] then local t = v if t == inspect.KEY or t == inspect.METATABLE then puts(buf, tostring(t)) elseif self.level >= self.depth then puts(buf, '{...}') else if self.cycles[t] > 1 then puts(buf, fmt('<%d>', self:getId(t))) end local keys, keysLen, seqLen = getKeys(t) puts(buf, '{') self.level = self.level + 1 for i = 1, seqLen + keysLen do if i > 1 then puts(buf, ',') end if i <= seqLen then puts(buf, ' ') self:putValue(t[i]) else local k = keys[i - seqLen] tabify(self) if isIdentifier(k) then puts(buf, k) else puts(buf, "[") self:putValue(k) puts(buf, "]") end puts(buf, ' = ') self:putValue(t[k]) end end local mt = getmetatable(t) if type(mt) == 'table' then if seqLen + keysLen > 0 then puts(buf, ',') end tabify(self) puts(buf, ' = ') self:putValue(mt) end self.level = self.level - 1 if keysLen > 0 or type(mt) == 'table' then tabify(self) elseif seqLen > 0 then puts(buf, ' ') end puts(buf, '}') end else puts(buf, fmt('<%s %d>', tv, self:getId(v))) end end function inspect.inspect(root, options) options = options or {} local depth = options.depth or (math.huge) local newline = options.newline or '\n' local indent = options.indent or ' ' local process = options.process if process then root = processRecursive(process, root, {}, {}) end local cycles = {} countCycles(root, cycles) local inspector = setmetatable({ buf = { n = 0 }, ids = {}, cycles = cycles, depth = depth, level = 0, newline = newline, indent = indent, }, Inspector_mt) inspector:putValue(root) return table.concat(inspector.buf) end setmetatable(inspect, { __call = function(_, root, options) return inspect.inspect(root, options) end, }) return inspect ValveSoftware-gamescope-eb620ab/scripts/00-gamescope/common/modegen.lua000066400000000000000000000022211502457270500262120ustar00rootroot00000000000000gamescope.modegen = {} gamescope.modegen.adjust_front_porch = function(mode, vfp) local vsync = mode.vsync_end - mode.vsync_start local vbp = mode.vtotal - mode.vsync_end mode.vsync_start = mode.vdisplay + vfp mode.vsync_end = mode.vsync_start + vsync mode.vtotal = mode.vsync_end + vbp end gamescope.modegen.set_h_timings = function(mode, hfp, hsync, hbp) mode.hsync_start = mode.hdisplay + hfp mode.hsync_end = mode.hsync_start + hsync mode.htotal = mode.hsync_end + hbp end gamescope.modegen.set_v_timings = function(mode, vfp, vsync, vbp) mode.vsync_start = mode.vdisplay + vfp mode.vsync_end = mode.vsync_start + vsync mode.vtotal = mode.vsync_end + vbp end gamescope.modegen.set_resolution = function(mode, width, height) mode.hdisplay = width mode.vdisplay = height end gamescope.modegen.calc_max_clock = function(mode, refresh) -- LuaJIT does not have // operator, sad face. return math.floor( ( ( mode.htotal * mode.vtotal * refresh ) + 999 ) / 1000 ) end gamescope.modegen.calc_vrefresh = function(mode, refresh) return math.floor( (1000 * mode.clock) / (mode.htotal * mode.vtotal) ) end ValveSoftware-gamescope-eb620ab/scripts/00-gamescope/common/util.lua000066400000000000000000000005631502457270500255600ustar00rootroot00000000000000function zero_index(index) return index + 1 end function error(text) gamescope.log(gamescope.log_priority.error, text) end function warn(text) gamescope.log(gamescope.log_priority.warning, text) end function info(text) gamescope.log(gamescope.log_priority.info, text) end function debug(text) gamescope.log(gamescope.log_priority.debug, text) end ValveSoftware-gamescope-eb620ab/scripts/00-gamescope/displays/000077500000000000000000000000001502457270500244345ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/scripts/00-gamescope/displays/asus.rogally.lcd.lua000066400000000000000000000070171502457270500303300ustar00rootroot00000000000000local rogally_lcd_refresh_rates = { 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120 } gamescope.config.known_displays.rogally_lcd = { pretty_name = "ASUS ROG Ally / ROG Ally X LCD", hdr = { -- Setup some fallbacks for undocking with HDR, meant -- for the internal panel. It does not support HDR. supported = false, force_enabled = false, eotf = gamescope.eotf.gamma22, max_content_light_level = 500, max_frame_average_luminance = 500, min_content_light_level = 0.5 }, dynamic_refresh_rates = rogally_lcd_refresh_rates, -- Follow the Steam Deck OLED style for modegen by varying the VFP (Vertical Front Porch) -- -- Given that this display is VRR and likely has an FB/Partial FB in the DDIC: -- it should be able to handle this method, and it is more optimal for latency -- than elongating the clock. dynamic_modegen = function(base_mode, refresh) debug("Generating mode "..refresh.."Hz for ASUS ROG Ally / ROG Ally X LCD with fixed pixel clock") local vfps = { 1771, 1720, 1655, 1600, 1549, 1499, 1455, 1405, 1361, 1320, 1279, 1224, 1200, 1162, 1120, 1088, 1055, 1022, 991, 958, 930, 900, 871, 845, 817, 794, 762, 740, 715, 690, 668, 647, 626, 605, 585, 566, 546, 526, 507, 488, 470, 452, 435, 419, 402, 387, 371, 355, 341, 326, 310, 297, 284, 269, 255, 244, 229, 217, 204, 194, 181, 172, 158, 147, 136, 123, 113, 104, 93, 83, 74, 64, 54 } local vfp = vfps[zero_index(refresh - 48)] if vfp == nil then warn("Couldn't do refresh "..refresh.." on ASUS ROG Ally / ROG Ally X LCD") return base_mode end local mode = base_mode gamescope.modegen.adjust_front_porch(mode, vfp) mode.vrefresh = gamescope.modegen.calc_vrefresh(mode) --debug(inspect(mode)) return mode end, matches = function(display) -- There are two panels used across the ROG Ally and ROG Ally X -- with the same timings, but the model names are in different -- parts of the EDID. local lcd_types = { { vendor = "TMX", model = "TL070FVXS01-0", product = 0x0002 }, { vendor = "BOE", data_string = "TS070FHM-LU0", product = 0x0C33 }, } for index, value in ipairs(lcd_types) do -- We only match if the vendor and product match exactly, plus either model or data_string if value.vendor == display.vendor and value.product == display.product then if (value.model and value.model == display.model) or (value.data_string and value.data_string == display.data_string) then debug("[rogally_lcd] Matched vendor: "..value.vendor.." model: "..(value.model or value.data_string).." product: "..value.product) return 5000 end end end return -1 end } debug("Registered ASUS ROG Ally / ROG Ally X LCD as a known display") --debug(inspect(gamescope.config.known_displays.rogally_lcd)) ValveSoftware-gamescope-eb620ab/scripts/00-gamescope/displays/deckhd.steamdeck.deckhd-lcd.lua000066400000000000000000000035721502457270500323100ustar00rootroot00000000000000gamescope.config.known_displays.steamdeck_deckhd_lcd = { pretty_name = "Steam Deck DeckHD - Unofficial Screen Replacement", dynamic_refresh_rates = { 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60 }, hdr = { -- Setup some fallbacks or undocking with HDR -- for this display. supported = false, force_enabled = false, eotf = gamescope.eotf.gamma22, max_content_light_level = 400, max_frame_average_luminance = 400, min_content_light_level = 0.5 }, -- No colorimetry was provided in the original PR, -- and I have no idea if their BIOS mod/whatever has colorimetry -- in their edid. -- Someone that works on this or uses this should go fix that. dynamic_modegen = function(base_mode, refresh) debug("Generating mode "..refresh.."Hz for DeckHD") local mode = base_mode -- These are only tuned for 1200x1920. gamescope.modegen.set_resolution(mode, 1200, 1920) -- hfp, hsync, hbp gamescope.modegen.set_h_timings(mode, 40, 20, 40) -- vfp, vsync, vbp gamescope.modegen.set_v_timings(mode, 18, 2, 20) mode.clock = gamescope.modegen.calc_max_clock(mode, refresh) mode.vrefresh = gamescope.modegen.calc_vrefresh(mode) --debug(inspect(mode)) return mode end, matches = function(display) if display.vendor == "DHD" and display.model == "DeckHD-1200p" and display.product == 0x4001 then debug("[steamdeck_deckhd_lcd] Matched vendor: "..display.vendor.." model: "..display.model.." product:"..display.product) return 5000 end return -1 end } debug("Registered Steam Deck DeckHD - Unofficial Screen Replacement as a known display") --debug(inspect(gamescope.config.known_displays.steamdeck_deckhd_lcd)) ValveSoftware-gamescope-eb620ab/scripts/00-gamescope/displays/gpd.win4.lcd.lua000066400000000000000000000037771502457270500273500ustar00rootroot00000000000000-- colorimetry from edid local gpd_win4_lcd_colorimetry = { r = { x = 0.6250, y = 0.3398 }, g = { x = 0.2802, y = 0.5947 }, b = { x = 0.1552, y = 0.0703 }, w = { x = 0.2832, y = 0.2978 } } gamescope.config.known_displays.gpd_win4_lcd = { pretty_name = "GPD Win 4", dynamic_refresh_rates = { 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60 }, hdr = { supported = false, force_enabled = false, eotf = gamescope.eotf.gamma22, max_content_light_level = 400, max_frame_average_luminance = 400, min_content_light_level = 0.5 }, colorimetry = gpd_win4_lcd_colorimetry, dynamic_modegen = function(base_mode, refresh) debug("Generating mode "..refresh.."Hz for GPD Win 4") local mode = base_mode gamescope.modegen.set_resolution(mode, 1920, 1080) -- Horizontal timings: Hfront, Hsync, Hback gamescope.modegen.set_h_timings(mode, 72, 8, 16) -- Vertical timings: Vfront, Vsync, Vback gamescope.modegen.set_v_timings(mode, 14, 3, 13) mode.clock = gamescope.modegen.calc_max_clock(mode, refresh) mode.vrefresh = gamescope.modegen.calc_vrefresh(mode) return mode end, matches = function(display) -- There are multiple revisions of the GPD Win 4 -- They all should have the same panel -- lcd_types is just in case there are different panels local lcd_types = { { vendor = "GPD", model = "G1618-04" }, } for index, value in ipairs(lcd_types) do if value.vendor == display.vendor and value.model == display.model then debug("[gpd_win4_lcd] Matched vendor: "..value.vendor.." model: "..value.model) return 5000 end end return -1 end } debug("Registered GPD Win 4 as a known display") --debug(inspect(gamescope.config.known_displays.gpd_win4_lcd)) ValveSoftware-gamescope-eb620ab/scripts/00-gamescope/displays/lenovo.legiongo.lcd.lua000066400000000000000000000034471502457270500310140ustar00rootroot00000000000000gamescope.config.known_displays.lenovo_legiongo_lcd = { pretty_name = "Lenovo Legion Go LCD", dynamic_refresh_rates = { 60, 144 }, hdr = { -- Setup some fallbacks for undocking with HDR, meant -- for the internal panel. It does not support HDR. supported = false, force_enabled = false, eotf = gamescope.eotf.gamma22, max_content_light_level = 500, max_frame_average_luminance = 500, min_content_light_level = 0.5 }, -- Use the EDID colorimetry for now, but someone should check -- if the EDID colorimetry truly matches what the display is capable of. dynamic_modegen = function(base_mode, refresh) debug("Generating mode "..refresh.."Hz for Lenovo Legion Go LCD") local mode = base_mode -- These are only tuned for 1600x2560 gamescope.modegen.set_resolution(mode, 1600, 2560) -- Horizontal timings: Hfront, Hsync, Hback gamescope.modegen.set_h_timings(mode, 60, 30, 130) -- Vertical timings: Vfront, Vsync, Vback gamescope.modegen.set_v_timings(mode, 30, 4, 96) mode.clock = gamescope.modegen.calc_max_clock(mode, refresh) mode.vrefresh = gamescope.modegen.calc_vrefresh(mode) return mode end, matches = function(display) -- There is only a single panel in use on the Lenovo Legion Go. if display.vendor == "LEN" and display.model == "Go Display" and display.product == 0x0001 then debug("[lenovo_legiongo_lcd] Matched vendor: "..display.vendor.." model: "..display.model.." product: "..display.product) return 5000 end return -1 end } debug("Registered Lenovo Legion Go LCD as a known display") --debug(inspect(gamescope.config.known_displays.lenovo_legiongo_lcd)) ValveSoftware-gamescope-eb620ab/scripts/00-gamescope/displays/lenovo.legiongos.lcd.lua000066400000000000000000000044411502457270500311720ustar00rootroot00000000000000local legiongos_lcd_refresh_rates = { 52, 53, 54, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 67, 68, 69, 70, 102, 103, 104, 105, 106, 107, 108, 109, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120 } gamescope.config.known_displays.legiongos_lcd = { pretty_name = "Lenovo Legion Go S LCD", hdr = { -- The Legion Go S panel does not support HDR. supported = false, force_enabled = false, eotf = gamescope.eotf.gamma22, max_content_light_level = 500, max_frame_average_luminance = 500, min_content_light_level = 0.5 }, -- 60Hz has a different pixel clock than 120Hz in the EDID with VRR disabled, -- and the panel is not responsive to tuning VFPs. To cover the non-VRR -- limiter, an LCD Deck-style dynamic modegen method works best. dynamic_refresh_rates = legiongos_lcd_refresh_rates, dynamic_modegen = function(base_mode, refresh) debug("Generating mode "..refresh.."Hz for Lenovo Legion Go S LCD") local mode = base_mode -- These are only tuned for 1920x1200. gamescope.modegen.set_resolution(mode, 1920, 1200) -- hfp, hsync, hbp gamescope.modegen.set_h_timings(mode, 48, 36, 80) -- vfp, vsync, vbp gamescope.modegen.set_v_timings(mode, 54, 6, 4) mode.clock = gamescope.modegen.calc_max_clock(mode, refresh) mode.vrefresh = gamescope.modegen.calc_vrefresh(mode) --debug(inspect(mode)) return mode end, matches = function(display) local lcd_types = { { vendor = "CSW", model = "PN8007QB1-1", product = 0x0800 }, { vendor = "BOE", model = "NS080WUM-LX1", product = 0x0C00 }, { vendor = "BOE", model = "NS080WUM-LX1", product = 0x0CFF }, } for index,value in ipairs(lcd_types) do if value.vendor == display.vendor and value.model == display.model and value.product == display.product then debug("[legiongos_lcd] Matched vendor: "..display.vendor.." model: "..display.model.." product: "..display.product) return 5000 end end return -1 end } debug("Registered Lenovo Legion Go S LCD as a known display") --debug(inspect(gamescope.config.known_displays.legiongos_lcd)) ValveSoftware-gamescope-eb620ab/scripts/00-gamescope/displays/valve.steamdeck.lcd.lua000066400000000000000000000046221502457270500307600ustar00rootroot00000000000000-- TODO: Make a vec2 class so we can alias .x and indices. local steamdeck_lcd_colorimetry_spec = { r = { x = 0.602, y = 0.355 }, g = { x = 0.340, y = 0.574 }, b = { x = 0.164, y = 0.121 }, w = { x = 0.3070, y = 0.3220 } } local steamdeck_lcd_colorimetry_measured = { r = { x = 0.603, y = 0.349 }, g = { x = 0.335, y = 0.571 }, b = { x = 0.163, y = 0.115 }, w = { x = 0.296, y = 0.307 } } gamescope.config.known_displays.steamdeck_lcd = { pretty_name = "Steam Deck LCD", dynamic_refresh_rates = { 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60 }, hdr = { -- Setup some fallbacks or undocking with HDR -- for this display. supported = false, force_enabled = false, eotf = gamescope.eotf.gamma22, max_content_light_level = 500, max_frame_average_luminance = 500, min_content_light_level = 0.5 }, -- Use measured colorimetry instead. --colorimetry = steamdeck_lcd_colorimetry_spec, colorimetry = steamdeck_lcd_colorimetry_measured, dynamic_modegen = function(base_mode, refresh) debug("Generating mode "..refresh.."Hz for Steam Deck LCD") local mode = base_mode -- These are only tuned for 800x1280. gamescope.modegen.set_resolution(mode, 800, 1280) -- hfp, hsync, hbp gamescope.modegen.set_h_timings(mode, 40, 4, 40) -- vfp, vsync, vbp gamescope.modegen.set_v_timings(mode, 30, 4, 8) mode.clock = gamescope.modegen.calc_max_clock(mode, refresh) mode.vrefresh = gamescope.modegen.calc_vrefresh(mode) --debug(inspect(mode)) return mode end, matches = function(display) local lcd_types = { { vendor = "WLC", model = "ANX7530 U" }, { vendor = "ANX", model = "ANX7530 U" }, { vendor = "VLV", model = "ANX7530 U" }, { vendor = "VLV", model = "Jupiter" }, } for index, value in ipairs(lcd_types) do if value.vendor == display.vendor and value.model == display.model then debug("[steamdeck_lcd] Matched vendor: "..value.vendor.." model: "..value.model) return 5000 end end return -1 end } debug("Registered Steam Deck LCD as a known display") --debug(inspect(gamescope.config.known_displays.steamdeck_lcd)) ValveSoftware-gamescope-eb620ab/scripts/00-gamescope/displays/valve.steamdeck.oled.lua000066400000000000000000000065331502457270500311440ustar00rootroot00000000000000local steamdeck_oled_hdr = { supported = true, force_enabled = true, eotf = gamescope.eotf.gamma22, max_content_light_level = 1000, max_frame_average_luminance = 800, min_content_light_level = 0 } local steamdeck_oled_refresh_rates = { 45, 47, 48, 49, 50, 51, 53, 55, 56, 59, 60, 62, 64, 65, 66, 68, 72, 73, 76, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90 } gamescope.config.known_displays.steamdeck_oled_sdc = { pretty_name = "Steam Deck OLED (SDC)", hdr = steamdeck_oled_hdr, dynamic_refresh_rates = steamdeck_oled_refresh_rates, dynamic_modegen = function(base_mode, refresh) debug("Generating mode "..refresh.."Hz for Steam Deck OLED (SDC)") local vfps = { 1321, 1264, 1209, 1157, 1106, 1058, 993, 967, 925, 883, 829, 805, 768, 732, 698, 665, 632, 601, 571, 542, 501, 486, 459, 433, 408, 383, 360, 337, 314, 292, 271, 250, 230, 210, 191, 173, 154, 137, 119, 102, 86, 70, 54, 38, 23, 9 } local vfp = vfps[zero_index(refresh - 45)] if vfp == nil then warn("Couldn't do refresh "..refresh.." on Steam Deck OLED (SDC)") return base_mode end local mode = base_mode gamescope.modegen.adjust_front_porch(mode, vfp) mode.vrefresh = gamescope.modegen.calc_vrefresh(mode) --debug(inspect(mode)) return mode end, matches = function(display) if display.vendor == "VLV" and display.product == 0x3003 then debug("[steamdeck_oled_sdc] Matched VLV and product 0x3003") -- Higher priorty than LCD. return 5100 end return -1 end } debug("Registered Steam Deck OLED (SDC) as a known display") --debug(inspect(gamescope.config.known_displays.steamdeck_oled_sdc)) gamescope.config.known_displays.steamdeck_oled_boe = { pretty_name = "Steam Deck OLED (BOE)", hdr = steamdeck_oled_hdr, dynamic_refresh_rates = steamdeck_oled_refresh_rates, dynamic_modegen = function(base_mode, refresh) debug("Generating mode for "..refresh.." for Steam Deck OLED (BOE)") local vfps = { 1320, 1272, 1216, 1156, 1112, 1064, 992, 972, 928, 888, 828, 808, 772, 736, 700, 664, 636, 604, 572, 544, 500, 488, 460, 436, 408, 384, 360, 336, 316, 292, 272, 252, 228, 212, 192, 172, 152, 136, 120, 100, 84, 68, 52, 36, 20, 8 } local vfp = vfps[zero_index(refresh - 45)] if vfp == nil then warn("Couldn't do refresh "..refresh.." on Steam Deck OLED (BOE)") return base_mode end local mode = base_mode gamescope.modegen.adjust_front_porch(mode, vfp) mode.vrefresh = gamescope.modegen.calc_vrefresh(mode) --debug(inspect(mode)) return mode end, matches = function(display) if display.vendor == "VLV" and display.product == 0x3004 then debug("[steamdeck_oled_boe] Matched VLV and product 0x3004") -- Higher priorty than LCD. return 5100 end return -1 end } debug("Registered Steam Deck OLED (BOE) as a known display") --debug(inspect(gamescope.config.known_displays.steamdeck_oled_boe)) ValveSoftware-gamescope-eb620ab/scripts/README.md000066400000000000000000000051351502457270500217070ustar00rootroot00000000000000# Gamescope Script/Config Files ## ⚠️ Health Warning ⚠️ Gamescope scripting/configuration is currently experimental and subject to change massively. Scripts and configs working between revisions is not guaranteed to work, it should at least not crash... probably. ## The Basics Gamescope uses Lua for it's configuration and scripting system. Scripts ending in `.lua` are executed recursively in alphabetical order from the following directories: - `/usr/share/gamescope` - `/etc/gamescope` - `$XDG_CONFIG_DIR/gamescope` You can develop easily without overriding your installation by setting `script_use_local_scripts` which will eliminate `/usr/share/gamescope` and `/etc/gamescope` from being read, and instead read from `../config` of where Gamescope is run instead of those. When errors are encountered, it will simply output that to the terminal. There is no visual indicator of this currently. Things should mostly fail-safe, unless you actually made an egregious mistake in your config like setting the refresh rate to 0 or the colorimetry to all 0, 0 or something. # Making modifications as a user If you wish to make modifications that will persist as a user, simply make a new `.lua` file in `$XDG_CONFIG_DIR/gamescope` which is usually `$HOME/.config/gamescope` with what you want to change. For example, to make the Steam Deck LCD use spec colorimetry instead of the measured colorimetry you could create the following file `~/.config/gamescope/my_deck_lcd_colorimetry.lua` with the following contents: ```lua local steamdeck_lcd_colorimetry_spec = { r = { x = 0.602, y = 0.355 }, g = { x = 0.340, y = 0.574 }, b = { x = 0.164, y = 0.121 }, w = { x = 0.3070, y = 0.3220 } } gamescope.config.known_displays.steamdeck_lcd.colorimetry = steamdeck_lcd_colorimetry_spec ``` and it would override that. You could also place this in `/etc/gamescope` if you really want it to apply to all users/system-wide, but that would need root privelages. # Features Being able to set known displays (`gamescope.config.known_displays`) The ability to set convars. Hooks # Examples A script that will enable composite debug and force composition on and off every 60 frames. ```lua my_counter = 0 gamescope.convars.composite_debug.value = 3 gamescope.hook("OnPostPaint", function() my_counter = my_counter + 1 if my_counter > 60 then gamescope.convars.composite_force.value = not gamescope.convars.composite_force.value my_counter = 0 warn("Changed composite_force to "..tostring(gamescope.convars.composite_force.value)..".") end end) ``` # Hot Reloading? Coming soon... ValveSoftware-gamescope-eb620ab/src/000077500000000000000000000000001502457270500175245ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/src/Apps/000077500000000000000000000000001502457270500204275ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/src/Apps/gamescope_hotkey_example.cpp000066400000000000000000000124561502457270500262040ustar00rootroot00000000000000#include #include #include #include #include #include #include "convar.h" #include "Utils/Version.h" #include #include #include // TODO: Consolidate #define WAYLAND_NULL() [] ( void *pData, Args... args ) { } #define WAYLAND_USERDATA_TO_THIS(type, name) [] ( void *pData, Args... args ) { type *pThing = (type *)pData; pThing->name( std::forward(args)... ); } namespace gamescope { class CActionBinding { public: bool Init( gamescope_action_binding_manager *pManager, std::span pKeySyms ) { Shutdown(); m_pBinding = gamescope_action_binding_manager_create_action_binding( pManager ); if ( !m_pBinding ) return false; wl_array array; wl_array_init(&array); for ( uint32_t uKeySym : pKeySyms ) { uint32_t *pKeySymPtr = (uint32_t *)wl_array_add(&array, sizeof(uint32_t) ); *pKeySymPtr = uKeySym; } gamescope_action_binding_add_listener( m_pBinding, &s_BindingListener, (void *)this ); gamescope_action_binding_add_keyboard_trigger( m_pBinding, &array ); gamescope_action_binding_set_description( m_pBinding, "My Example Hotkey :)" ); gamescope_action_binding_arm( m_pBinding, 0 ); return true; } void Shutdown() { if ( m_pBinding ) { gamescope_action_binding_destroy( m_pBinding ); m_pBinding = nullptr; } } void Wayland_Triggered( gamescope_action_binding *pBinding, uint32_t uSequence, uint32_t uTriggerFlags, uint32_t uTimeLo, uint32_t uTimeHi ) { fprintf( stderr, "Hotkey pressed!" ); } private: gamescope_action_binding *m_pBinding = nullptr; static const gamescope_action_binding_listener s_BindingListener; }; const gamescope_action_binding_listener CActionBinding::s_BindingListener = { .triggered = WAYLAND_USERDATA_TO_THIS( CActionBinding, Wayland_Triggered ), }; class GamescopeHotkeyExample { public: GamescopeHotkeyExample(); ~GamescopeHotkeyExample(); bool Init(); void Run(); private: wl_display *m_pDisplay = nullptr; gamescope_action_binding_manager *m_pActionBindingManager = nullptr; void Wayland_Registry_Global( wl_registry *pRegistry, uint32_t uName, const char *pInterface, uint32_t uVersion ); static const wl_registry_listener s_RegistryListener; }; GamescopeHotkeyExample::GamescopeHotkeyExample() { } GamescopeHotkeyExample::~GamescopeHotkeyExample() { } bool GamescopeHotkeyExample::Init() { const char *pDisplayName = getenv( "GAMESCOPE_WAYLAND_DISPLAY" ); if ( !pDisplayName || !*pDisplayName ) pDisplayName = "gamescope-0"; if ( !( m_pDisplay = wl_display_connect( pDisplayName ) ) ) { fprintf( stderr, "Failed to open GAMESCOPE_WAYLAND_DISPLAY.\n" ); return false; } { wl_registry *pRegistry; if ( !( pRegistry = wl_display_get_registry( m_pDisplay ) ) ) { fprintf( stderr, "Failed to get wl_registry.\n" ); return false; } wl_registry_add_listener( pRegistry, &s_RegistryListener, (void *)this ); wl_display_roundtrip( m_pDisplay ); wl_display_roundtrip( m_pDisplay ); if ( !m_pActionBindingManager ) { fprintf( stderr, "Failed to get Gamescope binding manager\n" ); return false; } wl_registry_destroy( pRegistry ); } return true; } void GamescopeHotkeyExample::Run() { // Add a test hotkey of Shift + P. std::vector uKeySyms = { 0xffe1, 0x0070 }; // XKB_KEY_Shift_L + XKB_KEY_p CActionBinding binding; if ( !binding.Init( m_pActionBindingManager, uKeySyms ) ) return; wl_display_flush( m_pDisplay ); for ( ;; ) { wl_display_dispatch( m_pDisplay ); } } void GamescopeHotkeyExample::Wayland_Registry_Global( wl_registry *pRegistry, uint32_t uName, const char *pInterface, uint32_t uVersion ) { if ( !strcmp( pInterface, gamescope_action_binding_manager_interface.name ) ) { m_pActionBindingManager = (decltype(m_pActionBindingManager)) wl_registry_bind( pRegistry, uName, &gamescope_action_binding_manager_interface, uVersion ); } } const wl_registry_listener GamescopeHotkeyExample::s_RegistryListener = { .global = WAYLAND_USERDATA_TO_THIS( GamescopeHotkeyExample, Wayland_Registry_Global ), .global_remove = WAYLAND_NULL(), }; static int RunHotkeyExample( int argc, char *argv[] ) { gamescope::GamescopeHotkeyExample hotkeyExample; if ( !hotkeyExample.Init() ) return 1; hotkeyExample.Run(); return 0; } } int main( int argc, char *argv[] ) { return gamescope::RunHotkeyExample( argc, argv ); } ValveSoftware-gamescope-eb620ab/src/Apps/gamescopectl.cpp000066400000000000000000000271101502457270500236020ustar00rootroot00000000000000#include #include #include #include #include #include #include "convar.h" #include "Utils/Version.h" #include #include #include // TODO: Consolidate #define WAYLAND_NULL() [] ( void *pData, Args... args ) { } #define WAYLAND_USERDATA_TO_THIS(type, name) [] ( void *pData, Args... args ) { type *pThing = (type *)pData; pThing->name( std::forward(args)... ); } namespace gamescope { struct GamescopeFeature { gamescope_control_feature eFeature; uint32_t uVersion; uint32_t uFlags; }; struct GamescopeActiveDisplayInfo { std::string szConnectorName; std::string szDisplayMake; std::string szDisplayModel; uint32_t uDisplayFlags; std::vector ValidRefreshRates; }; class GamescopeCtl { public: GamescopeCtl(); ~GamescopeCtl(); bool Init( bool bInitControl, bool bInitPrivate ); bool Execute( std::span args ); std::span GetFeatures() { return std::span{ m_Features }; } const std::optional &GetActiveDisplayInfo() { return m_ActiveDisplayInfo; } private: bool m_bInitControl = false; bool m_bInitPrivate = false; wl_display *m_pDisplay = nullptr; gamescope_control *m_pGamescopeControl = nullptr; gamescope_private *m_pGamescopePrivate = nullptr; uint32_t m_uCommandCount = 0; std::vector m_Features; std::optional m_ActiveDisplayInfo; void Wayland_Registry_Global( wl_registry *pRegistry, uint32_t uName, const char *pInterface, uint32_t uVersion ); static const wl_registry_listener s_RegistryListener; void Wayland_GamescopeControl_FeatureSupport( gamescope_control *pGamescopeControl, uint32_t uFeature, uint32_t uVersion, uint32_t uFlags ); void Wayland_GamescopeControl_ActiveDisplayInfo( gamescope_control *pGamescopeControl, const char *pConnectorName, const char *pDisplayMake, const char *pDisplayModel, uint32_t uDisplayFlags, wl_array *pValidRefreshRatesArray ); void Wayland_GamescopeControl_ScreenshotTaken( gamescope_control *pGamescopeControl, const char *pPath ); static const gamescope_control_listener s_GamescopeControlListener; void Wayland_GamescopePrivate_Log( gamescope_private *pGamescopePrivate, const char *pText ); void Wayland_GamescopePrivate_CommandExecuted( gamescope_private *pGamescopePrivate ); static const gamescope_private_listener s_GamescopePrivateListener; }; GamescopeCtl::GamescopeCtl() { } GamescopeCtl::~GamescopeCtl() { } bool GamescopeCtl::Init( bool bInitControl, bool bInitPrivate ) { m_bInitControl = bInitControl; m_bInitPrivate = bInitPrivate; const char *pDisplayName = getenv( "GAMESCOPE_WAYLAND_DISPLAY" ); if ( !pDisplayName || !*pDisplayName ) pDisplayName = "gamescope-0"; if ( !( m_pDisplay = wl_display_connect( pDisplayName ) ) ) { fprintf( stderr, "Failed to open GAMESCOPE_WAYLAND_DISPLAY.\n" ); return false; } { wl_registry *pRegistry; if ( !( pRegistry = wl_display_get_registry( m_pDisplay ) ) ) { fprintf( stderr, "Failed to get wl_registry.\n" ); return false; } wl_registry_add_listener( pRegistry, &s_RegistryListener, (void *)this ); wl_display_roundtrip( m_pDisplay ); wl_display_roundtrip( m_pDisplay ); if ( !( !m_bInitControl || m_pGamescopeControl ) || !( !m_bInitPrivate || m_pGamescopePrivate ) ) { fprintf( stderr, "Failed to get Gamescope interfaces\n" ); return false; } wl_registry_destroy( pRegistry ); } return true; } bool GamescopeCtl::Execute( std::span args ) { if ( args.size() < 1 ) { fprintf( stderr, "No command to execute\n" ); return false; } std::string szArg1 = std::string{ args[0] }; std::string szArg2 = args.size() == 1 ? "" : std::string{ args[1] }; gamescope_private_execute( m_pGamescopePrivate, szArg1.c_str(), szArg2.c_str() ); wl_display_roundtrip( m_pDisplay ); return true; } void GamescopeCtl::Wayland_Registry_Global( wl_registry *pRegistry, uint32_t uName, const char *pInterface, uint32_t uVersion ) { if ( m_bInitControl && !strcmp( pInterface, gamescope_control_interface.name ) ) { m_pGamescopeControl = (decltype(m_pGamescopeControl)) wl_registry_bind( pRegistry, uName, &gamescope_control_interface, uVersion ); gamescope_control_add_listener( m_pGamescopeControl, &s_GamescopeControlListener, this ); } else if ( m_bInitPrivate && !strcmp( pInterface, gamescope_private_interface.name ) ) { m_pGamescopePrivate = (decltype(m_pGamescopePrivate)) wl_registry_bind( pRegistry, uName, &gamescope_private_interface, uVersion ); gamescope_private_add_listener( m_pGamescopePrivate, &s_GamescopePrivateListener, this ); } } const wl_registry_listener GamescopeCtl::s_RegistryListener = { .global = WAYLAND_USERDATA_TO_THIS( GamescopeCtl, Wayland_Registry_Global ), .global_remove = WAYLAND_NULL(), }; void GamescopeCtl::Wayland_GamescopeControl_FeatureSupport( gamescope_control *pGamescopeControl, uint32_t uFeature, uint32_t uVersion, uint32_t uFlags ) { gamescope_control_feature eFeature = static_cast( uFeature ); if ( eFeature == GAMESCOPE_CONTROL_FEATURE_DONE ) return; m_Features.emplace_back( GamescopeFeature { .eFeature = eFeature, .uVersion = uVersion, .uFlags = uFlags } ); } void GamescopeCtl::Wayland_GamescopeControl_ActiveDisplayInfo( gamescope_control *pGamescopeControl, const char *pConnectorName, const char *pDisplayMake, const char *pDisplayModel, uint32_t uDisplayFlags, wl_array *pValidRefreshRatesArray ) { const uint32_t *pValidRefreshRates = reinterpret_cast( pValidRefreshRatesArray->data ); std::vector validRefreshRates; for ( size_t i = 0; i < pValidRefreshRatesArray->size / sizeof( uint32_t ); i++ ) validRefreshRates.push_back( pValidRefreshRates[i] ); m_ActiveDisplayInfo = GamescopeActiveDisplayInfo { .szConnectorName = pConnectorName, .szDisplayMake = pDisplayMake, .szDisplayModel = pDisplayModel, .uDisplayFlags = uDisplayFlags, .ValidRefreshRates = std::move( validRefreshRates ), }; } void GamescopeCtl::Wayland_GamescopeControl_ScreenshotTaken( gamescope_control *pGamescopeControl, const char *pPath ) { fprintf( stderr, "Screenshot taken to: %s\n", pPath ); } const gamescope_control_listener GamescopeCtl::s_GamescopeControlListener = { .feature_support = WAYLAND_USERDATA_TO_THIS( GamescopeCtl, Wayland_GamescopeControl_FeatureSupport ), .active_display_info = WAYLAND_USERDATA_TO_THIS( GamescopeCtl, Wayland_GamescopeControl_ActiveDisplayInfo ), .screenshot_taken = WAYLAND_USERDATA_TO_THIS( GamescopeCtl, Wayland_GamescopeControl_ScreenshotTaken ), }; void GamescopeCtl::Wayland_GamescopePrivate_Log( gamescope_private *pGamescopePrivate, const char *pText ) { fprintf( stderr, "%s\n", pText ); } void GamescopeCtl::Wayland_GamescopePrivate_CommandExecuted( gamescope_private *pGamescopePrivate ) { m_uCommandCount++; } const gamescope_private_listener GamescopeCtl::s_GamescopePrivateListener = { .log = WAYLAND_USERDATA_TO_THIS( GamescopeCtl, Wayland_GamescopePrivate_Log ), .command_executed = WAYLAND_USERDATA_TO_THIS( GamescopeCtl, Wayland_GamescopePrivate_CommandExecuted ), }; static std::string_view GetFeatureName( gamescope_control_feature eFeature ) { switch( eFeature ) { case GAMESCOPE_CONTROL_FEATURE_DONE: return "Done (dummy)"; case GAMESCOPE_CONTROL_FEATURE_RESHADE_SHADERS: return "Reshade Shaders"; case GAMESCOPE_CONTROL_FEATURE_DISPLAY_INFO: return "Display Info"; case GAMESCOPE_CONTROL_FEATURE_PIXEL_FILTER: return "Pixel Filter"; case GAMESCOPE_CONTROL_FEATURE_REFRESH_CYCLE_ONLY_CHANGE_REFRESH_RATE: return "Refresh Cycle Only Change Refresh Rate"; case GAMESCOPE_CONTROL_FEATURE_MURA_CORRECTION: return "Mura Correction"; default: return "Unknown"; } } static int RunGamescopeCtl( int argc, char *argv[] ) { console_log.bPrefixEnabled = false; bool bInfoOnly = argc < 2; gamescope::GamescopeCtl gamescopeCtl; if ( !gamescopeCtl.Init( bInfoOnly, !bInfoOnly ) ) return 1; if ( bInfoOnly ) { PrintVersion(); fprintf( stdout, "gamescope_control info:\n" ); const auto &oActiveDisplayInfo = gamescopeCtl.GetActiveDisplayInfo(); if ( oActiveDisplayInfo ) { fprintf( stdout, " - Connector Name: %.*s\n", (int)oActiveDisplayInfo->szConnectorName.length(), oActiveDisplayInfo->szConnectorName.data() ); fprintf( stdout, " - Display Make: %.*s\n", (int)oActiveDisplayInfo->szDisplayMake.length(), oActiveDisplayInfo->szDisplayMake.data() ); fprintf( stdout, " - Display Model: %.*s\n", (int)oActiveDisplayInfo->szDisplayModel.length(), oActiveDisplayInfo->szDisplayModel.data() ); fprintf( stdout, " - Display Flags: 0x%x\n", oActiveDisplayInfo->uDisplayFlags ); fprintf( stdout, " - ValidRefreshRates: " ); for ( size_t i = 0; i < oActiveDisplayInfo->ValidRefreshRates.size(); i++ ) { bool bLast = i == oActiveDisplayInfo->ValidRefreshRates.size() - 1; uint32_t uRate = oActiveDisplayInfo->ValidRefreshRates[i]; fprintf( stdout, bLast ? "%u" : "%u, ", uRate ); } fprintf( stdout, "\n" ); } fprintf( stdout, " Features:\n" ); for ( const GamescopeFeature &feature : gamescopeCtl.GetFeatures() ) { std::string_view szFeatureName = GetFeatureName( feature.eFeature ); fprintf( stdout, " - %.*s (%u) - Version: %u - Flags: 0x%x\n", (int)szFeatureName.size(), szFeatureName.data(), uint32_t{ feature.eFeature }, feature.uVersion, feature.uFlags ); } fprintf( stdout, "You can execute any debug command in Gamescope using this tool.\n" ); fprintf( stdout, "For a list of commands and convars, use 'gamescopectl help'\n" ); return 0; } std::vector args; for ( int i = 1; i < argc; i++ ) args.emplace_back( argv[i] ); if ( !gamescopeCtl.Execute( std::span{ args } ) ) return 1; return 0; } } int main( int argc, char *argv[] ) { return gamescope::RunGamescopeCtl( argc, argv ); }ValveSoftware-gamescope-eb620ab/src/Apps/gamescopereaper.cpp000066400000000000000000000130741502457270500243020ustar00rootroot00000000000000#include "../Utils/Process.h" #include "../log.hpp" #include #include #include #include #include #include #include #include #include namespace gamescope { static LogScope s_ReaperLog( "reaper" ); // Watches over a PID and waits for all the children to die. // It sets itself up as a subreaper so any children get reparented ti oti. // If the primary process dies, it kills all the children. // // Gamescope can have a lot of bad things happen to it, crashes, segfaults, whatever // but we always want to make sure that we cleanly kill all of our children when we die. // This child process attempts to stay alive as long as it can in order to fulfil Gamescope's // dying wish -- to kill all of it's children. int GamescopeReaperProcess( int argc, char **argv ) { pthread_setname_np( pthread_self(), "gamescope-reaper" ); static constexpr struct option k_ReaperOptions[] = { { "label", required_argument, nullptr, 0 }, { "new-session-id", no_argument, nullptr, 0 }, { "respawn", no_argument, nullptr, 0 }, }; bool bRespawn = false; bool bNewSession = false; static bool s_bRun = true; int nOptIndex = -1; int nOption = -1; while ( ( nOption = getopt_long(argc, argv, "", k_ReaperOptions, &nOptIndex ) ) != -1 ) { if ( nOption == '?' ) { s_ReaperLog.errorf( "Unknown option." ); } assert( nOption == 0 ); const char *pszOptionName = k_ReaperOptions[ nOptIndex ].name; if ( !strcmp( pszOptionName, "label" ) ) { // Do nothing. continue; } else if ( !strcmp( pszOptionName, "respawn" ) ) { bRespawn = true; } else if ( !strcmp( pszOptionName, "new-session-id" ) ) { bNewSession = true; } } int nSubCommandArgc = 0; for ( int i = 0; i < argc; i++ ) { if ( strcmp( "--", argv[ i ] ) == 0 && i + 1 < argc ) { nSubCommandArgc = i + 1; break; } } if ( nSubCommandArgc == 0 ) { s_ReaperLog.errorf( "No sub-command!" ); return 1; } // Mirror some of the busy work we do in ProcessPreSpawn, // in case someone else wants to use this utility. Process::ResetSignals(); std::array nExcludedFds = {{ STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO, }}; Process::CloseAllFds( nExcludedFds ); // We typically don't make a new sid, as we want to keep the same stdin/stdout // Don't really care about it for pgroup reasons, as processes can leave those. if ( bNewSession ) setsid(); // Set up a signal handler, so that SIGTERM, etc goes // and kills all the children. struct sigaction reaperSignalHandler{}; reaperSignalHandler.sa_handler = []( int nSignal ) { switch ( nSignal ) { case SIGHUP: case SIGINT: case SIGQUIT: case SIGTERM: sigaction( SIGHUP, nullptr, nullptr ); sigaction( SIGINT, nullptr, nullptr ); sigaction( SIGQUIT, nullptr, nullptr ); sigaction( SIGTERM, nullptr, nullptr ); if ( s_bRun ) { s_ReaperLog.infof( "Parent of gamescopereaper was killed. Killing children." ); s_bRun = false; Process::KillAllChildren( getpid(), SIGTERM ); } break; } }; sigaction( SIGHUP, &reaperSignalHandler, nullptr ); sigaction( SIGINT, &reaperSignalHandler, nullptr ); sigaction( SIGQUIT, &reaperSignalHandler, nullptr ); sigaction( SIGTERM, &reaperSignalHandler, nullptr ); // (Don't Lose) The Children Process::BecomeSubreaper(); Process::SetDeathSignal( SIGTERM ); pid_t nPrimaryChild = Process::SpawnProcess( &argv[ nSubCommandArgc ] ); assert( nPrimaryChild != 0 ); if ( nPrimaryChild > 0 ) { // Wait for the primary child to die, then forward the death signal to // all of the other children, if we aren't in a PID namespace. Process::WaitForAllChildren( nPrimaryChild ); if ( bRespawn ) { while ( s_bRun ) { s_ReaperLog.infof( "\"%s\" process shut down. Restarting.", argv[ nSubCommandArgc ] ); nPrimaryChild = Process::SpawnProcess( &argv[ nSubCommandArgc ] ); Process::WaitForAllChildren( nPrimaryChild ); } } s_bRun = false; Process::KillAllChildren( getpid(), SIGTERM ); Process::WaitForAllChildren(); return 0; } else { s_ReaperLog.errorf_errno( "Failed to create child process \"%s\" in reaper.", argv[ nSubCommandArgc ] ); s_bRun = false; Process::KillAllChildren( getpid(), SIGTERM ); Process::WaitForAllChildren(); return 1; } } } int main( int argc, char **argv ) { return gamescope::GamescopeReaperProcess( argc, argv ); } ValveSoftware-gamescope-eb620ab/src/Apps/gamescopestream.cpp000066400000000000000000000471641502457270500243260ustar00rootroot00000000000000/////////////////////////////////////////////////////////////////////////////////////////////////////// // Gracefully butchered from https://docs.pipewire.org/spa_2examples_2adapter-control_8c-example.html // by Wim Taymans under MIT /////////////////////////////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include #include #include #include #include #include #include #define DEFAULT_WIDTH 1280 #define DEFAULT_HEIGHT 720 #define MAX_BUFFERS 64 #include #include #include #define WAYLAND_NULL() [] ( void *pData, Args... args ) { } #include #include #include "pipewire_gamescope.hpp" #include "log.hpp" static LogScope s_StreamLog( "stream" ); void spa_gamescopestream_log( struct spa_debug_context *ctx, const char *fmt, ... ) { va_list args; va_start( args, fmt ); s_StreamLog.vlogf( LOG_DEBUG, fmt, args ); va_end( args ); } struct spa_debug_context s_SpaDebugContext = { .log = spa_gamescopestream_log, }; struct pw_version { int major; int minor; int micro; }; static uint32_t spa_format_to_drm(uint32_t spa_format) { switch (spa_format) { case SPA_VIDEO_FORMAT_NV12: return DRM_FORMAT_NV12; default: case SPA_VIDEO_FORMAT_BGR: return DRM_FORMAT_XRGB8888; } } struct data { const char *path; wl_display *pDisplay = nullptr; wl_compositor *pCompositor = nullptr; zwp_linux_dmabuf_v1 *pLinuxDmabuf = nullptr; libdecor *pDecor = nullptr; libdecor_frame *pFrame = nullptr; wl_surface *pSurface = nullptr; wl_buffer *pWaylandBuffer = nullptr; struct pw_main_loop *loop; struct spa_source *reneg; struct pw_stream *stream; struct spa_hook stream_listener; struct spa_video_info format; int32_t stride; struct spa_rectangle size; bool needs_decor_commit; uint32_t appid; std::unordered_map> m_FormatModifiers; int counter; }; static void handle_events( struct data *pData ) { wl_display_flush( pData->pDisplay ); if ( wl_display_prepare_read( pData->pDisplay ) == 0 ) { int nRet = 0; pollfd pollfd = { .fd = wl_display_get_fd( pData->pDisplay ), .events = POLLIN, }; do { nRet = poll( &pollfd, 1, 0 ); } while ( nRet < 0 && ( errno == EINTR || errno == EAGAIN ) ); if ( nRet > 0 ) wl_display_read_events( pData->pDisplay ); else wl_display_cancel_read( pData->pDisplay ); } wl_display_dispatch_pending( pData->pDisplay ); } static struct spa_pod *build_format(struct data *data, struct spa_pod_builder *b, enum spa_video_format format, uint64_t *modifiers, int modifier_count) { struct spa_pod_frame f[3]; int i, c; spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); spa_pod_builder_add(b, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), 0); spa_pod_builder_add(b, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), 0); /* format */ spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), 0); /* modifiers */ if (modifier_count == 1 && modifiers[0] == DRM_FORMAT_MOD_INVALID) { // we only support implicit modifiers, use shortpath to skip fixation phase spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY); spa_pod_builder_long(b, modifiers[0]); } else if (modifier_count > 0) { // build an enumeration of modifiers spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE); spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0); // modifiers from the array for (i = 0, c = 0; i < modifier_count; i++) { spa_pod_builder_long(b, modifiers[i]); if (c++ == 0) spa_pod_builder_long(b, modifiers[i]); } spa_pod_builder_pop(b, &f[1]); } spa_rectangle default_size = SPA_RECTANGLE(DEFAULT_WIDTH, DEFAULT_HEIGHT); spa_rectangle min_size = SPA_RECTANGLE(1,1); spa_rectangle max_size = SPA_RECTANGLE(65535, 65535); spa_fraction frac1 = SPA_FRACTION(25,1); spa_fraction frac2 = SPA_FRACTION(0,1); spa_fraction frac3 = SPA_FRACTION(30,1); spa_pod_builder_add(b, SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle( &default_size, &min_size, &max_size), 0); spa_pod_builder_add(b, SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction( &frac1, &frac2, &frac3), 0); if (data->appid) spa_pod_builder_add(b, SPA_FORMAT_VIDEO_gamescope_focus_appid, SPA_POD_Long(uint64_t(data->appid)), 0); return (spa_pod *)spa_pod_builder_pop(b, &f[0]); } void commit_libdecor( struct data *data, libdecor_configuration *pConfiguration ) { uint32_t uWidth = data->format.info.raw.size.width; uint32_t uHeight = data->format.info.raw.size.height; uWidth = uWidth ? uWidth : 1280; uHeight = uHeight ? uHeight : 720; libdecor_state *pState = libdecor_state_new( uWidth, uHeight ); libdecor_frame_commit( data->pFrame, pState, pConfiguration ); libdecor_state_free( pState ); data->needs_decor_commit = false; } /* our data processing function is in general: * * struct pw_buffer *b; * b = pw_stream_dequeue_buffer(stream); * * .. do stuff with buffer ... * * pw_stream_queue_buffer(stream, b); */ static void on_process(void *_data) { struct data *data = (struct data *)_data; struct pw_stream *stream = data->stream; struct pw_buffer *b; struct spa_buffer *buf; b = nullptr; /* dequeue and queue old buffers, use the last available * buffer */ while (true) { struct pw_buffer *t; if ((t = pw_stream_dequeue_buffer(stream)) == nullptr) break; if (b) pw_stream_queue_buffer(stream, b); b = t; } if (b == nullptr) { pw_log_warn("out of buffers: %m"); return; } buf = b->buffer; pw_log_info("new buffer %p", buf); handle_events(data); zwp_linux_buffer_params_v1 *pBufferParams = zwp_linux_dmabuf_v1_create_params( data->pLinuxDmabuf ); if ( !pBufferParams ) { pw_stream_queue_buffer(stream, b); return; } for ( uint32_t i = 0; i < buf->n_datas; i++ ) { zwp_linux_buffer_params_v1_add( pBufferParams, buf->datas[i].fd, i, buf->datas[i].chunk->offset, buf->datas[i].chunk->stride, data->format.info.raw.modifier >> 32, data->format.info.raw.modifier & 0xffffffff); } uint32_t uDrmFormat = spa_format_to_drm(data->format.info.raw.format); wl_buffer *pImportedBuffer = zwp_linux_buffer_params_v1_create_immed( pBufferParams, data->format.info.raw.size.width, data->format.info.raw.size.height, uDrmFormat, 0u ); assert( pImportedBuffer ); struct StreamBuffer { struct data *pData = nullptr; wl_buffer *pWaylandBuffer = nullptr; pw_buffer *pPipewireBuffer = nullptr; }; StreamBuffer *pStreamBuffer = new StreamBuffer { .pData = data, .pWaylandBuffer = pImportedBuffer, .pPipewireBuffer = b, }; static constexpr wl_buffer_listener s_BufferListener = { .release = []( void *pData, wl_buffer *pBuffer ) { StreamBuffer *pStreamBuffer = ( StreamBuffer * )pData; pw_stream_queue_buffer( pStreamBuffer->pData->stream, pStreamBuffer->pPipewireBuffer ); wl_buffer_destroy( pStreamBuffer->pWaylandBuffer ); delete pStreamBuffer; }, }; wl_buffer_add_listener( pImportedBuffer, &s_BufferListener, pStreamBuffer ); wl_surface_attach( data->pSurface, pImportedBuffer, 0, 0 ); wl_surface_damage( data->pSurface, 0, 0, INT32_MAX, INT32_MAX ); wl_surface_set_buffer_scale( data->pSurface, 1 ); if (data->needs_decor_commit) commit_libdecor( data, nullptr ); wl_surface_commit( data->pSurface ); wl_display_flush( data->pDisplay ); } static void on_stream_state_changed(void *_data, enum pw_stream_state old, enum pw_stream_state state, const char *error) { struct data *data = (struct data *)_data; s_StreamLog.debugf( "stream state: \"%s\"", pw_stream_state_as_string(state) ); if ( error ) s_StreamLog.errorf( "error: \"%s\"", error ); switch (state) { case PW_STREAM_STATE_UNCONNECTED: pw_main_loop_quit(data->loop); break; case PW_STREAM_STATE_PAUSED: break; case PW_STREAM_STATE_STREAMING: default: break; } } /* Be notified when the stream param changes. We're only looking at the * format changes. * * We are now supposed to call pw_stream_finish_format() with success or * failure, depending on if we can support the format. Because we gave * a list of supported formats, this should be ok. * * As part of pw_stream_finish_format() we can provide parameters that * will control the buffer memory allocation. This includes the metadata * that we would like on our buffer, the size, alignment, etc. */ static void on_stream_param_changed(void *_data, uint32_t id, const struct spa_pod *param) { struct data *data = (struct data *)_data; struct pw_stream *stream = data->stream; uint8_t params_buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); const struct spa_pod *params[1]; /* nullptr means to clear the format */ if (param == nullptr || id != SPA_PARAM_Format) return; s_StreamLog.debugf( "got format:" ); spa_debugc_format(&s_SpaDebugContext, 2, nullptr, param); if (spa_format_parse(param, &data->format.media_type, &data->format.media_subtype) < 0) return; if (data->format.media_type != SPA_MEDIA_TYPE_video || data->format.media_subtype != SPA_MEDIA_SUBTYPE_raw) return; /* call a helper function to parse the format for us. */ spa_format_video_raw_parse(param, &data->format.info.raw); data->size = data->format.info.raw.size; uint32_t drm_format = spa_format_to_drm(data->format.info.raw.format); if (drm_format == DRM_FORMAT_INVALID) { pw_stream_set_error(stream, -EINVAL, "unknown pixel format"); return; } if (data->size.width == 0 || data->size.height == 0) { pw_stream_set_error(stream, -EINVAL, "invalid size"); return; } data->stride = SPA_ROUND_UP_N( data->size.width * 4, 4 ); /* a SPA_TYPE_OBJECT_ParamBuffers object defines the acceptable size, * number, stride etc of the buffers */ params[0] = (const struct spa_pod *) spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(8, 2, MAX_BUFFERS), SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), SPA_PARAM_BUFFERS_size, SPA_POD_Int(data->stride * data->size.height), SPA_PARAM_BUFFERS_stride, SPA_POD_Int(data->stride), SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1<m_FormatModifiers.contains(DRM_FORMAT_XRGB8888)) params[n_params++] = build_format( data, b, SPA_VIDEO_FORMAT_BGRx, data->m_FormatModifiers[DRM_FORMAT_XRGB8888].data(), uint32_t( data->m_FormatModifiers[DRM_FORMAT_XRGB8888].size() ) ); params[n_params++] = build_format( data, b, SPA_VIDEO_FORMAT_BGRx, nullptr, 0 ); for (int i=0; i < n_params; i++) spa_debugc_format(&s_SpaDebugContext, 2, NULL, params[i]); return n_params; } static void reneg_format(void *_data, uint64_t expiration) { struct data *data = (struct data*) _data; uint8_t buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); const struct spa_pod *params[2]; uint32_t n_params; if (data->format.info.raw.format == 0) return; s_StreamLog.debugf( "renegotiate formats:" ); n_params = build_formats(data, &b, params); pw_stream_update_params(data->stream, params, n_params); } static void do_quit(void *userdata, int signal_number) { struct data *data = (struct data *)userdata; pw_main_loop_quit(data->loop); } int main(int argc, char *argv[]) { struct data data = { 0, }; const struct spa_pod *params[2]; uint8_t buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); struct pw_properties *props; int res, n_params; pw_init(&argc, &argv); /* create a main loop */ data.loop = pw_main_loop_new(nullptr); pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGINT, do_quit, &data); pw_loop_add_signal(pw_main_loop_get_loop(data.loop), SIGTERM, do_quit, &data); /* create a simple stream, the simple stream manages to core and remote * objects for you if you don't need to deal with them * * If you plan to autoconnect your stream, you need to provide at least * media, category and role properties * * Pass your events and a user_data pointer as the last arguments. This * will inform you about the stream state. The most important event * you need to listen to is the process event where you need to consume * the data provided to you. */ props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Camera", nullptr), data.appid = argc > 1 ? atoi(argv[1]) : 0; data.path = argc > 2 ? argv[2] : "gamescope"; if (data.path) /* Set stream target if given on command line */ pw_properties_set(props, PW_KEY_TARGET_OBJECT, data.path); data.stream = pw_stream_new_simple( pw_main_loop_get_loop(data.loop), "video-play-fixate", props, &stream_events, &data); // if ( !( data.pDisplay = wl_display_connect( nullptr ) ) ) return -1; wl_registry *pRegistry; if ( !( pRegistry = wl_display_get_registry( data.pDisplay ) ) ) return -1; static constexpr wl_registry_listener s_RegistryListener = { .global = [] ( void *pUserData, wl_registry *pRegistry, uint32_t uName, const char *pInterface, uint32_t uVersion ) { struct data *pData = (struct data *)pUserData; if ( !strcmp( pInterface, wl_compositor_interface.name ) && uVersion >= 4u ) { pData->pCompositor = (wl_compositor *)wl_registry_bind( pRegistry, uName, &wl_compositor_interface, 4u ); } else if ( !strcmp( pInterface, zwp_linux_dmabuf_v1_interface.name ) && uVersion >= 3 ) { pData->pLinuxDmabuf = (zwp_linux_dmabuf_v1 *)wl_registry_bind( pRegistry, uName, &zwp_linux_dmabuf_v1_interface, 3u ); static constexpr zwp_linux_dmabuf_v1_listener s_Listener = { .format = WAYLAND_NULL(), // Formats are also advertised by the modifier event, ignore them here. .modifier = [] ( void *pUserData, zwp_linux_dmabuf_v1 *pDmabuf, uint32_t uFormat, uint32_t uModifierHi, uint32_t uModifierLo ) { uint64_t ulModifier = ( uint64_t( uModifierHi ) << 32 ) | uModifierLo; struct data *pData = (struct data *)pUserData; if ( ulModifier != DRM_FORMAT_MOD_INVALID ) pData->m_FormatModifiers[ uFormat ].emplace_back( ulModifier ); }, }; zwp_linux_dmabuf_v1_add_listener( pData->pLinuxDmabuf, &s_Listener, pData ); } }, .global_remove = WAYLAND_NULL(), }; wl_registry_add_listener( pRegistry, &s_RegistryListener, (void *)&data ); wl_display_roundtrip( data.pDisplay ); if ( !data.pCompositor || !data.pLinuxDmabuf ) return -1; // Grab stuff from any extra bindings/listeners we set up, eg. format/modifiers. wl_display_roundtrip( data.pDisplay ); wl_registry_destroy( pRegistry ); pRegistry = nullptr; static libdecor_interface s_LibDecorInterface = { .error = []( libdecor *pContext, libdecor_error eError, const char *pMessage ) { s_StreamLog.errorf( "libdecor: %s", pMessage ); }, }; data.pDecor = libdecor_new( data.pDisplay, &s_LibDecorInterface ); if ( !data.pDecor ) return -1; static libdecor_frame_interface s_LibDecorFrameInterface { .configure = []( libdecor_frame *pFrame, libdecor_configuration *pConfiguration, void *pUserData ) { struct data *pData = (struct data *)pUserData; commit_libdecor( pData, pConfiguration ); }, .close = []( libdecor_frame *pFrame, void *pUserData ) { raise( SIGTERM ); }, .commit = []( libdecor_frame *pFrame, void *pUserData ) { struct data *pData = (struct data *)pUserData; pData->needs_decor_commit = true; }, .dismiss_popup = []( libdecor_frame *pFrame, const char *pSeatName, void *pUserData ) { }, }; data.pSurface = wl_compositor_create_surface( data.pCompositor ); data.pFrame = libdecor_decorate( data.pDecor, data.pSurface, &s_LibDecorFrameInterface, &data ); libdecor_frame_set_title( data.pFrame, "Gamescope Pipewire Stream" ); libdecor_frame_set_app_id( data.pFrame, "gamescopestream" ); libdecor_frame_map( data.pFrame ); wl_surface_commit( data.pSurface ); wl_display_roundtrip( data.pDisplay ); // /* build the extra parameters to connect with. To connect, we can provide * a list of supported formats. We use a builder that writes the param * object to the stack. */ s_StreamLog.debugf( "supported formats:" ); n_params = build_formats(&data, &b, params); /* now connect the stream, we need a direction (input/output), * an optional target node to connect to, some flags and parameters */ if ((res = pw_stream_connect(data.stream, PW_DIRECTION_INPUT, PW_ID_ANY, pw_stream_flags( PW_STREAM_FLAG_AUTOCONNECT | /* try to automatically connect this stream */ PW_STREAM_FLAG_MAP_BUFFERS), /* mmap the buffer data for us */ params, n_params)) /* extra parameters, see above */ < 0) { s_StreamLog.errorf( "can't connect: %s\n", spa_strerror(res) ); return -1; } data.reneg = pw_loop_add_event(pw_main_loop_get_loop(data.loop), reneg_format, &data); /* do things until we quit the mainloop */ pw_main_loop_run(data.loop); pw_stream_destroy(data.stream); pw_main_loop_destroy(data.loop); // TODO: cleanup wayland pw_deinit(); return 0; }ValveSoftware-gamescope-eb620ab/src/Backends/000077500000000000000000000000001502457270500212365ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/src/Backends/DRMBackend.cpp000066400000000000000000003743221502457270500236470ustar00rootroot00000000000000// DRM output stuff #include "Script/Script.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "backend.h" #include "color_helpers.h" #include "Utils/Defer.h" #include "drm_include.h" #include "edid.h" #include "gamescope_shared.h" #include "gpuvis_trace_utils.h" #include "log.hpp" #include "main.hpp" #include "modegen.hpp" #include "rendervulkan.hpp" #include "steamcompmgr.hpp" #include "vblankmanager.hpp" #include "wlserver.hpp" #include "refresh_rate.h" #include #include "wlr_begin.hpp" #include #include #include "libdisplay-info/info.h" #include "libdisplay-info/edid.h" #include "libdisplay-info/cta.h" #include "wlr_end.hpp" #include "gamescope-control-protocol.h" static constexpr bool k_bUseCursorPlane = false; extern int g_nPreferredOutputWidth; extern int g_nPreferredOutputHeight; gamescope::ConVar cv_drm_single_plane_optimizations( "drm_single_plane_optimizations", true, "Whether or not to enable optimizations for single plane usage." ); gamescope::ConVar cv_drm_debug_disable_shaper_and_3dlut( "drm_debug_disable_shaper_and_3dlut", false, "Shaper + 3DLUT chicken bit. (Force disable/DEFAULT, no logic change)" ); gamescope::ConVar cv_drm_debug_disable_degamma_tf( "drm_debug_disable_degamma_tf", false, "Degamma chicken bit. (Forces DEGAMMA_TF to DEFAULT, does not affect other logic)" ); gamescope::ConVar cv_drm_debug_disable_regamma_tf( "drm_debug_disable_regamma_tf", false, "Regamma chicken bit. (Forces REGAMMA_TF to DEFAULT, does not affect other logic)" ); gamescope::ConVar cv_drm_debug_disable_output_tf( "drm_debug_disable_output_tf", false, "Force default (identity) output TF, affects other logic. Not a property directly." ); gamescope::ConVar cv_drm_debug_disable_blend_tf( "drm_debug_disable_blend_tf", false, "Blending chicken bit. (Forces BLEND_TF to DEFAULT, does not affect other logic)" ); gamescope::ConVar cv_drm_debug_disable_ctm( "drm_debug_disable_ctm", false, "CTM chicken bit. (Forces CTM off, does not affect other logic)" ); gamescope::ConVar cv_drm_debug_disable_color_encoding( "drm_debug_disable_color_encoding", false, "YUV Color Encoding chicken bit. (Forces COLOR_ENCODING to DEFAULT, does not affect other logic)" ); gamescope::ConVar cv_drm_debug_disable_color_range( "drm_debug_disable_color_range", false, "YUV Color Range chicken bit. (Forces COLOR_RANGE to DEFAULT, does not affect other logic)" ); gamescope::ConVar cv_drm_debug_disable_explicit_sync( "drm_debug_disable_explicit_sync", false, "Force disable explicit sync on the DRM backend." ); gamescope::ConVar cv_drm_debug_disable_in_fence_fd( "drm_debug_disable_in_fence_fd", false, "Force disable IN_FENCE_FD being set to avoid over-synchronization on the DRM backend." ); gamescope::ConVar cv_drm_allow_dynamic_modes_for_external_display( "drm_allow_dynamic_modes_for_external_display", false, "Allow dynamic mode/refresh rate switching for external displays." ); int HackyDRMPresent( const FrameInfo_t *pFrameInfo, bool bAsync ); enum GamescopeBroadcastRGBMode_t : uint32_t { GAMESCOPE_BROADCAST_RGB_MODE_AUTOMATIC = 0, GAMESCOPE_BROADCAST_RGB_MODE_FULL = 1, GAMESCOPE_BROADCAST_RGB_MODE_LIMITED = 2, }; struct saved_mode { int width; int height; int refresh; GamescopeBroadcastRGBMode_t broadcast_mode; }; gamescope::ConVar cv_drm_ignore_internal_connectors( "drm_ignore_internal_connectors", false, "Disable internal displays for good, for debugging." ); namespace gamescope { class CDRMPlane; class CDRMCRTC; class CDRMConnector; } struct drm_t { bool bUseLiftoff; int fd = -1; int preferred_width, preferred_height, preferred_refresh; uint64_t cursor_width, cursor_height; bool allow_modifiers; struct wlr_drm_format_set formats; std::vector< std::unique_ptr< gamescope::CDRMPlane > > planes; std::vector< std::unique_ptr< gamescope::CDRMCRTC > > crtcs; std::unordered_map< uint32_t, gamescope::CDRMConnector > connectors; gamescope::CDRMPlane *pPrimaryPlane; gamescope::CDRMCRTC *pCRTC; gamescope::CDRMConnector *pConnector; struct wlr_drm_format_set primary_formats; drmModeAtomicReq *req; uint32_t flags; struct liftoff_device *lo_device; struct liftoff_output *lo_output; struct liftoff_layer *lo_layers[ k_nMaxLayers ]; std::shared_ptr sdr_static_metadata; struct drm_state_t { std::shared_ptr mode_id; uint32_t color_mgmt_serial; std::shared_ptr lut3d_id[ EOTF_Count ]; std::shared_ptr shaperlut_id[ EOTF_Count ]; amdgpu_transfer_function output_tf = AMDGPU_TRANSFER_FUNCTION_DEFAULT; } current, pending; // FBs in the atomic request, but not yet submitted to KMS // Accessed only on req thread std::vector> m_FbIdsInRequest; // FBs currently queued to go on screen. // May be accessed by page flip handler thread and req thread, thus mutex. std::mutex m_QueuedFbIdsMutex; std::vector> m_QueuedFbIds; // FBs currently on screen. // Accessed only on page flip handler thread. std::mutex m_mutVisibleFbIds; std::vector> m_VisibleFbIds; std::atomic < uint32_t > uPendingFlipCount = { 0 }; std::atomic < bool > paused = { false }; std::atomic < int > out_of_date = { false }; std::atomic < bool > needs_modeset = { false }; std::unordered_map< std::string, int > connector_priorities; char *device_name = nullptr; }; void drm_drop_fbid( struct drm_t *drm, uint32_t fbid ); bool drm_set_mode( struct drm_t *drm, const drmModeModeInfo *mode ); using namespace std::literals; struct drm_t g_DRM = {}; namespace gamescope { class CDRMBackend; std::tuple GetKernelVersion() { utsname name; if ( uname( &name ) != 0 ) return std::make_tuple( 0, 0, 0 ); std::vector szVersionParts = Split( name.release, "." ); uint32_t uVersion[3] = { 0 }; for ( size_t i = 0; i < szVersionParts.size() && i < 3; i++ ) { auto oPart = Parse( szVersionParts[i] ); if ( !oPart ) break; uVersion[i] = *oPart; } return std::make_tuple( uVersion[0], uVersion[1], uVersion[2] ); } // Get a DRM mode in mHz // Taken from wlroots, but we can't access it as we don't // use the drm backend. static int32_t GetModeRefresh(const drmModeModeInfo *mode) { int32_t nRefresh = (mode->clock * 1'000'000ll / mode->htotal + mode->vtotal / 2) / mode->vtotal; if (mode->flags & DRM_MODE_FLAG_INTERLACE) nRefresh *= 2; if (mode->flags & DRM_MODE_FLAG_DBLSCAN) nRefresh /= 2; if (mode->vscan > 1) nRefresh /= mode->vscan; return nRefresh; } template using CAutoDeletePtr = std::unique_ptr; //////////////////////////////////////// // DRM Object Wrappers + State Trackers //////////////////////////////////////// struct DRMObjectRawProperty { uint32_t uPropertyId = 0ul; uint64_t ulValue = 0ul; }; using DRMObjectRawProperties = std::unordered_map; class CDRMAtomicObject { public: CDRMAtomicObject( uint32_t ulObjectId ); uint32_t GetObjectId() const { return m_ulObjectId; } // No copy or move constructors. CDRMAtomicObject( const CDRMAtomicObject& ) = delete; CDRMAtomicObject& operator=( const CDRMAtomicObject& ) = delete; CDRMAtomicObject( CDRMAtomicObject&& ) = delete; CDRMAtomicObject& operator=( CDRMAtomicObject&& ) = delete; protected: uint32_t m_ulObjectId = 0ul; }; template < uint32_t DRMObjectType > class CDRMAtomicTypedObject : public CDRMAtomicObject { public: CDRMAtomicTypedObject( uint32_t ulObjectId ); protected: std::optional GetRawProperties(); }; class CDRMAtomicProperty { public: CDRMAtomicProperty( CDRMAtomicObject *pObject, DRMObjectRawProperty rawProperty ); static std::optional Instantiate( const char *pszName, CDRMAtomicObject *pObject, const DRMObjectRawProperties& rawProperties ); uint64_t GetPendingValue() const { return m_ulPendingValue; } uint64_t GetCurrentValue() const { return m_ulCurrentValue; } uint64_t GetInitialValue() const { return m_ulInitialValue; } int SetPendingValue( drmModeAtomicReq *pRequest, uint64_t ulValue, bool bForce ); void OnCommit(); void Rollback(); private: CDRMAtomicObject *m_pObject = nullptr; uint32_t m_uPropertyId = 0u; uint64_t m_ulPendingValue = 0ul; uint64_t m_ulCurrentValue = 0ul; uint64_t m_ulInitialValue = 0ul; }; class CDRMPlane final : public CDRMAtomicTypedObject { public: // Takes ownership of pPlane. CDRMPlane( drmModePlane *pPlane ); void RefreshState(); drmModePlane *GetModePlane() const { return m_pPlane.get(); } struct PlaneProperties { std::optional *begin() { return &FB_ID; } std::optional *end() { return &DUMMY_END; } std::optional type; // Immutable std::optional IN_FORMATS; // Immutable std::optional FB_ID; std::optional IN_FENCE_FD; std::optional CRTC_ID; std::optional SRC_X; std::optional SRC_Y; std::optional SRC_W; std::optional SRC_H; std::optional CRTC_X; std::optional CRTC_Y; std::optional CRTC_W; std::optional CRTC_H; std::optional zpos; std::optional alpha; std::optional rotation; std::optional COLOR_ENCODING; std::optional COLOR_RANGE; std::optional AMD_PLANE_DEGAMMA_TF; std::optional AMD_PLANE_DEGAMMA_LUT; std::optional AMD_PLANE_CTM; std::optional AMD_PLANE_HDR_MULT; std::optional AMD_PLANE_SHAPER_LUT; std::optional AMD_PLANE_SHAPER_TF; std::optional AMD_PLANE_LUT3D; std::optional AMD_PLANE_BLEND_TF; std::optional AMD_PLANE_BLEND_LUT; std::optional DUMMY_END; }; PlaneProperties &GetProperties() { return m_Props; } const PlaneProperties &GetProperties() const { return m_Props; } private: CAutoDeletePtr m_pPlane; PlaneProperties m_Props; }; class CDRMCRTC final : public CDRMAtomicTypedObject { public: // Takes ownership of pCRTC. CDRMCRTC( drmModeCrtc *pCRTC, uint32_t uCRTCMask ); void RefreshState(); uint32_t GetCRTCMask() const { return m_uCRTCMask; } struct CRTCProperties { std::optional *begin() { return &ACTIVE; } std::optional *end() { return &DUMMY_END; } std::optional ACTIVE; std::optional MODE_ID; std::optional GAMMA_LUT; std::optional DEGAMMA_LUT; std::optional CTM; std::optional VRR_ENABLED; std::optional OUT_FENCE_PTR; std::optional AMD_CRTC_REGAMMA_TF; std::optional DUMMY_END; }; CRTCProperties &GetProperties() { return m_Props; } const CRTCProperties &GetProperties() const { return m_Props; } private: CAutoDeletePtr m_pCRTC; uint32_t m_uCRTCMask = 0u; CRTCProperties m_Props; }; class CDRMConnector final : public CBaseBackendConnector, public CDRMAtomicTypedObject { public: CDRMConnector( CDRMBackend *pBackend, drmModeConnector *pConnector ); void RefreshState(); struct ConnectorProperties { std::optional *begin() { return &CRTC_ID; } std::optional *end() { return &DUMMY_END; } std::optional CRTC_ID; std::optional Colorspace; std::optional content_type; // "content type" with space! std::optional panel_orientation; // "panel orientation" with space! std::optional HDR_OUTPUT_METADATA; std::optional vrr_capable; std::optional EDID; std::optional Broadcast_RGB; std::optional DUMMY_END; }; ConnectorProperties &GetProperties() { return m_Props; } const ConnectorProperties &GetProperties() const { return m_Props; } drmModeConnector *GetModeConnector() { return m_pConnector.get(); } const char *GetName() const override { return m_Mutable.szName; } const char *GetMake() const override { return m_Mutable.pszMake; } const char *GetModel() const override { return m_Mutable.szModel; } const char *GetDataString() const { return m_Mutable.szDataString; } uint32_t GetPossibleCRTCMask() const { return m_Mutable.uPossibleCRTCMask; } std::span GetValidDynamicRefreshRates() const override { return m_Mutable.ValidDynamicRefreshRates; } const displaycolorimetry_t& GetDisplayColorimetry() const { return m_Mutable.DisplayColorimetry; } std::span GetRawEDID() const override { return std::span{ m_Mutable.EdidData.begin(), m_Mutable.EdidData.end() }; } bool SupportsHDR10() const { return !!GetProperties().Colorspace && !!GetProperties().HDR_OUTPUT_METADATA && GetHDRInfo().IsHDR10(); } bool SupportsHDRG22() const { return GetHDRInfo().IsHDRG22(); } ////////////////////////////////////// // IBackendConnector implementation ////////////////////////////////////// GamescopeScreenType GetScreenType() const override { if ( m_pConnector->connector_type == DRM_MODE_CONNECTOR_eDP || m_pConnector->connector_type == DRM_MODE_CONNECTOR_LVDS || m_pConnector->connector_type == DRM_MODE_CONNECTOR_DSI ) return GAMESCOPE_SCREEN_TYPE_INTERNAL; return GAMESCOPE_SCREEN_TYPE_EXTERNAL; } GamescopePanelOrientation GetCurrentOrientation() const override { return m_ChosenOrientation; } bool SupportsHDR() const override { return SupportsHDR10() || SupportsHDRG22(); } bool IsHDRActive() const override { if ( SupportsHDR10() ) { return GetProperties().Colorspace->GetCurrentValue() == DRM_MODE_COLORIMETRY_BT2020_RGB; } else if ( SupportsHDRG22() ) { return true; } return false; } const BackendConnectorHDRInfo &GetHDRInfo() const override { return m_Mutable.HDR; } virtual bool IsVRRActive() const override { if ( !g_DRM.pCRTC || !g_DRM.pCRTC->GetProperties().VRR_ENABLED ) return false; return !!g_DRM.pCRTC->GetProperties().VRR_ENABLED->GetCurrentValue(); } virtual std::span GetModes() const override { return m_Mutable.BackendModes; } bool SupportsVRR() const override { return this->GetProperties().vrr_capable && !!this->GetProperties().vrr_capable->GetCurrentValue(); } void GetNativeColorimetry( bool bHDR, displaycolorimetry_t *displayColorimetry, EOTF *displayEOTF, displaycolorimetry_t *outputEncodingColorimetry, EOTF *outputEncodingEOTF ) const override { *displayColorimetry = GetDisplayColorimetry(); *displayEOTF = EOTF_Gamma22; if ( bHDR && GetHDRInfo().IsHDR10() ) { // For HDR10 output, expected content colorspace != native colorspace. *outputEncodingColorimetry = displaycolorimetry_2020; *outputEncodingEOTF = GetHDRInfo().eOutputEncodingEOTF; } else { *outputEncodingColorimetry = GetDisplayColorimetry(); *outputEncodingEOTF = EOTF_Gamma22; } } virtual int Present( const FrameInfo_t *pFrameInfo, bool bAsync ) override; using DRMModeGenerator = std::function; const DRMModeGenerator &GetModeGenerator() const { return m_Mutable.fnDynamicModeGenerator; } void UpdateEffectiveOrientation( const drmModeModeInfo *pMode ); private: void ParseEDID(); CDRMBackend *m_pBackend = nullptr; CAutoDeletePtr m_pConnector; struct MutableConnectorState { int nDefaultRefresh = 0; uint32_t uPossibleCRTCMask = 0u; char szName[32]{}; char szMakePNP[4]{}; char szModel[16]{}; char szDataString[16]{}; const char *pszMake = ""; // Not owned, no free. This is a pointer to pnp db or szMakePNP. DRMModeGenerator fnDynamicModeGenerator; std::vector ValidDynamicRefreshRates{}; std::vector EdidData; // Raw, unmodified. std::vector BackendModes; displaycolorimetry_t DisplayColorimetry = displaycolorimetry_709; BackendConnectorHDRInfo HDR; } m_Mutable; GamescopePanelOrientation m_ChosenOrientation = GAMESCOPE_PANEL_ORIENTATION_AUTO; ConnectorProperties m_Props; }; class CDRMFb final : public CBaseBackendFb { public: CDRMFb( uint32_t uFbId ); ~CDRMFb(); uint32_t GetFbId() const { return m_uFbId; } private: uint32_t m_uFbId = 0; }; } uint32_t g_nDRMFormat = DRM_FORMAT_INVALID; uint32_t g_nDRMFormatOverlay = DRM_FORMAT_INVALID; // for partial composition, we may have more limited formats than base planes + alpha. bool g_bRotated = false; extern bool g_bDebugLayers; struct DRMPresentCtx { uint64_t ulPendingFlipCount = 0; }; extern gamescope::ConVar cv_composite_force; extern bool g_bColorSliderInUse; extern bool fadingOut; extern std::string g_reshade_effect; #ifndef DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP #define DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP 0x15 #endif bool drm_update_color_mgmt(struct drm_t *drm); bool drm_supports_color_mgmt(struct drm_t *drm); bool drm_set_connector( struct drm_t *drm, gamescope::CDRMConnector *conn ); struct drm_color_ctm2 { /* * Conversion matrix in S31.32 sign-magnitude * (not two's complement!) format. */ __u64 matrix[12]; }; bool g_bSupportsAsyncFlips = false; bool g_bSupportsSyncObjs = false; extern gamescope::GamescopeModeGeneration g_eGamescopeModeGeneration; extern GamescopePanelOrientation g_DesiredInternalOrientation; extern bool g_bForceDisableColorMgmt; static LogScope drm_log( "drm" ); static LogScope liftoff_log_scope( "liftoff" ); static std::unordered_map< std::string, std::string > pnps = {}; static void drm_unset_mode( struct drm_t *drm ); static void drm_unset_connector( struct drm_t *drm ); static constexpr uint32_t s_kSteamDeckLCDRates[] = { 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, }; static constexpr uint32_t s_kSteamDeckOLEDRates[] = { 45, 47, 48, 49, 50, 51, 53, 55, 56, 59, 60, 62, 64, 65, 66, 68, 72, 73, 76, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, }; void update_connector_display_info_wl(struct drm_t *drm) { wlserver_lock(); for ( const auto &control : wlserver.gamescope_controls ) { wlserver_send_gamescope_control( control ); } wlserver_unlock(); } inline uint64_t drm_calc_s31_32(float val) { // S31.32 sign-magnitude float integral = 0.0f; float fractional = modf( fabsf( val ), &integral ); union { struct { uint64_t fractional : 32; uint64_t integral : 31; uint64_t sign_part : 1; } s31_32_bits; uint64_t s31_32; } color; color.s31_32_bits.sign_part = val < 0 ? 1 : 0; color.s31_32_bits.integral = uint64_t( integral ); color.s31_32_bits.fractional = uint64_t( fractional * float( 1ull << 32 ) ); return color.s31_32; } static gamescope::CDRMCRTC *find_crtc_for_connector( struct drm_t *drm, gamescope::CDRMConnector *pConnector ) { for ( std::unique_ptr< gamescope::CDRMCRTC > &pCRTC : drm->crtcs ) { if ( pConnector->GetPossibleCRTCMask() & pCRTC->GetCRTCMask() ) return pCRTC.get(); } return nullptr; } static bool get_plane_formats( struct drm_t *drm, gamescope::CDRMPlane *pPlane, struct wlr_drm_format_set *pFormatSet ) { for ( uint32_t i = 0; i < pPlane->GetModePlane()->count_formats; i++ ) { const uint32_t uFormat = pPlane->GetModePlane()->formats[ i ]; wlr_drm_format_set_add( pFormatSet, uFormat, DRM_FORMAT_MOD_INVALID ); } if ( pPlane->GetProperties().IN_FORMATS ) { const uint64_t ulBlobId = pPlane->GetProperties().IN_FORMATS->GetCurrentValue(); drmModePropertyBlobRes *pBlob = drmModeGetPropertyBlob( drm->fd, ulBlobId ); if ( !pBlob ) { drm_log.errorf_errno("drmModeGetPropertyBlob(IN_FORMATS) failed"); return false; } defer( drmModeFreePropertyBlob( pBlob ) ); drm_format_modifier_blob *pModifierBlob = reinterpret_cast( pBlob->data ); uint32_t *pFormats = reinterpret_cast( reinterpret_cast( pBlob->data ) + pModifierBlob->formats_offset ); drm_format_modifier *pMods = reinterpret_cast( reinterpret_cast( pBlob->data ) + pModifierBlob->modifiers_offset ); for ( uint32_t i = 0; i < pModifierBlob->count_modifiers; i++ ) { for ( uint32_t j = 0; j < 64; j++ ) { if ( pMods[i].formats & ( uint64_t(1) << j ) ) wlr_drm_format_set_add( pFormatSet, pFormats[j + pMods[i].offset], pMods[i].modifier ); } } } return true; } static uint32_t pick_plane_format( const struct wlr_drm_format_set *formats, uint32_t Xformat, uint32_t Aformat ) { uint32_t result = DRM_FORMAT_INVALID; for ( size_t i = 0; i < formats->len; i++ ) { uint32_t fmt = formats->formats[i].format; if ( fmt == Xformat ) { // Prefer formats without alpha channel for main plane result = fmt; } else if ( result == DRM_FORMAT_INVALID && fmt == Aformat ) { result = fmt; } } return result; } /* Pick a primary plane that can be connected to the chosen CRTC. */ static gamescope::CDRMPlane *find_primary_plane(struct drm_t *drm) { if ( !drm->pCRTC ) return nullptr; for ( std::unique_ptr< gamescope::CDRMPlane > &pPlane : drm->planes ) { if ( pPlane->GetModePlane()->possible_crtcs & drm->pCRTC->GetCRTCMask() ) { if ( pPlane->GetProperties().type->GetCurrentValue() == DRM_PLANE_TYPE_PRIMARY ) return pPlane.get(); } } return nullptr; } extern void mangoapp_output_update( uint64_t vblanktime ); static void page_flip_handler(int fd, unsigned int frame, unsigned int sec, unsigned int usec, unsigned int crtc_id, void *data) { DRMPresentCtx *pCtx = reinterpret_cast( data ); // Make this const when we move into CDRMBackend. GetBackend()->GetCurrentConnector()->PresentationFeedback().m_uCompletedPresents = pCtx->ulPendingFlipCount; if ( !g_DRM.pCRTC ) return; if ( g_DRM.pCRTC->GetObjectId() != crtc_id ) return; static uint64_t ulLastVBlankTime = 0; // This is the last vblank time uint64_t vblanktime = sec * 1'000'000'000lu + usec * 1'000lu; GetVBlankTimer().MarkVBlank( vblanktime, true ); // TODO: get the fbids_queued instance from data if we ever have more than one in flight drm_log.debugf("page_flip_handler %" PRIu64 " delta: %" PRIu64, pCtx->ulPendingFlipCount, vblanktime - ulLastVBlankTime ); gpuvis_trace_printf("page_flip_handler %" PRIu64, pCtx->ulPendingFlipCount); ulLastVBlankTime = vblanktime; { std::scoped_lock lock{ g_DRM.m_QueuedFbIdsMutex, g_DRM.m_mutVisibleFbIds }; // Swap and clear from queue -> visible to avoid allocations. g_DRM.m_VisibleFbIds.swap( g_DRM.m_QueuedFbIds ); g_DRM.m_QueuedFbIds.clear(); } g_DRM.uPendingFlipCount--; g_DRM.uPendingFlipCount.notify_all(); mangoapp_output_update( vblanktime ); // Nudge so that steamcompmgr releases commits. nudge_steamcompmgr(); } void flip_handler_thread_run(void) { pthread_setname_np( pthread_self(), "gamescope-kms" ); struct pollfd pollfd = { .fd = g_DRM.fd, .events = POLLIN, }; while ( true ) { int ret = poll( &pollfd, 1, -1 ); if ( ret < 0 ) { drm_log.errorf_errno( "polling for DRM events failed" ); break; } drmEventContext evctx = { .version = 3, .page_flip_handler2 = page_flip_handler, }; drmHandleEvent(g_DRM.fd, &evctx); } } static bool refresh_state( drm_t *drm ) { drmModeRes *pResources = drmModeGetResources( drm->fd ); if ( pResources == nullptr ) { drm_log.errorf_errno( "drmModeGetResources failed" ); return false; } defer( drmModeFreeResources( pResources ) ); // Add connectors which appeared for ( int i = 0; i < pResources->count_connectors; i++ ) { uint32_t uConnectorId = pResources->connectors[i]; drmModeConnector *pConnector = drmModeGetConnector( drm->fd, uConnectorId ); if ( !pConnector ) continue; if ( cv_drm_ignore_internal_connectors ) { if ( pConnector->connector_type == DRM_MODE_CONNECTOR_eDP || pConnector->connector_type == DRM_MODE_CONNECTOR_LVDS || pConnector->connector_type == DRM_MODE_CONNECTOR_DSI ) { drmModeFreeConnector( pConnector ); continue; } } if ( !drm->connectors.contains( uConnectorId ) ) { drm->connectors.emplace( std::piecewise_construct, std::forward_as_tuple( uConnectorId ), std::forward_as_tuple( reinterpret_cast( GetBackend() ), pConnector ) ); } } // Remove connectors which disappeared for ( auto iter = drm->connectors.begin(); iter != drm->connectors.end(); ) { gamescope::CDRMConnector *pConnector = &iter->second; const bool bFound = std::any_of( pResources->connectors, pResources->connectors + pResources->count_connectors, std::bind_front( std::equal_to{}, pConnector->GetObjectId() ) ); if ( !bFound ) { drm_log.debugf( "Connector '%s' disappeared.", pConnector->GetName() ); if ( drm->pConnector == pConnector ) { drm_log.infof( "Current connector '%s' disappeared.", pConnector->GetName() ); drm->pConnector = nullptr; } iter = drm->connectors.erase( iter ); } else iter++; } // Re-probe connectors props and status) for ( auto &iter : drm->connectors ) { gamescope::CDRMConnector *pConnector = &iter.second; pConnector->RefreshState(); } for ( std::unique_ptr< gamescope::CDRMCRTC > &pCRTC : drm->crtcs ) pCRTC->RefreshState(); for ( std::unique_ptr< gamescope::CDRMPlane > &pPlane : drm->planes ) pPlane->RefreshState(); return true; } static bool get_resources(struct drm_t *drm) { { drmModeRes *pResources = drmModeGetResources( drm->fd ); if ( !pResources ) { drm_log.errorf_errno( "drmModeGetResources failed" ); return false; } defer( drmModeFreeResources( pResources ) ); for ( int i = 0; i < pResources->count_crtcs; i++ ) { drmModeCrtc *pCRTC = drmModeGetCrtc( drm->fd, pResources->crtcs[ i ] ); if ( pCRTC ) drm->crtcs.emplace_back( std::make_unique( pCRTC, 1u << i ) ); } } { drmModePlaneRes *pPlaneResources = drmModeGetPlaneResources( drm->fd ); if ( !pPlaneResources ) { drm_log.errorf_errno( "drmModeGetPlaneResources failed" ); return false; } defer( drmModeFreePlaneResources( pPlaneResources ) ); for ( uint32_t i = 0; i < pPlaneResources->count_planes; i++ ) { drmModePlane *pPlane = drmModeGetPlane( drm->fd, pPlaneResources->planes[ i ] ); if ( pPlane ) drm->planes.emplace_back( std::make_unique( pPlane ) ); } } return refresh_state( drm ); } struct mode_blocklist_entry { uint32_t width, height, refresh; }; // Filter out reporting some modes that are required for // certain certifications, but are completely useless, // and probably don't fit the display pixel size. static mode_blocklist_entry g_badModes[] = { { 4096, 2160, 0 }, }; static const drmModeModeInfo *find_mode( const drmModeConnector *connector, int hdisplay, int vdisplay, uint32_t vrefresh ) { for (int i = 0; i < connector->count_modes; i++) { const drmModeModeInfo *mode = &connector->modes[i]; bool bad = false; for (const auto& badMode : g_badModes) { bad |= (badMode.width == 0 || mode->hdisplay == badMode.width) && (badMode.height == 0 || mode->vdisplay == badMode.height) && (badMode.refresh == 0 || mode->vrefresh == badMode.refresh); } if (bad) continue; if (hdisplay != 0 && hdisplay != mode->hdisplay) continue; if (vdisplay != 0 && vdisplay != mode->vdisplay) continue; if (vrefresh != 0 && vrefresh != mode->vrefresh) continue; return mode; } return NULL; } static std::unordered_map parse_connector_priorities(const char *str) { std::unordered_map priorities{}; if (!str) { return priorities; } int i = 0; char *buf = strdup(str); char *name = strtok(buf, ","); while (name) { priorities[name] = i; i++; name = strtok(nullptr, ","); } free(buf); return priorities; } static int get_connector_priority(struct drm_t *drm, const char *name) { if (drm->connector_priorities.count(name) > 0) { return drm->connector_priorities[name]; } if (drm->connector_priorities.count("*") > 0) { return drm->connector_priorities["*"]; } return drm->connector_priorities.size(); } static bool get_saved_mode(const char *description, saved_mode &mode_info) { const char *mode_file = getenv("GAMESCOPE_MODE_SAVE_FILE"); if (!mode_file || !*mode_file) return false; FILE *file = fopen(mode_file, "r"); if (!file) return false; char line[256]; while (fgets(line, sizeof(line), file)) { char saved_description[256]; uint32_t broadcast_mode = 0; int ret = sscanf(line, "%255[^:]:%dx%d@%d %u", saved_description, &mode_info.width, &mode_info.height, &mode_info.refresh, &broadcast_mode); mode_info.broadcast_mode = (GamescopeBroadcastRGBMode_t) broadcast_mode; bool valid = ret == 4 || ret == 5; if (valid && !strcmp(saved_description, description)) { fclose(file); return true; } } fclose(file); return false; } static GamescopeBroadcastRGBMode_t s_ExternalBroadcastRGBMode = GAMESCOPE_BROADCAST_RGB_MODE_AUTOMATIC; static bool setup_best_connector(struct drm_t *drm, bool force, bool initial) { if (drm->pConnector && drm->pConnector->GetModeConnector()->connection != DRM_MODE_CONNECTED) { drm_log.infof("current connector '%s' disconnected", drm->pConnector->GetName()); drm->pConnector = nullptr; } gamescope::CDRMConnector *best = nullptr; int nBestPriority = INT_MAX; for ( auto &iter : drm->connectors ) { gamescope::CDRMConnector *pConnector = &iter.second; if ( pConnector->GetModeConnector()->connection != DRM_MODE_CONNECTED ) continue; if ( g_bForceInternal && pConnector->GetScreenType() == gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL ) continue; int nPriority = get_connector_priority( drm, pConnector->GetName() ); if ( nPriority < nBestPriority ) { best = pConnector; nBestPriority = nPriority; } } if (!force) { if ((!best && drm->pConnector) || (best && best == drm->pConnector)) { // Let's keep our current connector return true; } } if (best == nullptr) { drm_log.infof("cannot find any connected connector!"); drm_unset_connector(drm); drm_unset_mode(drm); const struct wlserver_output_info wlserver_output_info = { .description = "Virtual screen", }; wlserver_lock(); wlserver_set_output_info(&wlserver_output_info); wlserver_unlock(); return true; } if (!drm_set_connector(drm, best)) { return false; } char description[256]; if (best->GetScreenType() == gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL) { snprintf(description, sizeof(description), "Internal screen"); } else if (best->GetMake() && best->GetModel()) { snprintf(description, sizeof(description), "%s %s", best->GetMake(), best->GetModel()); } else if (best->GetModel()) { snprintf(description, sizeof(description), "%s", best->GetModel()); } else { snprintf(description, sizeof(description), "External screen"); } s_ExternalBroadcastRGBMode = GAMESCOPE_BROADCAST_RGB_MODE_AUTOMATIC; const drmModeModeInfo *mode = nullptr; if ( drm->preferred_width != 0 || drm->preferred_height != 0 || drm->preferred_refresh != 0 ) { mode = find_mode(best->GetModeConnector(), drm->preferred_width, drm->preferred_height, gamescope::ConvertmHzToHz( drm->preferred_refresh )); } if (!mode && best->GetScreenType() == gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL) { saved_mode mode_info{}; if (get_saved_mode(description, mode_info)) { s_ExternalBroadcastRGBMode = mode_info.broadcast_mode; mode = find_mode(best->GetModeConnector(), mode_info.width, mode_info.height, mode_info.refresh); } } if (!mode) { mode = find_mode(best->GetModeConnector(), 0, 0, 0); } if (!mode) { drm_log.errorf("could not find mode!"); return false; } if (!drm_set_mode(drm, mode)) { return false; } // Don't allow rollback of mode_id after connector change drm->current.mode_id = drm->pending.mode_id; const struct wlserver_output_info wlserver_output_info = { .description = description, .phys_width = (int) best->GetModeConnector()->mmWidth, .phys_height = (int) best->GetModeConnector()->mmHeight, }; wlserver_lock(); wlserver_set_output_info(&wlserver_output_info); wlserver_unlock(); if (!initial) WritePatchedEdid( best->GetRawEDID(), best->GetHDRInfo(), g_bRotated ); update_connector_display_info_wl( drm ); return true; } void load_pnps(void) { #ifdef HWDATA_PNP_IDS const char *filename = HWDATA_PNP_IDS; FILE *f = fopen(filename, "r"); if (!f) { drm_log.infof("failed to open PNP IDs file at '%s'", filename); return; } char *line = NULL; size_t line_size = 0; while (getline(&line, &line_size, f) >= 0) { char *nl = strchr(line, '\n'); if (nl) { *nl = '\0'; } char *sep = strchr(line, '\t'); if (!sep) { continue; } *sep = '\0'; std::string id(line); std::string name(sep + 1); pnps[id] = name; } free(line); fclose(f); #endif } extern bool env_to_bool(const char *env); uint32_t g_uAlwaysSignalledSyncobj = 0; int g_nAlwaysSignalledSyncFile = -1; static void gamescope_liftoff_log_handler(enum liftoff_log_priority liftoff_priority, const char *fmt, va_list args) { enum LogPriority priority = LOG_DEBUG; switch ( liftoff_priority ) { case LIFTOFF_ERROR: priority = LOG_ERROR; break; case LIFTOFF_DEBUG: priority = LOG_DEBUG; break; case LIFTOFF_SILENT: priority = LOG_SILENT; break; } liftoff_log_scope.vlogf(priority, fmt, args); } bool init_drm(struct drm_t *drm, int width, int height, int refresh) { load_pnps(); drm->bUseLiftoff = true; drm->preferred_width = width; drm->preferred_height = height; drm->preferred_refresh = refresh; drm->device_name = nullptr; dev_t dev_id = 0; if (vulkan_primary_dev_id(&dev_id)) { drmDevice *drm_dev = nullptr; if (drmGetDeviceFromDevId(dev_id, 0, &drm_dev) != 0) { drm_log.errorf("Failed to find DRM device with device ID %" PRIu64, (uint64_t)dev_id); return false; } assert(drm_dev->available_nodes & (1 << DRM_NODE_PRIMARY)); drm->device_name = strdup(drm_dev->nodes[DRM_NODE_PRIMARY]); drm_log.infof("opening DRM node '%s'", drm->device_name); } else { drm_log.infof("warning: picking an arbitrary DRM device"); } drm->fd = wlsession_open_kms( drm->device_name ); if ( drm->fd < 0 ) { drm_log.errorf("Could not open KMS device"); return false; } if ( !drmIsKMS( drm->fd ) ) { drm_log.errorf( "'%s' is not a KMS device", drm->device_name ); wlsession_close_kms(); return -1; } if (drmSetClientCap(drm->fd, DRM_CLIENT_CAP_ATOMIC, 1) != 0) { drm_log.errorf("drmSetClientCap(ATOMIC) failed"); return false; } if (drmGetCap(drm->fd, DRM_CAP_CURSOR_WIDTH, &drm->cursor_width) != 0) { drm->cursor_width = 64; } if (drmGetCap(drm->fd, DRM_CAP_CURSOR_HEIGHT, &drm->cursor_height) != 0) { drm->cursor_height = 64; } uint64_t cap; g_bSupportsSyncObjs = drmGetCap(drm->fd, DRM_CAP_SYNCOBJ, &cap) == 0 && cap != 0; if ( g_bSupportsSyncObjs ) { int err = drmSyncobjCreate(drm->fd, DRM_SYNCOBJ_CREATE_SIGNALED, &g_uAlwaysSignalledSyncobj); if (err < 0) { drm_log.errorf("Failed to create dummy signalled syncobj"); return false; } err = drmSyncobjExportSyncFile(drm->fd, g_uAlwaysSignalledSyncobj, &g_nAlwaysSignalledSyncFile); if (err < 0) { drm_log.errorf("Failed to create dummy signalled sync file"); return false; } } else { drm_log.errorf("Syncobjs are not supported by the KMS driver"); } if (drmGetCap(drm->fd, DRM_CAP_ADDFB2_MODIFIERS, &cap) == 0 && cap != 0) { drm->allow_modifiers = true; } g_bSupportsAsyncFlips = drmGetCap(drm->fd, DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP, &cap) == 0 && cap != 0; if (!g_bSupportsAsyncFlips) drm_log.errorf("Immediate flips are not supported by the KMS driver"); static bool async_disabled = env_to_bool(getenv("GAMESCOPE_DISABLE_ASYNC_FLIPS")); if ( async_disabled ) { g_bSupportsAsyncFlips = false; drm_log.errorf("Immediate flips disabled from environment"); } if (!get_resources(drm)) { return false; } drm->lo_device = liftoff_device_create( drm->fd ); if ( drm->lo_device == nullptr ) return false; if ( liftoff_device_register_all_planes( drm->lo_device ) < 0 ) return false; drm_log.infof("Connectors:"); for ( auto &iter : drm->connectors ) { gamescope::CDRMConnector *pConnector = &iter.second; const char *status_str = "disconnected"; if ( pConnector->GetModeConnector()->connection == DRM_MODE_CONNECTED ) status_str = "connected"; drm_log.infof(" %s (%s)", pConnector->GetName(), status_str); } drm->connector_priorities = parse_connector_priorities( g_sOutputName ); if (!setup_best_connector(drm, true, true)) { return false; } // Fetch formats which can be scanned out for ( std::unique_ptr< gamescope::CDRMPlane > &pPlane : drm->planes ) { if ( !get_plane_formats( drm, pPlane.get(), &drm->formats ) ) return false; } // TODO: intersect primary planes formats instead if ( !drm->pPrimaryPlane ) drm->pPrimaryPlane = find_primary_plane( drm ); if ( !drm->pPrimaryPlane ) { drm_log.errorf("Failed to find a primary plane"); return false; } if ( !get_plane_formats( drm, drm->pPrimaryPlane, &drm->primary_formats ) ) { return false; } // Pick a 10-bit format at first for our composition buffer, for a couple of reasons: // // 1. Many game engines automatically render to 10-bit formats such as UE4 which means // that when we have to composite, we can keep the same HW dithering that we would get if // we just scanned them out directly. // // 2. When compositing HDR content as a fallback when we undock, it avoids introducing // a bunch of horrible banding when going to G2.2 curve. // It ensures that we can dither that. g_nDRMFormat = pick_plane_format(&drm->primary_formats, DRM_FORMAT_XRGB2101010, DRM_FORMAT_ARGB2101010); if ( g_nDRMFormat == DRM_FORMAT_INVALID ) { g_nDRMFormat = pick_plane_format(&drm->primary_formats, DRM_FORMAT_XBGR2101010, DRM_FORMAT_ABGR2101010); if ( g_nDRMFormat == DRM_FORMAT_INVALID ) { g_nDRMFormat = pick_plane_format(&drm->primary_formats, DRM_FORMAT_XRGB8888, DRM_FORMAT_ARGB8888); if ( g_nDRMFormat == DRM_FORMAT_INVALID ) { drm_log.errorf("Primary plane doesn't support any formats >= 8888"); return false; } } } // ARGB8888 is the Xformat and AFormat here in this function as we want transparent overlay g_nDRMFormatOverlay = pick_plane_format(&drm->primary_formats, DRM_FORMAT_ARGB2101010, DRM_FORMAT_ARGB2101010); if ( g_nDRMFormatOverlay == DRM_FORMAT_INVALID ) { g_nDRMFormatOverlay = pick_plane_format(&drm->primary_formats, DRM_FORMAT_ABGR2101010, DRM_FORMAT_ABGR2101010); if ( g_nDRMFormatOverlay == DRM_FORMAT_INVALID ) { g_nDRMFormatOverlay = pick_plane_format(&drm->primary_formats, DRM_FORMAT_ARGB8888, DRM_FORMAT_ARGB8888); if ( g_nDRMFormatOverlay == DRM_FORMAT_INVALID ) { drm_log.errorf("Overlay plane doesn't support any formats >= 8888"); return false; } } } std::thread flip_handler_thread( flip_handler_thread_run ); flip_handler_thread.detach(); // Set log priority to the max, liftoff_log_scope will filter for us. liftoff_log_set_priority(LIFTOFF_DEBUG); liftoff_log_set_handler(gamescope_liftoff_log_handler); hdr_output_metadata sdr_metadata; memset(&sdr_metadata, 0, sizeof(sdr_metadata)); drm->sdr_static_metadata = GetBackend()->CreateBackendBlob( sdr_metadata ); drm->needs_modeset = true; return true; } void OnSleepScreenChanged( gamescope::ConVar & ) { force_repaint(); } gamescope::ConVar cv_drm_sleep_screens[] = { { "drm_sleep_internal_screen", false, "Force the internal screen to be asleep", OnSleepScreenChanged }, { "drm_sleep_external_screen", false, "Force the external screen to be asleep", OnSleepScreenChanged }, }; void drm_sleep_screen( gamescope::GamescopeScreenType eType, bool bSleep ) { if ( cv_drm_sleep_screens[ eType ] == bSleep ) return; cv_drm_sleep_screens[ eType ] = bSleep; } void finish_drm(struct drm_t *drm) { // Disable all connectors, CRTCs and planes. This is necessary to leave a // clean KMS state behind. Some other KMS clients might not support all of // the properties we use, e.g. "rotation" and Xorg don't play well // together. drmModeAtomicReq *req = drmModeAtomicAlloc(); for ( auto &iter : drm->connectors ) { gamescope::CDRMConnector *pConnector = &iter.second; pConnector->GetProperties().CRTC_ID->SetPendingValue( req, 0, true ); if ( pConnector->GetProperties().Colorspace ) pConnector->GetProperties().Colorspace->SetPendingValue( req, 0, true ); if ( pConnector->GetProperties().HDR_OUTPUT_METADATA ) { if ( drm->sdr_static_metadata && pConnector->GetHDRInfo().IsHDR10() ) pConnector->GetProperties().HDR_OUTPUT_METADATA->SetPendingValue( req, drm->sdr_static_metadata->GetBlobValue(), true ); else pConnector->GetProperties().HDR_OUTPUT_METADATA->SetPendingValue( req, 0, true ); } if ( pConnector->GetProperties().content_type ) pConnector->GetProperties().content_type->SetPendingValue( req, 0, true ); } for ( std::unique_ptr< gamescope::CDRMCRTC > &pCRTC : drm->crtcs ) { pCRTC->GetProperties().ACTIVE->SetPendingValue( req, 0, true ); pCRTC->GetProperties().MODE_ID->SetPendingValue( req, 0, true ); if ( pCRTC->GetProperties().GAMMA_LUT ) pCRTC->GetProperties().GAMMA_LUT->SetPendingValue( req, 0, true ); if ( pCRTC->GetProperties().DEGAMMA_LUT ) pCRTC->GetProperties().DEGAMMA_LUT->SetPendingValue( req, 0, true ); if ( pCRTC->GetProperties().CTM ) pCRTC->GetProperties().CTM->SetPendingValue( req, 0, true ); if ( pCRTC->GetProperties().VRR_ENABLED ) pCRTC->GetProperties().VRR_ENABLED->SetPendingValue( req, 0, true ); if ( pCRTC->GetProperties().OUT_FENCE_PTR ) pCRTC->GetProperties().OUT_FENCE_PTR->SetPendingValue( req, 0, true ); if ( pCRTC->GetProperties().AMD_CRTC_REGAMMA_TF ) pCRTC->GetProperties().AMD_CRTC_REGAMMA_TF->SetPendingValue( req, 0, true ); } for ( std::unique_ptr< gamescope::CDRMPlane > &pPlane : drm->planes ) { pPlane->GetProperties().FB_ID->SetPendingValue( req, 0, true ); pPlane->GetProperties().IN_FENCE_FD->SetPendingValue( req, -1, true ); pPlane->GetProperties().CRTC_ID->SetPendingValue( req, 0, true ); pPlane->GetProperties().SRC_X->SetPendingValue( req, 0, true ); pPlane->GetProperties().SRC_Y->SetPendingValue( req, 0, true ); pPlane->GetProperties().SRC_W->SetPendingValue( req, 0, true ); pPlane->GetProperties().SRC_H->SetPendingValue( req, 0, true ); pPlane->GetProperties().CRTC_X->SetPendingValue( req, 0, true ); pPlane->GetProperties().CRTC_Y->SetPendingValue( req, 0, true ); pPlane->GetProperties().CRTC_W->SetPendingValue( req, 0, true ); pPlane->GetProperties().CRTC_H->SetPendingValue( req, 0, true ); if ( pPlane->GetProperties().rotation ) pPlane->GetProperties().rotation->SetPendingValue( req, DRM_MODE_ROTATE_0, true ); if ( pPlane->GetProperties().alpha ) pPlane->GetProperties().alpha->SetPendingValue( req, 0xFFFF, true ); //if ( pPlane->GetProperties().zpos ) // pPlane->GetProperties().zpos->SetPendingValue( req, , true ); if ( pPlane->GetProperties().AMD_PLANE_DEGAMMA_TF ) pPlane->GetProperties().AMD_PLANE_DEGAMMA_TF->SetPendingValue( req, AMDGPU_TRANSFER_FUNCTION_DEFAULT, true ); if ( pPlane->GetProperties().AMD_PLANE_DEGAMMA_LUT ) pPlane->GetProperties().AMD_PLANE_DEGAMMA_LUT->SetPendingValue( req, 0, true ); if ( pPlane->GetProperties().AMD_PLANE_CTM ) pPlane->GetProperties().AMD_PLANE_CTM->SetPendingValue( req, 0, true ); if ( pPlane->GetProperties().AMD_PLANE_HDR_MULT ) pPlane->GetProperties().AMD_PLANE_HDR_MULT->SetPendingValue( req, 0x100000000ULL, true ); if ( pPlane->GetProperties().AMD_PLANE_SHAPER_TF ) pPlane->GetProperties().AMD_PLANE_SHAPER_TF->SetPendingValue( req, AMDGPU_TRANSFER_FUNCTION_DEFAULT, true ); if ( pPlane->GetProperties().AMD_PLANE_SHAPER_LUT ) pPlane->GetProperties().AMD_PLANE_SHAPER_LUT->SetPendingValue( req, 0, true ); if ( pPlane->GetProperties().AMD_PLANE_LUT3D ) pPlane->GetProperties().AMD_PLANE_LUT3D->SetPendingValue( req, 0, true ); if ( pPlane->GetProperties().AMD_PLANE_BLEND_TF ) pPlane->GetProperties().AMD_PLANE_BLEND_TF->SetPendingValue( req, AMDGPU_TRANSFER_FUNCTION_DEFAULT, true ); if ( pPlane->GetProperties().AMD_PLANE_BLEND_LUT ) pPlane->GetProperties().AMD_PLANE_BLEND_LUT->SetPendingValue( req, 0, true ); } // We can't do a non-blocking commit here or else risk EBUSY in case the // previous page-flip is still in flight. uint32_t flags = DRM_MODE_ATOMIC_ALLOW_MODESET; int ret = drmModeAtomicCommit( drm->fd, req, flags, nullptr ); if ( ret != 0 ) { drm_log.errorf_errno( "finish_drm: drmModeAtomicCommit failed" ); } drmModeAtomicFree(req); free(drm->device_name); wlr_drm_format_set_finish( &drm->formats ); wlr_drm_format_set_finish( &drm->primary_formats ); drm->m_FbIdsInRequest.clear(); { std::unique_lock lock( drm->m_QueuedFbIdsMutex ); drm->m_QueuedFbIds.clear(); } { std::unique_lock lock( drm->m_mutVisibleFbIds ); drm->m_VisibleFbIds.clear(); } drm->sdr_static_metadata = nullptr; drm->current = drm_t::drm_state_t{}; drm->pending = drm_t::drm_state_t{}; drm->planes.clear(); drm->crtcs.clear(); drm->connectors.clear(); // We can't close the DRM FD here, it might still be in use by the // page-flip handler thread. } gamescope::OwningRc drm_fbid_from_dmabuf( struct drm_t *drm, struct wlr_buffer *buf, struct wlr_dmabuf_attributes *dma_buf ) { gamescope::OwningRc pBackendFb; uint32_t fb_id = 0; if ( !wlr_drm_format_set_has( &drm->formats, dma_buf->format, dma_buf->modifier ) ) { drm_log.errorf( "Cannot import FB to DRM: format 0x%" PRIX32 " and modifier 0x%" PRIX64 " not supported for scan-out", dma_buf->format, dma_buf->modifier ); return nullptr; } uint32_t handles[4] = {0}; uint64_t modifiers[4] = {0}; for ( int i = 0; i < dma_buf->n_planes; i++ ) { if ( drmPrimeFDToHandle( drm->fd, dma_buf->fd[i], &handles[i] ) != 0 ) { drm_log.errorf_errno("drmPrimeFDToHandle failed"); goto out; } /* KMS requires all planes to have the same modifier */ modifiers[i] = dma_buf->modifier; } if ( dma_buf->modifier != DRM_FORMAT_MOD_INVALID ) { if ( !drm->allow_modifiers ) { drm_log.errorf("Cannot import DMA-BUF: has a modifier (0x%" PRIX64 "), but KMS doesn't support them", dma_buf->modifier); goto out; } if ( drmModeAddFB2WithModifiers( drm->fd, dma_buf->width, dma_buf->height, dma_buf->format, handles, dma_buf->stride, dma_buf->offset, modifiers, &fb_id, DRM_MODE_FB_MODIFIERS ) != 0 ) { drm_log.errorf_errno("drmModeAddFB2WithModifiers failed"); goto out; } } else { if ( drmModeAddFB2( drm->fd, dma_buf->width, dma_buf->height, dma_buf->format, handles, dma_buf->stride, dma_buf->offset, &fb_id, 0 ) != 0 ) { drm_log.errorf_errno("drmModeAddFB2 failed"); goto out; } } drm_log.debugf("make fbid %u", fb_id); pBackendFb = new gamescope::CDRMFb( fb_id ); out: for ( int i = 0; i < dma_buf->n_planes; i++ ) { if ( handles[i] == 0 ) continue; // GEM handles aren't ref'counted by the kernel. Two DMA-BUFs may // return the same GEM handle, we need to be careful not to // double-close them. bool already_closed = false; for ( int j = 0; j < i; j++ ) { if ( handles[i] == handles[j] ) already_closed = true; } if ( already_closed ) continue; struct drm_gem_close args = { .handle = handles[i] }; if ( drmIoctl( drm->fd, DRM_IOCTL_GEM_CLOSE, &args ) != 0 ) { drm_log.errorf_errno( "drmIoctl(GEM_CLOSE) failed" ); } } return pBackendFb; } static void update_drm_effective_orientations( struct drm_t *drm, const drmModeModeInfo *pMode ) { gamescope::IBackendConnector *pInternalConnector = GetBackend()->GetConnector( gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL ); if ( pInternalConnector ) { gamescope::CDRMConnector *pDRMInternalConnector = static_cast( pInternalConnector ); const drmModeModeInfo *pInternalMode = pMode; if ( pDRMInternalConnector != drm->pConnector ) pInternalMode = find_mode( pDRMInternalConnector->GetModeConnector(), 0, 0, 0 ); pDRMInternalConnector->UpdateEffectiveOrientation( pInternalMode ); } gamescope::IBackendConnector *pExternalConnector = GetBackend()->GetConnector( gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL ); if ( pExternalConnector ) { gamescope::CDRMConnector *pDRMExternalConnector = static_cast( pExternalConnector ); const drmModeModeInfo *pExternalMode = pMode; if ( pDRMExternalConnector != drm->pConnector ) pExternalMode = find_mode( pDRMExternalConnector->GetModeConnector(), 0, 0, 0 ); pDRMExternalConnector->UpdateEffectiveOrientation( pExternalMode ); } } // Only used for NV12 buffers static drm_color_encoding drm_get_color_encoding(EStreamColorspace colorspace) { switch (colorspace) { default: case k_EStreamColorspace_Unknown: return DRM_COLOR_YCBCR_BT709; case k_EStreamColorspace_BT601: return DRM_COLOR_YCBCR_BT601; case k_EStreamColorspace_BT601_Full: return DRM_COLOR_YCBCR_BT601; case k_EStreamColorspace_BT709: return DRM_COLOR_YCBCR_BT709; case k_EStreamColorspace_BT709_Full: return DRM_COLOR_YCBCR_BT709; } } static drm_color_range drm_get_color_range(EStreamColorspace colorspace) { switch (colorspace) { default: case k_EStreamColorspace_Unknown: return DRM_COLOR_YCBCR_FULL_RANGE; case k_EStreamColorspace_BT601: return DRM_COLOR_YCBCR_LIMITED_RANGE; case k_EStreamColorspace_BT601_Full: return DRM_COLOR_YCBCR_FULL_RANGE; case k_EStreamColorspace_BT709: return DRM_COLOR_YCBCR_LIMITED_RANGE; case k_EStreamColorspace_BT709_Full: return DRM_COLOR_YCBCR_FULL_RANGE; } } template void hash_combine(size_t& s, const T& v) { std::hash h; s^= h(v) + 0x9e3779b9 + (s<< 6) + (s>> 2); } struct LiftoffStateCacheEntry { LiftoffStateCacheEntry() { memset(this, 0, sizeof(LiftoffStateCacheEntry)); } int nLayerCount; struct LiftoffLayerState_t { bool ycbcr; uint32_t zpos; uint32_t srcW, srcH; uint32_t crtcX, crtcY, crtcW, crtcH; uint16_t opacity; drm_color_encoding colorEncoding; drm_color_range colorRange; GamescopeAppTextureColorspace colorspace; AlphaBlendingMode_t eAlphaBlendingMode; } layerState[ k_nMaxLayers ]; bool operator == (const LiftoffStateCacheEntry& entry) const { return !memcmp(this, &entry, sizeof(LiftoffStateCacheEntry)); } }; struct LiftoffStateCacheEntryKasher { size_t operator()(const LiftoffStateCacheEntry& k) const { size_t hash = 0; hash_combine(hash, k.nLayerCount); for ( int i = 0; i < k.nLayerCount; i++ ) { hash_combine(hash, k.layerState[i].ycbcr); hash_combine(hash, k.layerState[i].zpos); hash_combine(hash, k.layerState[i].srcW); hash_combine(hash, k.layerState[i].srcH); hash_combine(hash, k.layerState[i].crtcX); hash_combine(hash, k.layerState[i].crtcY); hash_combine(hash, k.layerState[i].crtcW); hash_combine(hash, k.layerState[i].crtcH); hash_combine(hash, k.layerState[i].opacity); hash_combine(hash, k.layerState[i].colorEncoding); hash_combine(hash, k.layerState[i].colorRange); hash_combine(hash, k.layerState[i].colorspace); hash_combine(hash, k.layerState[i].eAlphaBlendingMode); } return hash; } }; std::unordered_set g_LiftoffStateCache; static inline amdgpu_transfer_function colorspace_to_plane_degamma_tf(GamescopeAppTextureColorspace colorspace) { switch ( colorspace ) { default: // Linear in this sense is SRGB. Linear = sRGB image view doing automatic sRGB -> Linear which doesn't happen on DRM side. case GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB: return AMDGPU_TRANSFER_FUNCTION_SRGB_EOTF; case GAMESCOPE_APP_TEXTURE_COLORSPACE_PASSTHRU: case GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB: // Use LINEAR TF for scRGB float format as 80 nit = 1.0 in scRGB, which matches // what PQ TF decodes to/encodes from. // AMD internal format is FP16, and generally expected for 1.0 -> 80 nit. // which just so happens to match scRGB. return AMDGPU_TRANSFER_FUNCTION_IDENTITY; case GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ: return AMDGPU_TRANSFER_FUNCTION_PQ_EOTF; } } static inline amdgpu_transfer_function colorspace_to_plane_shaper_tf(GamescopeAppTextureColorspace colorspace) { switch ( colorspace ) { default: case GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB: return AMDGPU_TRANSFER_FUNCTION_SRGB_INV_EOTF; case GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB: // scRGB Linear -> PQ for shaper + 3D LUT case GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ: return AMDGPU_TRANSFER_FUNCTION_PQ_INV_EOTF; case GAMESCOPE_APP_TEXTURE_COLORSPACE_PASSTHRU: return AMDGPU_TRANSFER_FUNCTION_DEFAULT; } } static inline amdgpu_transfer_function inverse_tf(amdgpu_transfer_function tf) { switch ( tf ) { default: case AMDGPU_TRANSFER_FUNCTION_DEFAULT: return AMDGPU_TRANSFER_FUNCTION_DEFAULT; case AMDGPU_TRANSFER_FUNCTION_IDENTITY: return AMDGPU_TRANSFER_FUNCTION_IDENTITY; case AMDGPU_TRANSFER_FUNCTION_SRGB_EOTF: return AMDGPU_TRANSFER_FUNCTION_SRGB_INV_EOTF; case AMDGPU_TRANSFER_FUNCTION_BT709_OETF: return AMDGPU_TRANSFER_FUNCTION_BT709_INV_OETF; case AMDGPU_TRANSFER_FUNCTION_PQ_EOTF: return AMDGPU_TRANSFER_FUNCTION_PQ_INV_EOTF; case AMDGPU_TRANSFER_FUNCTION_GAMMA22_EOTF: return AMDGPU_TRANSFER_FUNCTION_GAMMA22_INV_EOTF; case AMDGPU_TRANSFER_FUNCTION_GAMMA24_EOTF: return AMDGPU_TRANSFER_FUNCTION_GAMMA24_INV_EOTF; case AMDGPU_TRANSFER_FUNCTION_GAMMA26_EOTF: return AMDGPU_TRANSFER_FUNCTION_GAMMA26_INV_EOTF; case AMDGPU_TRANSFER_FUNCTION_SRGB_INV_EOTF: return AMDGPU_TRANSFER_FUNCTION_SRGB_EOTF; case AMDGPU_TRANSFER_FUNCTION_BT709_INV_OETF: return AMDGPU_TRANSFER_FUNCTION_BT709_OETF; case AMDGPU_TRANSFER_FUNCTION_PQ_INV_EOTF: return AMDGPU_TRANSFER_FUNCTION_PQ_EOTF; case AMDGPU_TRANSFER_FUNCTION_GAMMA22_INV_EOTF: return AMDGPU_TRANSFER_FUNCTION_GAMMA22_EOTF; case AMDGPU_TRANSFER_FUNCTION_GAMMA24_INV_EOTF: return AMDGPU_TRANSFER_FUNCTION_GAMMA24_EOTF; case AMDGPU_TRANSFER_FUNCTION_GAMMA26_INV_EOTF: return AMDGPU_TRANSFER_FUNCTION_GAMMA26_EOTF; } } static inline uint32_t ColorSpaceToEOTFIndex( GamescopeAppTextureColorspace colorspace ) { switch ( colorspace ) { default: case GAMESCOPE_APP_TEXTURE_COLORSPACE_LINEAR: // Not actually linear, just Linear vs sRGB image views in Vulkan. Still viewed as sRGB on the DRM side. case GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB: // SDR sRGB content treated as native Gamma 22 curve. No need to do sRGB -> 2.2 or whatever. return EOTF_Gamma22; case GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB: // Okay, so this is WEIRD right? OKAY Let me explain it to you. // The plan for scRGB content is to go from scRGB -> PQ in a SHAPER_TF // before indexing into the shaper. (input from colorspace_to_plane_regamma_tf!) return EOTF_PQ; case GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ: return EOTF_PQ; } } LiftoffStateCacheEntry FrameInfoToLiftoffStateCacheEntry( struct drm_t *drm, const FrameInfo_t *frameInfo ) { LiftoffStateCacheEntry entry{}; entry.nLayerCount = frameInfo->layerCount; for ( int i = 0; i < entry.nLayerCount; i++ ) { const uint16_t srcWidth = frameInfo->layers[ i ].tex->width(); const uint16_t srcHeight = frameInfo->layers[ i ].tex->height(); int32_t crtcX = -frameInfo->layers[ i ].offset.x; int32_t crtcY = -frameInfo->layers[ i ].offset.y; uint64_t crtcW = srcWidth / frameInfo->layers[ i ].scale.x; uint64_t crtcH = srcHeight / frameInfo->layers[ i ].scale.y; if (g_bRotated) { int64_t imageH = frameInfo->layers[ i ].tex->contentHeight() / frameInfo->layers[ i ].scale.y; const int32_t x = crtcX; const uint64_t w = crtcW; crtcX = g_nOutputHeight - imageH - crtcY; crtcY = x; crtcW = crtcH; crtcH = w; } entry.layerState[i].zpos = frameInfo->layers[ i ].zpos; entry.layerState[i].srcW = srcWidth << 16; entry.layerState[i].srcH = srcHeight << 16; entry.layerState[i].crtcX = crtcX; entry.layerState[i].crtcY = crtcY; entry.layerState[i].crtcW = crtcW; entry.layerState[i].crtcH = crtcH; entry.layerState[i].opacity = frameInfo->layers[i].opacity * 0xffff; entry.layerState[i].ycbcr = frameInfo->layers[i].isYcbcr(); if ( entry.layerState[i].ycbcr ) { entry.layerState[i].colorEncoding = drm_get_color_encoding( g_ForcedNV12ColorSpace ); entry.layerState[i].colorRange = drm_get_color_range( g_ForcedNV12ColorSpace ); entry.layerState[i].colorspace = GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB; } else { entry.layerState[i].colorspace = frameInfo->layers[ i ].colorspace; } entry.layerState[i].eAlphaBlendingMode = frameInfo->layers[i].eAlphaBlendingMode; } return entry; } static bool is_liftoff_caching_enabled() { static bool disabled = env_to_bool(getenv("GAMESCOPE_LIFTOFF_CACHE_DISABLE")); return !disabled; } namespace gamescope { //////////////////// // CDRMAtomicObject //////////////////// CDRMAtomicObject::CDRMAtomicObject( uint32_t ulObjectId ) : m_ulObjectId{ ulObjectId } { } ///////////////////////// // CDRMAtomicTypedObject ///////////////////////// template < uint32_t DRMObjectType > CDRMAtomicTypedObject::CDRMAtomicTypedObject( uint32_t ulObjectId ) : CDRMAtomicObject{ ulObjectId } { } template < uint32_t DRMObjectType > std::optional CDRMAtomicTypedObject::GetRawProperties() { drmModeObjectProperties *pProperties = drmModeObjectGetProperties( g_DRM.fd, m_ulObjectId, DRMObjectType ); if ( !pProperties ) { drm_log.errorf_errno( "drmModeObjectGetProperties failed" ); return std::nullopt; } defer( drmModeFreeObjectProperties( pProperties ) ); DRMObjectRawProperties rawProperties; for ( uint32_t i = 0; i < pProperties->count_props; i++ ) { drmModePropertyRes *pProperty = drmModeGetProperty( g_DRM.fd, pProperties->props[ i ] ); if ( !pProperty ) continue; defer( drmModeFreeProperty( pProperty ) ); rawProperties[ pProperty->name ] = DRMObjectRawProperty{ pProperty->prop_id, pProperties->prop_values[ i ] }; } return rawProperties; } ///////////////////////// // CDRMAtomicProperty ///////////////////////// CDRMAtomicProperty::CDRMAtomicProperty( CDRMAtomicObject *pObject, DRMObjectRawProperty rawProperty ) : m_pObject{ pObject } , m_uPropertyId{ rawProperty.uPropertyId } , m_ulPendingValue{ rawProperty.ulValue } , m_ulCurrentValue{ rawProperty.ulValue } , m_ulInitialValue{ rawProperty.ulValue } { } /*static*/ std::optional CDRMAtomicProperty::Instantiate( const char *pszName, CDRMAtomicObject *pObject, const DRMObjectRawProperties& rawProperties ) { auto iter = rawProperties.find( pszName ); if ( iter == rawProperties.end() ) return std::nullopt; return CDRMAtomicProperty{ pObject, iter->second }; } int CDRMAtomicProperty::SetPendingValue( drmModeAtomicReq *pRequest, uint64_t ulValue, bool bForce /*= false*/ ) { // In instances where we rolled back due to -EINVAL, or we want to ensure a value from an unclean state // eg. from an unclean or other initial state, you can force an update in the request with bForce. if ( ulValue == m_ulPendingValue && !bForce ) return 0; int ret = drmModeAtomicAddProperty( pRequest, m_pObject->GetObjectId(), m_uPropertyId, ulValue ); if ( ret < 0 ) return ret; m_ulPendingValue = ulValue; return ret; } void CDRMAtomicProperty::OnCommit() { m_ulCurrentValue = m_ulPendingValue; } void CDRMAtomicProperty::Rollback() { m_ulPendingValue = m_ulCurrentValue; } ///////////////////////// // CDRMPlane ///////////////////////// CDRMPlane::CDRMPlane( drmModePlane *pPlane ) : CDRMAtomicTypedObject( pPlane->plane_id ) , m_pPlane{ pPlane, []( drmModePlane *pPlane ){ drmModeFreePlane( pPlane ); } } { RefreshState(); } void CDRMPlane::RefreshState() { auto rawProperties = GetRawProperties(); if ( rawProperties ) { m_Props.type = CDRMAtomicProperty::Instantiate( "type", this, *rawProperties ); m_Props.IN_FORMATS = CDRMAtomicProperty::Instantiate( "IN_FORMATS", this, *rawProperties ); m_Props.FB_ID = CDRMAtomicProperty::Instantiate( "FB_ID", this, *rawProperties ); m_Props.IN_FENCE_FD = CDRMAtomicProperty::Instantiate( "IN_FENCE_FD", this, *rawProperties ); m_Props.CRTC_ID = CDRMAtomicProperty::Instantiate( "CRTC_ID", this, *rawProperties ); m_Props.SRC_X = CDRMAtomicProperty::Instantiate( "SRC_X", this, *rawProperties ); m_Props.SRC_Y = CDRMAtomicProperty::Instantiate( "SRC_Y", this, *rawProperties ); m_Props.SRC_W = CDRMAtomicProperty::Instantiate( "SRC_W", this, *rawProperties ); m_Props.SRC_H = CDRMAtomicProperty::Instantiate( "SRC_H", this, *rawProperties ); m_Props.CRTC_X = CDRMAtomicProperty::Instantiate( "CRTC_X", this, *rawProperties ); m_Props.CRTC_Y = CDRMAtomicProperty::Instantiate( "CRTC_Y", this, *rawProperties ); m_Props.CRTC_W = CDRMAtomicProperty::Instantiate( "CRTC_W", this, *rawProperties ); m_Props.CRTC_H = CDRMAtomicProperty::Instantiate( "CRTC_H", this, *rawProperties ); m_Props.zpos = CDRMAtomicProperty::Instantiate( "zpos", this, *rawProperties ); m_Props.alpha = CDRMAtomicProperty::Instantiate( "alpha", this, *rawProperties ); m_Props.rotation = CDRMAtomicProperty::Instantiate( "rotation", this, *rawProperties ); m_Props.COLOR_ENCODING = CDRMAtomicProperty::Instantiate( "COLOR_ENCODING", this, *rawProperties ); m_Props.COLOR_RANGE = CDRMAtomicProperty::Instantiate( "COLOR_RANGE", this, *rawProperties ); m_Props.AMD_PLANE_DEGAMMA_TF = CDRMAtomicProperty::Instantiate( "AMD_PLANE_DEGAMMA_TF", this, *rawProperties ); m_Props.AMD_PLANE_DEGAMMA_LUT = CDRMAtomicProperty::Instantiate( "AMD_PLANE_DEGAMMA_LUT", this, *rawProperties ); m_Props.AMD_PLANE_CTM = CDRMAtomicProperty::Instantiate( "AMD_PLANE_CTM", this, *rawProperties ); m_Props.AMD_PLANE_HDR_MULT = CDRMAtomicProperty::Instantiate( "AMD_PLANE_HDR_MULT", this, *rawProperties ); m_Props.AMD_PLANE_SHAPER_LUT = CDRMAtomicProperty::Instantiate( "AMD_PLANE_SHAPER_LUT", this, *rawProperties ); m_Props.AMD_PLANE_SHAPER_TF = CDRMAtomicProperty::Instantiate( "AMD_PLANE_SHAPER_TF", this, *rawProperties ); m_Props.AMD_PLANE_LUT3D = CDRMAtomicProperty::Instantiate( "AMD_PLANE_LUT3D", this, *rawProperties ); m_Props.AMD_PLANE_BLEND_TF = CDRMAtomicProperty::Instantiate( "AMD_PLANE_BLEND_TF", this, *rawProperties ); m_Props.AMD_PLANE_BLEND_LUT = CDRMAtomicProperty::Instantiate( "AMD_PLANE_BLEND_LUT", this, *rawProperties ); } } ///////////////////////// // CDRMCRTC ///////////////////////// CDRMCRTC::CDRMCRTC( drmModeCrtc *pCRTC, uint32_t uCRTCMask ) : CDRMAtomicTypedObject( pCRTC->crtc_id ) , m_pCRTC{ pCRTC, []( drmModeCrtc *pCRTC ){ drmModeFreeCrtc( pCRTC ); } } , m_uCRTCMask{ uCRTCMask } { RefreshState(); } void CDRMCRTC::RefreshState() { auto rawProperties = GetRawProperties(); if ( rawProperties ) { m_Props.ACTIVE = CDRMAtomicProperty::Instantiate( "ACTIVE", this, *rawProperties ); m_Props.MODE_ID = CDRMAtomicProperty::Instantiate( "MODE_ID", this, *rawProperties ); m_Props.GAMMA_LUT = CDRMAtomicProperty::Instantiate( "GAMMA_LUT", this, *rawProperties ); m_Props.DEGAMMA_LUT = CDRMAtomicProperty::Instantiate( "DEGAMMA_LUT", this, *rawProperties ); m_Props.CTM = CDRMAtomicProperty::Instantiate( "CTM", this, *rawProperties ); m_Props.VRR_ENABLED = CDRMAtomicProperty::Instantiate( "VRR_ENABLED", this, *rawProperties ); m_Props.OUT_FENCE_PTR = CDRMAtomicProperty::Instantiate( "OUT_FENCE_PTR", this, *rawProperties ); m_Props.AMD_CRTC_REGAMMA_TF = CDRMAtomicProperty::Instantiate( "AMD_CRTC_REGAMMA_TF", this, *rawProperties ); } } ///////////////////////// // CDRMConnector ///////////////////////// CDRMConnector::CDRMConnector( CDRMBackend *pBackend, drmModeConnector *pConnector ) : CDRMAtomicTypedObject( pConnector->connector_id ) , m_pBackend{ pBackend } , m_pConnector{ pConnector, []( drmModeConnector *pConnector ){ drmModeFreeConnector( pConnector ); } } { RefreshState(); } void CDRMConnector::RefreshState() { // For the connector re-poll the drmModeConnector to get new modes, etc. // This isn't needed for CRTC/Planes in which the state is immutable for their lifetimes. // Connectors can be re-plugged. // TODO: Clean this up. m_pConnector = CAutoDeletePtr< drmModeConnector > { drmModeGetConnector( g_DRM.fd, m_pConnector->connector_id ), []( drmModeConnector *pConnector ){ drmModeFreeConnector( pConnector ); } }; // Sort the modes to our preference. std::stable_sort( m_pConnector->modes, m_pConnector->modes + m_pConnector->count_modes, []( const drmModeModeInfo &a, const drmModeModeInfo &b ) { bool bGoodRefreshA = a.vrefresh >= 60; bool bGoodRefreshB = b.vrefresh >= 60; if (bGoodRefreshA != bGoodRefreshB) return bGoodRefreshA; bool bPreferredA = a.type & DRM_MODE_TYPE_PREFERRED; bool bPreferredB = b.type & DRM_MODE_TYPE_PREFERRED; if (bPreferredA != bPreferredB) return bPreferredA; int nAreaA = a.hdisplay * a.vdisplay; int nAreaB = b.hdisplay * b.vdisplay; if (nAreaA != nAreaB) return nAreaA > nAreaB; return a.vrefresh > b.vrefresh; } ); // Clear this information out. m_Mutable = MutableConnectorState{}; m_Mutable.uPossibleCRTCMask = drmModeConnectorGetPossibleCrtcs( g_DRM.fd, GetModeConnector() ); // These are string constants from libdrm, no free. const char *pszTypeStr = drmModeGetConnectorTypeName( GetModeConnector()->connector_type ); if ( !pszTypeStr ) pszTypeStr = "Unknown"; snprintf( m_Mutable.szName, sizeof( m_Mutable.szName ), "%s-%d", pszTypeStr, GetModeConnector()->connector_type_id ); m_Mutable.szName[ sizeof( m_Mutable.szName ) - 1 ] = '\0'; for ( int i = 0; i < m_pConnector->count_modes; i++ ) { drmModeModeInfo *pMode = &m_pConnector->modes[i]; m_Mutable.BackendModes.emplace_back( BackendMode { .uWidth = pMode->hdisplay, .uHeight = pMode->vdisplay, .uRefresh = pMode->vrefresh, }); } auto rawProperties = GetRawProperties(); if ( rawProperties ) { m_Props.CRTC_ID = CDRMAtomicProperty::Instantiate( "CRTC_ID", this, *rawProperties ); m_Props.Colorspace = CDRMAtomicProperty::Instantiate( "Colorspace", this, *rawProperties ); m_Props.content_type = CDRMAtomicProperty::Instantiate( "content type", this, *rawProperties ); m_Props.panel_orientation = CDRMAtomicProperty::Instantiate( "panel orientation", this, *rawProperties ); m_Props.HDR_OUTPUT_METADATA = CDRMAtomicProperty::Instantiate( "HDR_OUTPUT_METADATA", this, *rawProperties ); m_Props.vrr_capable = CDRMAtomicProperty::Instantiate( "vrr_capable", this, *rawProperties ); m_Props.EDID = CDRMAtomicProperty::Instantiate( "EDID", this, *rawProperties ); m_Props.Broadcast_RGB = CDRMAtomicProperty::Instantiate( "Broadcast RGB", this, *rawProperties ); } ParseEDID(); } int CDRMConnector::Present( const FrameInfo_t *pFrameInfo, bool bAsync ) { return HackyDRMPresent( pFrameInfo, bAsync ); } void CDRMConnector::UpdateEffectiveOrientation( const drmModeModeInfo *pMode ) { if ( this->GetScreenType() == GAMESCOPE_SCREEN_TYPE_INTERNAL && g_DesiredInternalOrientation != GAMESCOPE_PANEL_ORIENTATION_AUTO ) { m_ChosenOrientation = g_DesiredInternalOrientation; } else { if ( this->GetProperties().panel_orientation ) { switch ( this->GetProperties().panel_orientation->GetCurrentValue() ) { case DRM_MODE_PANEL_ORIENTATION_NORMAL: m_ChosenOrientation = GAMESCOPE_PANEL_ORIENTATION_0; return; case DRM_MODE_PANEL_ORIENTATION_BOTTOM_UP: m_ChosenOrientation = GAMESCOPE_PANEL_ORIENTATION_180; return; case DRM_MODE_PANEL_ORIENTATION_LEFT_UP: m_ChosenOrientation = GAMESCOPE_PANEL_ORIENTATION_90; return; case DRM_MODE_PANEL_ORIENTATION_RIGHT_UP: m_ChosenOrientation = GAMESCOPE_PANEL_ORIENTATION_270; return; default: break; } } if ( this->GetScreenType() == gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL && pMode ) { // Auto-detect portait mode for internal displays m_ChosenOrientation = pMode->hdisplay < pMode->vdisplay ? GAMESCOPE_PANEL_ORIENTATION_270 : GAMESCOPE_PANEL_ORIENTATION_0; } else { m_ChosenOrientation = GAMESCOPE_PANEL_ORIENTATION_0; } } } void CDRMConnector::ParseEDID() { if ( !GetProperties().EDID ) return; uint64_t ulBlobId = GetProperties().EDID->GetCurrentValue(); if ( !ulBlobId ) return; drmModePropertyBlobRes *pBlob = drmModeGetPropertyBlob( g_DRM.fd, ulBlobId ); if ( !pBlob ) return; defer( drmModeFreePropertyBlob( pBlob ) ); const uint8_t *pDataPointer = reinterpret_cast( pBlob->data ); m_Mutable.EdidData = std::vector{ pDataPointer, pDataPointer + pBlob->length }; di_info *pInfo = di_info_parse_edid( m_Mutable.EdidData.data(), m_Mutable.EdidData.size() ); if ( !pInfo ) { drm_log.errorf( "Failed to parse edid for connector: %s", m_Mutable.szName ); return; } defer( di_info_destroy( pInfo ) ); const di_edid *pEdid = di_info_get_edid( pInfo ); const di_edid_vendor_product *pProduct = di_edid_get_vendor_product( pEdid ); m_Mutable.szMakePNP[0] = pProduct->manufacturer[0]; m_Mutable.szMakePNP[1] = pProduct->manufacturer[1]; m_Mutable.szMakePNP[2] = pProduct->manufacturer[2]; m_Mutable.szMakePNP[3] = '\0'; m_Mutable.pszMake = m_Mutable.szMakePNP; auto pnpIter = pnps.find( m_Mutable.szMakePNP ); if ( pnpIter != pnps.end() ) m_Mutable.pszMake = pnpIter->second.c_str(); const di_edid_display_descriptor *const *pDescriptors = di_edid_get_display_descriptors( pEdid ); for ( size_t i = 0; pDescriptors[i] != nullptr; i++ ) { const di_edid_display_descriptor *pDesc = pDescriptors[i]; const di_edid_display_descriptor_tag eTag = di_edid_display_descriptor_get_tag( pDesc ); if ( eTag == DI_EDID_DISPLAY_DESCRIPTOR_PRODUCT_NAME ) { // Max length of di_edid_display_descriptor_get_string is 14 // m_szModel is 16 bytes. const char *pszModel = di_edid_display_descriptor_get_string( pDesc ); strncpy( m_Mutable.szModel, pszModel, sizeof( m_Mutable.szModel ) ); } else if ( eTag == DI_EDID_DISPLAY_DESCRIPTOR_DATA_STRING ) { const char *pszDataString = di_edid_display_descriptor_get_string( pDesc ); strncpy( m_Mutable.szDataString, pszDataString, sizeof( m_Mutable.szDataString ) ); } } drm_log.infof("Connector %s -> %s - %s", m_Mutable.szName, m_Mutable.szMakePNP, m_Mutable.szModel ); bool bHasKnownColorimetry = false; bool bHasKnownHDRInfo = false; m_Mutable.ValidDynamicRefreshRates.clear(); m_Mutable.fnDynamicModeGenerator = nullptr; { CScriptScopedLock script; auto oKnownDisplay = script.Manager().Gamescope().Config.LookupDisplay( script, m_Mutable.szMakePNP, pProduct->product, m_Mutable.szModel, m_Mutable.szDataString ); if ( oKnownDisplay ) { sol::table tTable = oKnownDisplay->second; std::string_view psvPrettyName = tTable.get_or( "pretty_name", std::string_view{ "Untitled Display" } ); drm_log.infof( "Got known display: %.*s (%.*s)", (int)oKnownDisplay->first.size(), oKnownDisplay->first.data(), (int)psvPrettyName.size(), psvPrettyName.data() ); sol::optional otDynamicRefreshRates = tTable["dynamic_refresh_rates"]; sol::optional ofnDynamicModegen = tTable["dynamic_modegen"]; if ( otDynamicRefreshRates && ofnDynamicModegen ) { m_Mutable.ValidDynamicRefreshRates = TableToVector( *otDynamicRefreshRates ); m_Mutable.fnDynamicModeGenerator = [ fnDynamicModegen = *ofnDynamicModegen ]( const drmModeModeInfo *pBaseMode, int nRefreshHz ) -> drmModeModeInfo { CScriptScopedLock script; sol::table tInMode = script->create_table(); tInMode["clock"] = pBaseMode->clock; tInMode["hdisplay"] = pBaseMode->hdisplay; tInMode["hsync_start"] = pBaseMode->hsync_start; tInMode["hsync_end"] = pBaseMode->hsync_end; tInMode["htotal"] = pBaseMode->htotal; tInMode["vdisplay"] = pBaseMode->vdisplay; tInMode["vsync_start"] = pBaseMode->vsync_start; tInMode["vsync_end"] = pBaseMode->vsync_end; tInMode["vtotal"] = pBaseMode->vtotal; tInMode["vrefresh"] = pBaseMode->vrefresh; sol::function_result ret = fnDynamicModegen(tInMode, nRefreshHz); if ( !ret.valid() || !ret.get() ) return *pBaseMode; sol::table tOutMode = ret; drmModeModeInfo outMode = *pBaseMode; outMode.clock = tOutMode["clock"]; outMode.hdisplay = tOutMode["hdisplay"]; outMode.hsync_start = tOutMode["hsync_start"]; outMode.hsync_end = tOutMode["hsync_end"]; outMode.htotal = tOutMode["htotal"]; outMode.vdisplay = tOutMode["vdisplay"]; outMode.vsync_start = tOutMode["vsync_start"]; outMode.vsync_end = tOutMode["vsync_end"]; outMode.vtotal = tOutMode["vtotal"]; outMode.vrefresh = tOutMode["vrefresh"]; snprintf( outMode.name, sizeof( outMode.name ), "%dx%d@%d.00", outMode.hdisplay, outMode.vdisplay, nRefreshHz ); return outMode; }; } if ( sol::optional otColorimetry = tTable["colorimetry"] ) { sol::table tColorimetry = *otColorimetry; // TODO: Add a vec2 + colorimetry type? sol::optional otR = tColorimetry["r"]; sol::optional otG = tColorimetry["g"]; sol::optional otB = tColorimetry["b"]; sol::optional otW = tColorimetry["w"]; if ( otR && otG && otB && otW ) { m_Mutable.DisplayColorimetry.primaries.r = TableToVec( *otR ); m_Mutable.DisplayColorimetry.primaries.g = TableToVec( *otG ); m_Mutable.DisplayColorimetry.primaries.b = TableToVec( *otB ); m_Mutable.DisplayColorimetry.white = TableToVec( *otW ); bHasKnownColorimetry = true; } } if ( sol::optional otHDRInfo = tTable["hdr"] ) { m_Mutable.HDR.bExposeHDRSupport = otHDRInfo->get_or( "supported", false ); m_Mutable.HDR.eOutputEncodingEOTF = otHDRInfo->get_or( "eotf", EOTF_Gamma22 ); m_Mutable.HDR.uMaxContentLightLevel = nits_to_u16( otHDRInfo->get_or( "max_content_light_level", 400.0f ) ); m_Mutable.HDR.uMaxFrameAverageLuminance = nits_to_u16( otHDRInfo->get_or( "max_frame_average_luminance", 400.0f ) ); m_Mutable.HDR.uMinContentLightLevel = nits_to_u16_dark( otHDRInfo->get_or( "min_content_light_level", 0.1f ) ); bHasKnownHDRInfo = true; } } else { // Unknown display, see if there are any other refresh rates in the EDID we can get. if ( GetScreenType() == GAMESCOPE_SCREEN_TYPE_INTERNAL || cv_drm_allow_dynamic_modes_for_external_display ) { const drmModeModeInfo *pPreferredMode = find_mode( m_pConnector.get(), 0, 0, 0 ); if ( pPreferredMode ) { // See if the EDID has any modes for us. for (int i = 0; i < m_pConnector->count_modes; i++) { const drmModeModeInfo *pMode = &m_pConnector->modes[i]; if ( pMode->hdisplay != pPreferredMode->hdisplay || pMode->vdisplay != pPreferredMode->vdisplay ) continue; if ( !Algorithm::Contains( m_Mutable.ValidDynamicRefreshRates, pMode->vrefresh ) ) { m_Mutable.ValidDynamicRefreshRates.push_back( pMode->vrefresh ); } } std::sort( m_Mutable.ValidDynamicRefreshRates.begin(), m_Mutable.ValidDynamicRefreshRates.end() ); } } } } if ( !bHasKnownColorimetry ) { // Steam Deck OLED has calibrated chromaticity coordinates in the EDID // for each unit. // Other external displays probably have this too. const di_edid_chromaticity_coords *pChroma = di_edid_get_chromaticity_coords( pEdid ); if ( pChroma && pChroma->red_x != 0.0f ) { drm_log.infof( "[colorimetry]: EDID with colorimetry detected. Using it" ); m_Mutable.DisplayColorimetry = displaycolorimetry_t { .primaries = { { pChroma->red_x, pChroma->red_y }, { pChroma->green_x, pChroma->green_y }, { pChroma->blue_x, pChroma->blue_y } }, .white = { pChroma->white_x, pChroma->white_y }, }; } else { // Assume 709 if we have no data at all. m_Mutable.DisplayColorimetry = displaycolorimetry_709; } } drm_log.infof( "[colorimetry]: r %f %f", m_Mutable.DisplayColorimetry.primaries.r.x, m_Mutable.DisplayColorimetry.primaries.r.y ); drm_log.infof( "[colorimetry]: g %f %f", m_Mutable.DisplayColorimetry.primaries.g.x, m_Mutable.DisplayColorimetry.primaries.g.y ); drm_log.infof( "[colorimetry]: b %f %f", m_Mutable.DisplayColorimetry.primaries.b.x, m_Mutable.DisplayColorimetry.primaries.b.y ); drm_log.infof( "[colorimetry]: w %f %f", m_Mutable.DisplayColorimetry.white.x, m_Mutable.DisplayColorimetry.white.y ); ///////////////////// // Parse HDR stuff. ///////////////////// if ( !bHasKnownHDRInfo ) { const di_cta_hdr_static_metadata_block *pHDRStaticMetadata = nullptr; const di_cta_colorimetry_block *pColorimetry = nullptr; const di_edid_cta* pCTA = NULL; const di_edid_ext *const *ppExts = di_edid_get_extensions( pEdid ); for ( ; *ppExts != nullptr; ppExts++ ) { if ( ( pCTA = di_edid_ext_get_cta( *ppExts ) ) ) break; } if ( pCTA ) { const di_cta_data_block *const *ppBlocks = di_edid_cta_get_data_blocks( pCTA ); for ( ; *ppBlocks != nullptr; ppBlocks++ ) { if ( di_cta_data_block_get_tag( *ppBlocks ) == DI_CTA_DATA_BLOCK_HDR_STATIC_METADATA ) { pHDRStaticMetadata = di_cta_data_block_get_hdr_static_metadata( *ppBlocks ); continue; } if ( di_cta_data_block_get_tag( *ppBlocks ) == DI_CTA_DATA_BLOCK_COLORIMETRY ) { pColorimetry = di_cta_data_block_get_colorimetry( *ppBlocks ); continue; } } } if ( pColorimetry && pColorimetry->bt2020_rgb && pHDRStaticMetadata && pHDRStaticMetadata->eotfs && pHDRStaticMetadata->eotfs->pq ) { m_Mutable.HDR.bExposeHDRSupport = true; m_Mutable.HDR.eOutputEncodingEOTF = EOTF_PQ; m_Mutable.HDR.uMaxContentLightLevel = pHDRStaticMetadata->desired_content_max_luminance ? nits_to_u16( pHDRStaticMetadata->desired_content_max_luminance ) : nits_to_u16( 1499.0f ); m_Mutable.HDR.uMaxFrameAverageLuminance = pHDRStaticMetadata->desired_content_max_frame_avg_luminance ? nits_to_u16( pHDRStaticMetadata->desired_content_max_frame_avg_luminance ) : nits_to_u16( std::min( 799.f, nits_from_u16( m_Mutable.HDR.uMaxContentLightLevel ) ) ); m_Mutable.HDR.uMinContentLightLevel = pHDRStaticMetadata->desired_content_min_luminance ? nits_to_u16_dark( pHDRStaticMetadata->desired_content_min_luminance ) : nits_to_u16_dark( 0.0f ); // Generate a default HDR10 infoframe. hdr_output_metadata defaultHDRMetadata{}; hdr_metadata_infoframe *pInfoframe = &defaultHDRMetadata.hdmi_metadata_type1; // To be filled in by the app based on the scene, default to desired_content_max_luminance // // Using display's max_fall for the default metadata max_cll to avoid displays // overcompensating with tonemapping for SDR content. uint16_t uDefaultInfoframeLuminances = m_Mutable.HDR.uMaxFrameAverageLuminance; pInfoframe->display_primaries[0].x = color_xy_to_u16( m_Mutable.DisplayColorimetry.primaries.r.x ); pInfoframe->display_primaries[0].y = color_xy_to_u16( m_Mutable.DisplayColorimetry.primaries.r.y ); pInfoframe->display_primaries[1].x = color_xy_to_u16( m_Mutable.DisplayColorimetry.primaries.g.x ); pInfoframe->display_primaries[1].y = color_xy_to_u16( m_Mutable.DisplayColorimetry.primaries.g.y ); pInfoframe->display_primaries[2].x = color_xy_to_u16( m_Mutable.DisplayColorimetry.primaries.b.x ); pInfoframe->display_primaries[2].y = color_xy_to_u16( m_Mutable.DisplayColorimetry.primaries.b.y ); pInfoframe->white_point.x = color_xy_to_u16( m_Mutable.DisplayColorimetry.white.x ); pInfoframe->white_point.y = color_xy_to_u16( m_Mutable.DisplayColorimetry.white.y ); pInfoframe->max_display_mastering_luminance = uDefaultInfoframeLuminances; pInfoframe->min_display_mastering_luminance = m_Mutable.HDR.uMinContentLightLevel; pInfoframe->max_cll = uDefaultInfoframeLuminances; pInfoframe->max_fall = uDefaultInfoframeLuminances; pInfoframe->eotf = HDMI_EOTF_ST2084; m_Mutable.HDR.pDefaultMetadataBlob = GetBackend()->CreateBackendBlob( defaultHDRMetadata ); } else { m_Mutable.HDR.bExposeHDRSupport = false; } } } ///////////////////////// // CDRMFb ///////////////////////// CDRMFb::CDRMFb( uint32_t uFbId ) : m_uFbId{ uFbId } { } CDRMFb::~CDRMFb() { // I own the fbid. if ( drmModeRmFB( g_DRM.fd, m_uFbId ) != 0 ) drm_log.errorf_errno( "drmModeRmFB failed" ); m_uFbId = 0; } } static int drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, bool needs_modeset ) { auto entry = FrameInfoToLiftoffStateCacheEntry( drm, frameInfo ); // If we are modesetting, reset the state cache, we might // move to another CRTC or whatever which might have differing caps. // (same with different modes) if (needs_modeset) g_LiftoffStateCache.clear(); if (is_liftoff_caching_enabled()) { if (g_LiftoffStateCache.count(entry) != 0) return -EINVAL; } bool bSinglePlane = frameInfo->layerCount < 2 && cv_drm_single_plane_optimizations; for ( int i = 0; i < k_nMaxLayers; i++ ) { if ( i < frameInfo->layerCount ) { const FrameInfo_t::Layer_t *pLayer = &frameInfo->layers[ i ]; gamescope::CDRMFb *pDrmFb = static_cast( pLayer->tex ? pLayer->tex->GetBackendFb() : nullptr ); if ( pDrmFb == nullptr ) { drm_log.debugf("drm_prepare_liftoff: layer %d has no FB", i ); return -EINVAL; } const int nFence = cv_drm_debug_disable_in_fence_fd ? -1 : g_nAlwaysSignalledSyncFile; liftoff_layer_set_property( drm->lo_layers[ i ], "FB_ID", pDrmFb->GetFbId()); liftoff_layer_set_property( drm->lo_layers[ i ], "IN_FENCE_FD", nFence ); drm->m_FbIdsInRequest.emplace_back( pDrmFb ); liftoff_layer_set_property( drm->lo_layers[ i ], "zpos", entry.layerState[i].zpos ); liftoff_layer_set_property( drm->lo_layers[ i ], "alpha", frameInfo->layers[ i ].opacity * 0xffff); if ( entry.layerState[i].zpos != g_zposBase ) { liftoff_layer_set_property( drm->lo_layers[ i ], "pixel blend mode", (uint64_t) frameInfo->layers[i].eAlphaBlendingMode ); } else { liftoff_layer_unset_property( drm->lo_layers[ i ], "pixel blend mode" ); } liftoff_layer_set_property( drm->lo_layers[ i ], "SRC_X", 0); liftoff_layer_set_property( drm->lo_layers[ i ], "SRC_Y", 0); liftoff_layer_set_property( drm->lo_layers[ i ], "SRC_W", entry.layerState[i].srcW ); liftoff_layer_set_property( drm->lo_layers[ i ], "SRC_H", entry.layerState[i].srcH ); uint64_t ulOrientation = DRM_MODE_ROTATE_0; switch ( drm->pConnector->GetCurrentOrientation() ) { default: case GAMESCOPE_PANEL_ORIENTATION_0: ulOrientation = DRM_MODE_ROTATE_0; break; case GAMESCOPE_PANEL_ORIENTATION_270: ulOrientation = DRM_MODE_ROTATE_270; break; case GAMESCOPE_PANEL_ORIENTATION_90: ulOrientation = DRM_MODE_ROTATE_90; break; case GAMESCOPE_PANEL_ORIENTATION_180: ulOrientation = DRM_MODE_ROTATE_180; break; } liftoff_layer_set_property( drm->lo_layers[ i ], "rotation", ulOrientation ); liftoff_layer_set_property( drm->lo_layers[ i ], "CRTC_X", entry.layerState[i].crtcX); liftoff_layer_set_property( drm->lo_layers[ i ], "CRTC_Y", entry.layerState[i].crtcY); liftoff_layer_set_property( drm->lo_layers[ i ], "CRTC_W", entry.layerState[i].crtcW); liftoff_layer_set_property( drm->lo_layers[ i ], "CRTC_H", entry.layerState[i].crtcH); if ( frameInfo->layers[i].applyColorMgmt ) { bool bYCbCr = entry.layerState[i].ycbcr; if ( !cv_drm_debug_disable_color_encoding && bYCbCr ) { liftoff_layer_set_property( drm->lo_layers[ i ], "COLOR_ENCODING", entry.layerState[i].colorEncoding ); } else { liftoff_layer_unset_property( drm->lo_layers[ i ], "COLOR_ENCODING" ); } if ( !cv_drm_debug_disable_color_range && bYCbCr ) { liftoff_layer_set_property( drm->lo_layers[ i ], "COLOR_RANGE", entry.layerState[i].colorRange ); } else { liftoff_layer_unset_property( drm->lo_layers[ i ], "COLOR_RANGE" ); } if ( drm_supports_color_mgmt( drm ) ) { amdgpu_transfer_function degamma_tf = colorspace_to_plane_degamma_tf( entry.layerState[i].colorspace ); amdgpu_transfer_function shaper_tf = colorspace_to_plane_shaper_tf( entry.layerState[i].colorspace ); if ( bYCbCr ) { // JoshA: Based on the Steam In-Home Streaming Shader, // it looks like Y is actually sRGB, not HDTV G2.4 // // Matching BT709 for degamma -> regamma on shaper TF here // is identity and works on YUV NV12 planes to preserve this. // // Doing LINEAR/DEFAULT here introduces banding so... this is the best way. // (sRGB DEGAMMA does NOT work on YUV planes!) degamma_tf = AMDGPU_TRANSFER_FUNCTION_BT709_INV_OETF; shaper_tf = AMDGPU_TRANSFER_FUNCTION_BT709_OETF; } bool bUseDegamma = !cv_drm_debug_disable_degamma_tf; if ( bUseDegamma ) liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_DEGAMMA_TF", degamma_tf ); else liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_DEGAMMA_TF", 0 ); bool bUseShaperAnd3DLUT = !cv_drm_debug_disable_shaper_and_3dlut; if ( bUseShaperAnd3DLUT ) { liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_SHAPER_LUT", drm->pending.shaperlut_id[ ColorSpaceToEOTFIndex( entry.layerState[i].colorspace ) ]->GetBlobValue() ); liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_SHAPER_TF", shaper_tf ); liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_LUT3D", drm->pending.lut3d_id[ ColorSpaceToEOTFIndex( entry.layerState[i].colorspace ) ]->GetBlobValue() ); // Josh: See shaders/colorimetry.h colorspace_blend_tf if you have questions as to why we start doing sRGB for BLEND_TF despite potentially working in Gamma 2.2 space prior. } else { liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_SHAPER_LUT", 0 ); liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_SHAPER_TF", 0 ); liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_LUT3D", 0 ); } } } else { if ( drm_supports_color_mgmt( drm ) ) { liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_DEGAMMA_TF", AMDGPU_TRANSFER_FUNCTION_DEFAULT ); liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_SHAPER_LUT", 0 ); liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_SHAPER_TF", 0 ); liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_LUT3D", 0 ); liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_CTM", 0 ); } } if ( drm_supports_color_mgmt( drm ) ) { if (!cv_drm_debug_disable_blend_tf && !bSinglePlane) liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_BLEND_TF", drm->pending.output_tf ); else liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_BLEND_TF", AMDGPU_TRANSFER_FUNCTION_DEFAULT ); if (!cv_drm_debug_disable_ctm && frameInfo->layers[i].ctm != nullptr) liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_CTM", frameInfo->layers[i].ctm->GetBlobValue() ); else liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_CTM", 0 ); } } else { liftoff_layer_set_property( drm->lo_layers[ i ], "FB_ID", 0 ); liftoff_layer_set_property( drm->lo_layers[ i ], "IN_FENCE_FD", -1 ); liftoff_layer_unset_property( drm->lo_layers[ i ], "COLOR_ENCODING" ); liftoff_layer_unset_property( drm->lo_layers[ i ], "COLOR_RANGE" ); if ( drm_supports_color_mgmt( drm ) ) { liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_DEGAMMA_TF", AMDGPU_TRANSFER_FUNCTION_DEFAULT ); liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_SHAPER_LUT", 0 ); liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_SHAPER_TF", 0 ); liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_LUT3D", 0 ); liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_BLEND_TF", AMDGPU_TRANSFER_FUNCTION_DEFAULT ); liftoff_layer_set_property( drm->lo_layers[ i ], "AMD_PLANE_CTM", 0 ); } } } struct liftoff_output_apply_options lo_options = { .timeout_ns = std::numeric_limits::max() }; int ret = liftoff_output_apply( drm->lo_output, drm->req, drm->flags, &lo_options); // The NVIDIA 555 series drivers started advertising DRM_CAP_SYNCOBJ, but do // not support IN_FENCE_FD. However, there is no way to hide the IN_FENCE_FD // property in a DRM-KMS driver, so the driver returns EPERM when an // application sets IN_FENCE_FD. To work around this, the first time a // commit fails with -EPERM, try it again with the IN_FENCE_FD property // reset to its default value. If this succeeds, disable use of the // IN_FENCE_FD property. static bool attempted_in_fence_fallback = false; if ( ret == -EPERM && !attempted_in_fence_fallback && !cv_drm_debug_disable_in_fence_fd ) { attempted_in_fence_fallback = true; for ( int i = 0; i < frameInfo->layerCount; i++ ) { liftoff_layer_set_property( drm->lo_layers[ i ], "IN_FENCE_FD", -1 ); } ret = liftoff_output_apply( drm->lo_output, drm->req, drm->flags, &lo_options ); if ( ret == 0 ) { // IN_FENCE_FD isn't actually supported. Avoid it in the future. cv_drm_debug_disable_in_fence_fd = true; } } if ( ret == 0 ) { // We don't support partial composition yet if ( liftoff_output_needs_composition( drm->lo_output ) ) ret = -EINVAL; } // If we aren't modesetting and we got -EINVAL, that means that we // probably can't do this layout, so add it to our state cache so we don't // try it again. if (!needs_modeset) { if (ret == -EINVAL) g_LiftoffStateCache.insert(entry); } if ( ret == 0 ) drm_log.debugf( "can drm present %i layers", frameInfo->layerCount ); else drm_log.debugf( "can NOT drm present %i layers", frameInfo->layerCount ); return ret; } bool g_bForceAsyncFlips = false; void drm_rollback( struct drm_t *drm ) { drm->pending = drm->current; for ( std::unique_ptr< gamescope::CDRMCRTC > &pCRTC : drm->crtcs ) { for ( std::optional &oProperty : pCRTC->GetProperties() ) { if ( oProperty ) oProperty->Rollback(); } } for ( std::unique_ptr< gamescope::CDRMPlane > &pPlane : drm->planes ) { for ( std::optional &oProperty : pPlane->GetProperties() ) { if ( oProperty ) oProperty->Rollback(); } } for ( auto &iter : drm->connectors ) { gamescope::CDRMConnector *pConnector = &iter.second; for ( std::optional &oProperty : pConnector->GetProperties() ) { if ( oProperty ) oProperty->Rollback(); } } } /* Prepares an atomic commit for the provided scene-graph. Returns 0 on success, * negative errno on failure or if the scene-graph can't be presented directly. */ int drm_prepare( struct drm_t *drm, bool async, const struct FrameInfo_t *frameInfo ) { if ( !drm->pConnector ) return -EACCES; drm_update_color_mgmt(drm); const bool bIsVRRCapable = drm->pConnector && drm->pConnector->GetProperties().vrr_capable && !!drm->pConnector->GetProperties().vrr_capable->GetCurrentValue(); const bool bHasVRREnable = drm->pCRTC && drm->pCRTC->GetProperties().VRR_ENABLED; const bool bVRREnabled = bIsVRRCapable && bHasVRREnable && frameInfo->allowVRR; if ( bIsVRRCapable ) { if ( bVRREnabled != !!drm->pCRTC->GetProperties().VRR_ENABLED->GetCurrentValue() ) drm->needs_modeset = true; } drm_colorspace uColorimetry = DRM_MODE_COLORIMETRY_DEFAULT; const bool bWantsHDR10 = g_bOutputHDREnabled && frameInfo->outputEncodingEOTF == EOTF_PQ; gamescope::BackendBlob *pHDRMetadata = nullptr; if ( drm->pConnector && drm->pConnector->SupportsHDR10() ) { if ( bWantsHDR10 ) { pHDRMetadata = drm->pConnector->GetHDRInfo().pDefaultMetadataBlob.get(); wlserver_vk_swapchain_feedback* pFeedback = steamcompmgr_get_base_layer_swapchain_feedback(); if ( pFeedback && pFeedback->hdr_metadata_blob != nullptr ) pHDRMetadata = pFeedback->hdr_metadata_blob.get(); uColorimetry = DRM_MODE_COLORIMETRY_BT2020_RGB; } else { pHDRMetadata = drm->sdr_static_metadata.get(); uColorimetry = DRM_MODE_COLORIMETRY_DEFAULT; } if ( uColorimetry != drm->pConnector->GetProperties().Colorspace->GetCurrentValue() ) drm->needs_modeset = true; } drm->m_FbIdsInRequest.clear(); bool needs_modeset = drm->needs_modeset.exchange(false); assert( drm->req == nullptr ); drm->req = drmModeAtomicAlloc(); bool bSinglePlane = frameInfo->layerCount < 2 && cv_drm_single_plane_optimizations; if ( drm_supports_color_mgmt( &g_DRM ) && frameInfo->applyOutputColorMgmt ) { if ( !cv_drm_debug_disable_output_tf && !bSinglePlane ) { drm->pending.output_tf = g_bOutputHDREnabled ? AMDGPU_TRANSFER_FUNCTION_PQ_EOTF : AMDGPU_TRANSFER_FUNCTION_SRGB_EOTF; } else { drm->pending.output_tf = AMDGPU_TRANSFER_FUNCTION_DEFAULT; } } else { drm->pending.output_tf = AMDGPU_TRANSFER_FUNCTION_DEFAULT; } uint32_t flags = DRM_MODE_ATOMIC_NONBLOCK; // We do internal refcounting with these events bool bSleep = false; if ( drm->pConnector ) { bSleep = cv_drm_sleep_screens[ drm->pConnector->GetScreenType() ]; bool bCurrentlyAsleep = drm->pConnector->GetProperties().CRTC_ID->GetCurrentValue() == 0; if ( bCurrentlyAsleep != bSleep ) needs_modeset = true; } if ( !bSleep ) { if ( drm->pCRTC != nullptr ) flags |= DRM_MODE_PAGE_FLIP_EVENT; if ( async || g_bForceAsyncFlips ) flags |= DRM_MODE_PAGE_FLIP_ASYNC; } bool bForceInRequest = needs_modeset; if ( needs_modeset ) { flags |= DRM_MODE_ATOMIC_ALLOW_MODESET; // Disable all connectors and CRTCs for ( auto &iter : drm->connectors ) { gamescope::CDRMConnector *pConnector = &iter.second; if ( pConnector->GetProperties().CRTC_ID->GetCurrentValue() == 0 ) continue; pConnector->GetProperties().CRTC_ID->SetPendingValue( drm->req, 0, bForceInRequest ); if ( pConnector->GetProperties().Colorspace ) pConnector->GetProperties().Colorspace->SetPendingValue( drm->req, 0, bForceInRequest ); if ( pConnector->GetProperties().HDR_OUTPUT_METADATA ) pConnector->GetProperties().HDR_OUTPUT_METADATA->SetPendingValue( drm->req, 0, bForceInRequest ); if ( pConnector->GetProperties().content_type ) pConnector->GetProperties().content_type->SetPendingValue( drm->req, 0, bForceInRequest ); if ( pConnector->GetProperties().Broadcast_RGB ) pConnector->GetProperties().Broadcast_RGB->SetPendingValue( drm->req, 0, bForceInRequest ); } for ( std::unique_ptr< gamescope::CDRMCRTC > &pCRTC : drm->crtcs ) { // We can't disable a CRTC if it's already disabled, or else the // kernel will error out with "requesting event but off". if ( pCRTC->GetProperties().ACTIVE->GetCurrentValue() == 0 ) continue; pCRTC->GetProperties().ACTIVE->SetPendingValue( drm->req, 0, bForceInRequest ); pCRTC->GetProperties().MODE_ID->SetPendingValue( drm->req, 0, bForceInRequest ); if ( pCRTC->GetProperties().GAMMA_LUT ) pCRTC->GetProperties().GAMMA_LUT->SetPendingValue( drm->req, 0, bForceInRequest ); if ( pCRTC->GetProperties().DEGAMMA_LUT ) pCRTC->GetProperties().DEGAMMA_LUT->SetPendingValue( drm->req, 0, bForceInRequest ); if ( pCRTC->GetProperties().CTM ) pCRTC->GetProperties().CTM->SetPendingValue( drm->req, 0, bForceInRequest ); if ( pCRTC->GetProperties().VRR_ENABLED ) pCRTC->GetProperties().VRR_ENABLED->SetPendingValue( drm->req, 0, bForceInRequest ); if ( pCRTC->GetProperties().OUT_FENCE_PTR ) pCRTC->GetProperties().OUT_FENCE_PTR->SetPendingValue( drm->req, 0, bForceInRequest ); if ( pCRTC->GetProperties().AMD_CRTC_REGAMMA_TF ) pCRTC->GetProperties().AMD_CRTC_REGAMMA_TF->SetPendingValue( drm->req, 0, bForceInRequest ); } if ( drm->pConnector && !bSleep ) { // Always set our CRTC_ID for the modeset, especially // as we zero-ed it above. drm->pConnector->GetProperties().CRTC_ID->SetPendingValue( drm->req, drm->pCRTC->GetObjectId(), bForceInRequest ); if ( drm->pConnector->GetProperties().Colorspace ) drm->pConnector->GetProperties().Colorspace->SetPendingValue( drm->req, uColorimetry, bForceInRequest ); } if ( drm->pCRTC && !bSleep ) { drm->pCRTC->GetProperties().ACTIVE->SetPendingValue( drm->req, 1u, true ); drm->pCRTC->GetProperties().MODE_ID->SetPendingValue( drm->req, drm->pending.mode_id ? drm->pending.mode_id->GetBlobValue() : 0lu, true ); if ( drm->pCRTC->GetProperties().VRR_ENABLED ) drm->pCRTC->GetProperties().VRR_ENABLED->SetPendingValue( drm->req, bVRREnabled, true ); } } if ( drm->pConnector && !bSleep ) { if ( drm->pConnector->GetProperties().HDR_OUTPUT_METADATA ) drm->pConnector->GetProperties().HDR_OUTPUT_METADATA->SetPendingValue( drm->req, pHDRMetadata ? pHDRMetadata->GetBlobValue() : 0lu, bForceInRequest ); if ( drm->pConnector->GetProperties().content_type ) drm->pConnector->GetProperties().content_type->SetPendingValue( drm->req, DRM_MODE_CONTENT_TYPE_GAME, bForceInRequest ); GamescopeBroadcastRGBMode_t eBroadcastRGB = drm->pConnector->GetScreenType() == gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL ? s_ExternalBroadcastRGBMode : GAMESCOPE_BROADCAST_RGB_MODE_AUTOMATIC; if ( drm->pConnector->GetProperties().Broadcast_RGB ) drm->pConnector->GetProperties().Broadcast_RGB->SetPendingValue( drm->req, eBroadcastRGB, bForceInRequest ); } if ( drm->pCRTC && !bSleep ) { if ( drm->pCRTC->GetProperties().AMD_CRTC_REGAMMA_TF ) { if ( !cv_drm_debug_disable_regamma_tf ) drm->pCRTC->GetProperties().AMD_CRTC_REGAMMA_TF->SetPendingValue( drm->req, inverse_tf( drm->pending.output_tf ), bForceInRequest ); else drm->pCRTC->GetProperties().AMD_CRTC_REGAMMA_TF->SetPendingValue( drm->req, AMDGPU_TRANSFER_FUNCTION_DEFAULT, bForceInRequest ); } } drm->flags = flags; int ret; if ( drm->pCRTC == nullptr || bSleep ) { ret = 0; } else if ( drm->bUseLiftoff ) { ret = drm_prepare_liftoff( drm, frameInfo, needs_modeset ); } else { ret = 0; } if ( ret != 0 ) { drm_rollback( drm ); drmModeAtomicFree( drm->req ); drm->req = nullptr; drm->m_FbIdsInRequest.clear(); if ( needs_modeset ) drm->needs_modeset = true; } return ret; } bool drm_poll_state( struct drm_t *drm ) { int out_of_date = drm->out_of_date.exchange(false); if ( !out_of_date ) return false; refresh_state( drm ); setup_best_connector(drm, out_of_date >= 2, false); return true; } static bool drm_set_crtc( struct drm_t *drm, gamescope::CDRMCRTC *pCRTC ) { drm->pCRTC = pCRTC; drm->needs_modeset = true; drm->pPrimaryPlane = find_primary_plane( drm ); if ( drm->pPrimaryPlane == nullptr ) { drm_log.errorf("could not find a suitable primary plane"); return false; } struct liftoff_output *lo_output = liftoff_output_create( drm->lo_device, pCRTC->GetObjectId() ); if ( lo_output == nullptr ) return false; for ( int i = 0; i < k_nMaxLayers; i++ ) { liftoff_layer_destroy( drm->lo_layers[ i ] ); drm->lo_layers[ i ] = liftoff_layer_create( lo_output ); if ( drm->lo_layers[ i ] == nullptr ) return false; } liftoff_output_destroy( drm->lo_output ); drm->lo_output = lo_output; return true; } bool drm_set_connector( struct drm_t *drm, gamescope::CDRMConnector *conn ) { drm_log.infof("selecting connector %s", conn->GetName()); gamescope::CDRMCRTC *pCRTC = find_crtc_for_connector(drm, conn); if (pCRTC == nullptr) { drm_log.errorf("no CRTC found!"); return false; } if (!drm_set_crtc(drm, pCRTC)) { return false; } // If we are changing connector, zero out the current and pending mode IDs. // So we don't try to use one mode from the old connector on the new one if we roll back. drm->pending.mode_id = nullptr; drm->current.mode_id = nullptr; drm->pConnector = conn; drm->needs_modeset = true; return true; } static void drm_unset_connector( struct drm_t *drm ) { drm->pCRTC = nullptr; drm->pPrimaryPlane = nullptr; for ( int i = 0; i < k_nMaxLayers; i++ ) { liftoff_layer_destroy( drm->lo_layers[ i ] ); drm->lo_layers[ i ] = nullptr; } liftoff_output_destroy(drm->lo_output); drm->lo_output = nullptr; drm->pConnector = nullptr; drm->needs_modeset = true; } bool drm_get_vrr_in_use(struct drm_t *drm) { if ( !drm->pCRTC || !drm->pCRTC->GetProperties().VRR_ENABLED ) return false; return !!drm->pCRTC->GetProperties().VRR_ENABLED->GetCurrentValue(); } gamescope::GamescopeScreenType drm_get_screen_type(struct drm_t *drm) { if ( !drm->pConnector ) return gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL; return drm->pConnector->GetScreenType(); } bool drm_update_color_mgmt(struct drm_t *drm) { if ( !drm_supports_color_mgmt( drm ) ) return true; if ( g_ColorMgmt.serial == drm->current.color_mgmt_serial ) return true; drm->pending.color_mgmt_serial = g_ColorMgmt.serial; for ( uint32_t i = 0; i < EOTF_Count; i++ ) { drm->pending.shaperlut_id[ i ] = 0; drm->pending.lut3d_id[ i ] = 0; } for ( uint32_t i = 0; i < EOTF_Count; i++ ) { if ( !g_ColorMgmtLuts[i].HasLuts() ) continue; drm->pending.shaperlut_id[ i ] = GetBackend()->CreateBackendBlob( g_ColorMgmtLuts[i].lut1d ); drm->pending.lut3d_id[ i ] = GetBackend()->CreateBackendBlob( g_ColorMgmtLuts[i].lut3d ); } return true; } int g_nDynamicRefreshHz = 0; static void drm_unset_mode( struct drm_t *drm ) { drm->pending.mode_id = 0; drm->needs_modeset = true; g_nOutputWidth = drm->preferred_width; g_nOutputHeight = drm->preferred_height; if (g_nOutputHeight == 0) g_nOutputHeight = 720; if (g_nOutputWidth == 0) g_nOutputWidth = g_nOutputHeight * 16 / 9; g_nOutputRefresh = drm->preferred_refresh; if (g_nOutputRefresh == 0) g_nOutputRefresh = gamescope::ConvertHztomHz( 60 ); g_nDynamicRefreshHz = 0; g_bRotated = false; } bool drm_set_mode( struct drm_t *drm, const drmModeModeInfo *mode ) { if (!drm->pConnector || !drm->pConnector->GetModeConnector()) return false; drm_log.infof("selecting mode %dx%d@%uHz", mode->hdisplay, mode->vdisplay, mode->vrefresh); drm->pending.mode_id = GetBackend()->CreateBackendBlob( *mode ); drm->needs_modeset = true; g_nOutputRefresh = gamescope::GetModeRefresh( mode ); g_nDynamicRefreshHz = 0; update_drm_effective_orientations(drm, mode); switch ( drm->pConnector->GetCurrentOrientation() ) { default: case GAMESCOPE_PANEL_ORIENTATION_0: case GAMESCOPE_PANEL_ORIENTATION_180: g_bRotated = false; g_nOutputWidth = mode->hdisplay; g_nOutputHeight = mode->vdisplay; break; case GAMESCOPE_PANEL_ORIENTATION_90: case GAMESCOPE_PANEL_ORIENTATION_270: g_bRotated = true; g_nOutputWidth = mode->vdisplay; g_nOutputHeight = mode->hdisplay; break; } return true; } bool drm_set_refresh( struct drm_t *drm, int refresh ) { int width = g_nOutputWidth; int height = g_nOutputHeight; if ( g_bRotated ) { int tmp = width; width = height; height = tmp; } if (!drm->pConnector || !drm->pConnector->GetModeConnector()) return false; drmModeConnector *connector = drm->pConnector->GetModeConnector(); const drmModeModeInfo *existing_mode = find_mode(connector, width, height, refresh); drmModeModeInfo mode = {0}; if ( existing_mode ) { mode = *existing_mode; } else { if ( g_DRM.pConnector && g_DRM.pConnector->GetModeGenerator() ) { const drmModeModeInfo *preferred_mode = find_mode(connector, 0, 0, 0); mode = g_DRM.pConnector->GetModeGenerator()( preferred_mode, refresh ); } else { /* TODO: check refresh is within the EDID limits */ switch ( g_eGamescopeModeGeneration ) { case gamescope::GAMESCOPE_MODE_GENERATE_CVT: generate_cvt_mode( &mode, width, height, refresh, true, false ); break; case gamescope::GAMESCOPE_MODE_GENERATE_FIXED: { const drmModeModeInfo *preferred_mode = find_mode(connector, 0, 0, 0); generate_fixed_mode( &mode, preferred_mode, refresh ); break; } } } } mode.type = DRM_MODE_TYPE_USERDEF; bool bSuccess = drm_set_mode(drm, &mode); if ( !bSuccess ) return false; g_nDynamicRefreshHz = refresh; return true; } bool drm_set_resolution( struct drm_t *drm, int width, int height ) { if (!drm->pConnector || !drm->pConnector->GetModeConnector()) return false; drmModeConnector *connector = drm->pConnector->GetModeConnector(); const drmModeModeInfo *mode = find_mode(connector, width, height, 0); if ( !mode ) { return false; } return drm_set_mode(drm, mode); } bool drm_get_vrr_capable(struct drm_t *drm) { if ( drm->pConnector ) return drm->pConnector->SupportsVRR(); return false; } bool drm_supports_hdr( struct drm_t *drm, uint16_t *maxCLL, uint16_t *maxFALL ) { if ( drm->pConnector && drm->pConnector->SupportsHDR() ) { if ( maxCLL ) *maxCLL = drm->pConnector->GetHDRInfo().uMaxContentLightLevel; if ( maxFALL ) *maxFALL = drm->pConnector->GetHDRInfo().uMaxFrameAverageLuminance; return true; } return false; } const char *drm_get_connector_name(struct drm_t *drm) { if ( !drm->pConnector ) return nullptr; return drm->pConnector->GetName(); } const char *drm_get_device_name(struct drm_t *drm) { return drm->device_name; } std::pair drm_get_connector_identifier(struct drm_t *drm) { if ( !drm->pConnector ) return { 0u, 0u }; return std::make_pair(drm->pConnector->GetModeConnector()->connector_type, drm->pConnector->GetModeConnector()->connector_type_id); } bool drm_supports_color_mgmt(struct drm_t *drm) { if ( g_bForceDisableColorMgmt ) return false; if ( !drm->pPrimaryPlane ) return false; return drm->pPrimaryPlane->GetProperties().AMD_PLANE_CTM.has_value() && drm->pPrimaryPlane->GetProperties().AMD_PLANE_BLEND_TF.has_value(); } std::span drm_get_valid_refresh_rates( struct drm_t *drm ) { if ( drm && drm->pConnector ) return drm->pConnector->GetValidDynamicRefreshRates(); return std::span{}; } namespace gamescope { class CDRMBackend; class CDRMBackend final : public CBaseBackend { public: CDRMBackend() { } virtual ~CDRMBackend() { if ( g_DRM.fd != -1 ) finish_drm( &g_DRM ); } virtual bool Init() override { if ( !vulkan_init( vulkan_get_instance(), VK_NULL_HANDLE ) ) { fprintf( stderr, "Failed to initialize Vulkan\n" ); return false; } if ( !wlsession_init() ) { fprintf( stderr, "Failed to initialize Wayland session\n" ); return false; } return init_drm( &g_DRM, g_nPreferredOutputWidth, g_nPreferredOutputHeight, g_nNestedRefresh ); } virtual bool PostInit() override { if ( g_DRM.pConnector ) WritePatchedEdid( g_DRM.pConnector->GetRawEDID(), g_DRM.pConnector->GetHDRInfo(), g_bRotated ); return true; } virtual std::span GetInstanceExtensions() const override { return std::span{}; } virtual std::span GetDeviceExtensions( VkPhysicalDevice pVkPhysicalDevice ) const override { return std::span{}; } virtual VkImageLayout GetPresentLayout() const override { // Does not matter, as this has a queue family transition // to VK_QUEUE_FAMILY_FOREIGN_EXT queue, // thus: newLayout is ignored. return VK_IMAGE_LAYOUT_GENERAL; } virtual void GetPreferredOutputFormat( uint32_t *pPrimaryPlaneFormat, uint32_t *pOverlayPlaneFormat ) const override { *pPrimaryPlaneFormat = g_nDRMFormat; *pOverlayPlaneFormat = g_nDRMFormatOverlay; } virtual bool ValidPhysicalDevice( VkPhysicalDevice pVkPhysicalDevice ) const override { return true; } virtual int Present( const FrameInfo_t *pFrameInfo, bool bAsync ) { static uint64_t s_ulLastTime = get_time_in_nanos(); uint64_t ulNow = get_time_in_nanos(); drm_log.debugf( "CDRMBackend::Present Begin: %lu -> delta: %lu", ulNow, ulNow - s_ulLastTime ); s_ulLastTime = ulNow; bool bWantsPartialComposite = pFrameInfo->layerCount >= 3 && !kDisablePartialComposition; static bool s_bWasFirstFrame = true; bool bWasFirstFrame = s_bWasFirstFrame; s_bWasFirstFrame = false; bool bDrewCursor = false; for ( uint32_t i = 0; i < k_nMaxLayers; i++ ) { if ( pFrameInfo->layers[i].zpos == g_zposCursor ) { bDrewCursor = true; break; } } bool bLayer0ScreenSize = close_enough(pFrameInfo->layers[0].scale.x, 1.0f) && close_enough(pFrameInfo->layers[0].scale.y, 1.0f); bool bNeedsCompositeFromFilter = (g_upscaleFilter == GamescopeUpscaleFilter::NEAREST || g_upscaleFilter == GamescopeUpscaleFilter::PIXEL) && !bLayer0ScreenSize; bool bNeedsFullComposite = false; bNeedsFullComposite |= cv_composite_force; bNeedsFullComposite |= bWasFirstFrame; bNeedsFullComposite |= pFrameInfo->useFSRLayer0; bNeedsFullComposite |= pFrameInfo->useNISLayer0; bNeedsFullComposite |= pFrameInfo->blurLayer0; bNeedsFullComposite |= bNeedsCompositeFromFilter; bNeedsFullComposite |= !k_bUseCursorPlane && bDrewCursor; bNeedsFullComposite |= g_bColorSliderInUse; bNeedsFullComposite |= pFrameInfo->bFadingOut; bNeedsFullComposite |= !g_reshade_effect.empty(); if ( g_bOutputHDREnabled ) { bNeedsFullComposite |= g_bHDRItmEnable; if ( !SupportsColorManagement() ) bNeedsFullComposite |= ( pFrameInfo->layerCount > 1 || pFrameInfo->layers[0].colorspace != GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ ); } else { if ( !SupportsColorManagement() ) bNeedsFullComposite |= ColorspaceIsHDR( pFrameInfo->layers[0].colorspace ); } bNeedsFullComposite |= !!(g_uCompositeDebug & CompositeDebugFlag::Heatmap); bool bDoComposite = true; if ( !bNeedsFullComposite && !bWantsPartialComposite ) { int ret = drm_prepare( &g_DRM, bAsync, pFrameInfo ); if ( ret == 0 ) bDoComposite = false; else if ( ret == -EACCES ) return 0; } // Update to let the vblank manager know we are currently compositing. GetVBlankTimer().UpdateWasCompositing( bDoComposite ); if ( !bDoComposite ) { // Scanout + Planes Path m_bWasPartialCompsiting = false; m_bWasCompositing = false; if ( pFrameInfo->layerCount == 2 ) m_nLastSingleOverlayZPos = pFrameInfo->layers[1].zpos; return Commit( pFrameInfo ); } // Composition Path if ( kDisablePartialComposition ) bNeedsFullComposite = true; FrameInfo_t compositeFrameInfo = *pFrameInfo; if ( compositeFrameInfo.layerCount == 1 ) { // If we failed to flip a single plane then // we definitely need to composite for some reason... bNeedsFullComposite = true; } if ( !bNeedsFullComposite ) { // If we want to partial composite, fallback to full // composite if we have mismatching colorspaces in our overlays. // This is 2, and we do i-1 so 1...layerCount. So AFTER we have removed baseplane. // Overlays only. // // Josh: // We could handle mismatching colorspaces for partial composition // but I want to keep overlay -> partial composition promotion as simple // as possible, using the same 3D + SHAPER LUTs + BLEND in DRM // as changing them is incredibly expensive!! It takes forever. // We can't just point it to random BDA or whatever, it has to be uploaded slowly // thru registers which is SUPER SLOW. // This avoids stutter. for ( int i = 2; i < compositeFrameInfo.layerCount; i++ ) { if ( pFrameInfo->layers[i - 1].colorspace != pFrameInfo->layers[i].colorspace ) { bNeedsFullComposite = true; break; } } } // If we ever promoted from partial -> full, for the first frame // do NOT defer this partial composition. // We were already stalling for the full composition before, so it's not an issue // for latency, we just need to make sure we get 1 partial frame that isn't deferred // in time so we don't lose layers. bool bDefer = !bNeedsFullComposite && ( !m_bWasCompositing || m_bWasPartialCompsiting ); // If doing a partial composition then remove the baseplane // from our frameinfo to composite. if ( !bNeedsFullComposite ) { for ( int i = 1; i < compositeFrameInfo.layerCount; i++ ) compositeFrameInfo.layers[i - 1] = compositeFrameInfo.layers[i]; compositeFrameInfo.layerCount -= 1; // When doing partial composition, apply the shaper + 3D LUT stuff // at scanout. for ( uint32_t nEOTF = 0; nEOTF < EOTF_Count; nEOTF++ ) { compositeFrameInfo.shaperLut[ nEOTF ] = nullptr; compositeFrameInfo.lut3D[ nEOTF ] = nullptr; } } // If using composite debug markers, make sure we mark them as partial // so we know! if ( bDefer && !!( g_uCompositeDebug & CompositeDebugFlag::Markers ) ) g_uCompositeDebug |= CompositeDebugFlag::Markers_Partial; std::optional oCompositeResult = vulkan_composite( &compositeFrameInfo, nullptr, !bNeedsFullComposite ); m_bWasCompositing = true; g_uCompositeDebug &= ~CompositeDebugFlag::Markers_Partial; if ( !oCompositeResult ) { xwm_log.errorf("vulkan_composite failed"); return -EINVAL; } vulkan_wait( *oCompositeResult, true ); FrameInfo_t presentCompFrameInfo = {}; presentCompFrameInfo.allowVRR = pFrameInfo->allowVRR; presentCompFrameInfo.outputEncodingEOTF = pFrameInfo->outputEncodingEOTF; if ( bNeedsFullComposite ) { presentCompFrameInfo.applyOutputColorMgmt = false; presentCompFrameInfo.layerCount = 1; FrameInfo_t::Layer_t *baseLayer = &presentCompFrameInfo.layers[ 0 ]; baseLayer->scale.x = 1.0; baseLayer->scale.y = 1.0; baseLayer->opacity = 1.0; baseLayer->zpos = g_zposBase; baseLayer->tex = vulkan_get_last_output_image( false, false ); baseLayer->applyColorMgmt = false; baseLayer->filter = GamescopeUpscaleFilter::NEAREST; baseLayer->ctm = nullptr; baseLayer->colorspace = pFrameInfo->outputEncodingEOTF == EOTF_PQ ? GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ : GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB; m_bWasPartialCompsiting = false; } else { if ( m_bWasPartialCompsiting || !bDefer ) { presentCompFrameInfo.applyOutputColorMgmt = g_ColorMgmt.pending.enabled; presentCompFrameInfo.layerCount = 2; presentCompFrameInfo.layers[ 0 ] = pFrameInfo->layers[ 0 ]; presentCompFrameInfo.layers[ 0 ].zpos = g_zposBase; FrameInfo_t::Layer_t *overlayLayer = &presentCompFrameInfo.layers[ 1 ]; overlayLayer->scale.x = 1.0; overlayLayer->scale.y = 1.0; overlayLayer->opacity = 1.0; overlayLayer->zpos = g_zposOverlay; overlayLayer->tex = vulkan_get_last_output_image( true, bDefer ); overlayLayer->applyColorMgmt = g_ColorMgmt.pending.enabled; overlayLayer->filter = GamescopeUpscaleFilter::NEAREST; // Partial composition stuff has the same colorspace. // So read that from the composite frame info overlayLayer->ctm = nullptr; overlayLayer->colorspace = compositeFrameInfo.layers[0].colorspace; } else { // Use whatever overlay we had last while waiting for the // partial composition to have anything queued. presentCompFrameInfo.applyOutputColorMgmt = g_ColorMgmt.pending.enabled; presentCompFrameInfo.layerCount = 1; presentCompFrameInfo.layers[ 0 ] = pFrameInfo->layers[ 0 ]; presentCompFrameInfo.layers[ 0 ].zpos = g_zposBase; const FrameInfo_t::Layer_t *lastPresentedOverlayLayer = nullptr; for (int i = 0; i < pFrameInfo->layerCount; i++) { if ( pFrameInfo->layers[i].zpos == m_nLastSingleOverlayZPos ) { lastPresentedOverlayLayer = &pFrameInfo->layers[i]; break; } } if ( lastPresentedOverlayLayer ) { FrameInfo_t::Layer_t *overlayLayer = &presentCompFrameInfo.layers[ 1 ]; *overlayLayer = *lastPresentedOverlayLayer; overlayLayer->zpos = g_zposOverlay; presentCompFrameInfo.layerCount = 2; } } m_bWasPartialCompsiting = true; } int ret = drm_prepare( &g_DRM, bAsync, &presentCompFrameInfo ); // Happens when we're VT-switched away if ( ret == -EACCES ) return 0; if ( ret != 0 ) { if ( g_DRM.current.mode_id == 0 ) { xwm_log.errorf("We failed our modeset and have no mode to fall back to! (Initial modeset failed?): %s", strerror(-ret)); return 0; } xwm_log.errorf("Failed to prepare 1-layer flip (%s), trying again with previous mode if modeset needed", strerror( -ret )); // Try once again to in case we need to fall back to another mode. ret = drm_prepare( &g_DRM, bAsync, &compositeFrameInfo ); // Happens when we're VT-switched away if ( ret == -EACCES ) return 0; if ( ret != 0 ) { xwm_log.errorf("Failed to prepare 1-layer flip entirely: %s", strerror( -ret )); // We should always handle a 1-layer flip, this used to abort, // but lets be more friendly and just avoid a commit and try again later. // Let's re-poll our state, and force grab the best connector again. // // Some intense connector hotplugging could be occuring and the // connector could become destroyed before we had a chance to use it // as we hadn't reffed it in a commit yet. this->DirtyState( true, false ); this->PollState(); return ret; } } return Commit( &compositeFrameInfo ); } virtual void DirtyState( bool bForce, bool bForceModeset ) override { if ( bForceModeset ) g_DRM.needs_modeset = true; g_DRM.out_of_date = std::max( g_DRM.out_of_date, bForce ? 2 : 1 ); g_DRM.paused = !wlsession_active(); } virtual bool PollState() override { return drm_poll_state( &g_DRM ); } virtual std::shared_ptr CreateBackendBlob( const std::type_info &type, std::span data ) override { uint32_t uBlob = 0; if ( type == typeid( glm::mat3x4 ) ) { assert( data.size() == sizeof( glm::mat3x4 ) ); drm_color_ctm2 ctm2; const float *pData = reinterpret_cast( data.data() ); for ( uint32_t i = 0; i < 12; i++ ) ctm2.matrix[i] = drm_calc_s31_32( pData[i] ); if ( drmModeCreatePropertyBlob( g_DRM.fd, reinterpret_cast( &ctm2 ), sizeof( ctm2 ), &uBlob ) != 0 ) return nullptr; } else { if ( drmModeCreatePropertyBlob( g_DRM.fd, data.data(), data.size(), &uBlob ) != 0 ) return nullptr; } return std::make_shared( data, uBlob, true ); } virtual OwningRc ImportDmabufToBackend( wlr_buffer *pBuffer, wlr_dmabuf_attributes *pDmaBuf ) override { return drm_fbid_from_dmabuf( &g_DRM, pBuffer, pDmaBuf ); } virtual bool UsesModifiers() const override { return g_DRM.allow_modifiers; } virtual std::span GetSupportedModifiers( uint32_t uDrmFormat ) const override { const wlr_drm_format *pFormat = wlr_drm_format_set_get( &g_DRM.formats, uDrmFormat ); if ( !pFormat ) return std::span{}; return std::span{ pFormat->modifiers, pFormat->modifiers + pFormat->len }; } virtual IBackendConnector *GetCurrentConnector() override { return g_DRM.pConnector; } virtual IBackendConnector *GetConnector( GamescopeScreenType eScreenType ) override { if ( GetCurrentConnector() && GetCurrentConnector()->GetScreenType() == eScreenType ) return GetCurrentConnector(); if ( eScreenType == GAMESCOPE_SCREEN_TYPE_INTERNAL ) { for ( auto &iter : g_DRM.connectors ) { gamescope::CDRMConnector *pConnector = &iter.second; if ( pConnector->GetScreenType() == GAMESCOPE_SCREEN_TYPE_INTERNAL ) return pConnector; } } return nullptr; } virtual bool SupportsPlaneHardwareCursor() const override { return true; } virtual bool SupportsTearing() const override { return g_bSupportsAsyncFlips; } virtual bool UsesVulkanSwapchain() const override { return false; } virtual bool IsSessionBased() const override { return true; } virtual bool SupportsExplicitSync() const override { #if __linux__ auto [nMajor, nMinor, nPatch] = GetKernelVersion(); // Only expose support on 6.8+ for eventfd fixes. if ( nMajor < 6 ) return false; if ( nMajor == 6 && nMinor < 8 ) return false; #else // I don't know about this for FreeBSD, etc. return false; #endif return g_bSupportsSyncObjs && !cv_drm_debug_disable_explicit_sync; } virtual bool IsPaused() const override { return g_DRM.paused; } virtual bool IsVisible() const override { return !this->IsPaused(); } virtual glm::uvec2 CursorSurfaceSize( glm::uvec2 uvecSize ) const override { if ( !k_bUseCursorPlane ) return uvecSize; return glm::uvec2{ g_DRM.cursor_width, g_DRM.cursor_height }; } virtual bool HackTemporarySetDynamicRefresh( int nRefresh ) override { return drm_set_refresh( &g_DRM, nRefresh ); } virtual void HackUpdatePatchedEdid() override { if ( !GetCurrentConnector() ) return; WritePatchedEdid( GetCurrentConnector()->GetRawEDID(), GetCurrentConnector()->GetHDRInfo(), g_bRotated ); } protected: virtual void OnBackendBlobDestroyed( BackendBlob *pBlob ) override { if ( pBlob->GetBlobValue() ) drmModeDestroyPropertyBlob( g_DRM.fd, pBlob->GetBlobValue() ); } private: bool m_bWasCompositing = false; bool m_bWasPartialCompsiting = false; int m_nLastSingleOverlayZPos = 0; uint32_t m_uNextPresentCtx = 0; DRMPresentCtx m_PresentCtxs[3]; bool SupportsColorManagement() const { return drm_supports_color_mgmt( &g_DRM ); } int Commit( const FrameInfo_t *pFrameInfo ) { drm_t *drm = &g_DRM; int ret = 0; assert( drm->req != nullptr ); defer( if ( drm->req != nullptr ) { drmModeAtomicFree( drm->req ); drm->req = nullptr; } ); bool isPageFlip = drm->flags & DRM_MODE_PAGE_FLIP_EVENT; uint32_t uNewPendingFlipCount = 0; if ( isPageFlip ) { uNewPendingFlipCount = ++drm->uPendingFlipCount; // Do it before the commit, as otherwise the pageflip handler could // potentially beat us to the refcount checks. // Swap over request FDs -> Queue std::unique_lock lock( drm->m_QueuedFbIdsMutex ); drm->m_QueuedFbIds.swap( drm->m_FbIdsInRequest ); } GetCurrentConnector()->PresentationFeedback().m_uQueuedPresents++; uint32_t uCurrentPresentCtx = m_uNextPresentCtx; m_uNextPresentCtx = ( m_uNextPresentCtx + 1 ) % 3; m_PresentCtxs[uCurrentPresentCtx].ulPendingFlipCount = GetCurrentConnector()->PresentationFeedback().m_uQueuedPresents; drm_log.debugf("flip commit %" PRIu64, (uint64_t)GetCurrentConnector()->PresentationFeedback().m_uQueuedPresents); gpuvis_trace_printf( "flip commit %" PRIu64, (uint64_t)GetCurrentConnector()->PresentationFeedback().m_uQueuedPresents ); ret = drmModeAtomicCommit(drm->fd, drm->req, drm->flags, &m_PresentCtxs[uCurrentPresentCtx] ); if ( ret != 0 ) { drm_log.errorf_errno( "flip error" ); if ( ret != -EBUSY && ret != -EACCES ) { drm_log.errorf( "fatal flip error, aborting" ); if ( isPageFlip ) drm->uPendingFlipCount--; abort(); } drm_rollback( drm ); // Swap back over to what was previously queued (probably nothing) // if this commit failed. { std::unique_lock lock( drm->m_QueuedFbIdsMutex ); drm->m_QueuedFbIds.swap( drm->m_FbIdsInRequest ); } // Clear our refs. drm->m_FbIdsInRequest.clear(); GetCurrentConnector()->PresentationFeedback().m_uQueuedPresents--; if ( isPageFlip ) drm->uPendingFlipCount--; return ret; } else { // Our request went through! // Clear what we swapped with (what was previously queued) drm->m_FbIdsInRequest.clear(); drm->current = drm->pending; for ( std::unique_ptr< gamescope::CDRMCRTC > &pCRTC : drm->crtcs ) { for ( std::optional &oProperty : pCRTC->GetProperties() ) { if ( oProperty ) oProperty->OnCommit(); } } for ( std::unique_ptr< gamescope::CDRMPlane > &pPlane : drm->planes ) { for ( std::optional &oProperty : pPlane->GetProperties() ) { if ( oProperty ) oProperty->OnCommit(); } } for ( auto &iter : drm->connectors ) { gamescope::CDRMConnector *pConnector = &iter.second; for ( std::optional &oProperty : pConnector->GetProperties() ) { if ( oProperty ) oProperty->OnCommit(); } } } // Update the draw time // Ideally this would be updated by something right before the page flip // is queued and would end up being the new page flip, rather than here. // However, the page flip handler is called when the page flip occurs, // not when it is successfully queued. GetVBlankTimer().UpdateLastDrawTime( get_time_in_nanos() - g_SteamCompMgrVBlankTime.ulWakeupTime ); if ( isPageFlip ) { // Wait for bPendingFlip to change from true -> false. drm->uPendingFlipCount.wait( uNewPendingFlipCount ); assert( drm->uPendingFlipCount == 0 ); } return ret; } }; ///////////////////////// // Backend Instantiator ///////////////////////// template <> bool IBackend::Set() { return Set( new CDRMBackend{} ); } } int HackyDRMPresent( const FrameInfo_t *pFrameInfo, bool bAsync ) { return static_cast( GetBackend() )->Present( pFrameInfo, bAsync ); } ValveSoftware-gamescope-eb620ab/src/Backends/HeadlessBackend.cpp000066400000000000000000000143701502457270500247470ustar00rootroot00000000000000#include "backend.h" #include "rendervulkan.hpp" #include "wlserver.hpp" #include "refresh_rate.h" extern int g_nPreferredOutputWidth; extern int g_nPreferredOutputHeight; namespace gamescope { class CHeadlessConnector final : public CBaseBackendConnector { public: CHeadlessConnector() { } virtual ~CHeadlessConnector() { } virtual gamescope::GamescopeScreenType GetScreenType() const override { return GAMESCOPE_SCREEN_TYPE_INTERNAL; } virtual GamescopePanelOrientation GetCurrentOrientation() const override { return GAMESCOPE_PANEL_ORIENTATION_0; } virtual bool SupportsHDR() const override { return false; } virtual bool IsHDRActive() const override { return false; } virtual const BackendConnectorHDRInfo &GetHDRInfo() const override { return m_HDRInfo; } virtual bool IsVRRActive() const override { return false; } virtual std::span GetModes() const override { return std::span{}; } virtual bool SupportsVRR() const override { return false; } virtual std::span GetRawEDID() const override { return std::span{}; } virtual std::span GetValidDynamicRefreshRates() const override { return std::span{}; } virtual void GetNativeColorimetry( bool bHDR10, displaycolorimetry_t *displayColorimetry, EOTF *displayEOTF, displaycolorimetry_t *outputEncodingColorimetry, EOTF *outputEncodingEOTF ) const override { *displayColorimetry = displaycolorimetry_709; *displayEOTF = EOTF_Gamma22; *outputEncodingColorimetry = displaycolorimetry_709; *outputEncodingEOTF = EOTF_Gamma22; } virtual const char *GetName() const override { return "Headless"; } virtual const char *GetMake() const override { return "Gamescope"; } virtual const char *GetModel() const override { return "Virtual Display"; } virtual int Present( const FrameInfo_t *pFrameInfo, bool bAsync ) override { return 0; } private: BackendConnectorHDRInfo m_HDRInfo{}; }; class CHeadlessBackend final : public CBaseBackend { public: CHeadlessBackend() { } virtual ~CHeadlessBackend() { } virtual bool Init() override { g_nOutputWidth = g_nPreferredOutputWidth; g_nOutputHeight = g_nPreferredOutputHeight; g_nOutputRefresh = g_nNestedRefresh; if ( g_nOutputHeight == 0 ) { if ( g_nOutputWidth != 0 ) { fprintf( stderr, "Cannot specify -W without -H\n" ); return false; } g_nOutputHeight = 720; } if ( g_nOutputWidth == 0 ) g_nOutputWidth = g_nOutputHeight * 16 / 9; if ( g_nOutputRefresh == 0 ) g_nOutputRefresh = ConvertHztomHz( 60 ); if ( !vulkan_init( vulkan_get_instance(), VK_NULL_HANDLE ) ) { return false; } if ( !wlsession_init() ) { fprintf( stderr, "Failed to initialize Wayland session\n" ); return false; } return true; } virtual bool PostInit() override { return true; } virtual std::span GetInstanceExtensions() const override { return std::span{}; } virtual std::span GetDeviceExtensions( VkPhysicalDevice pVkPhysicalDevice ) const override { return std::span{}; } virtual VkImageLayout GetPresentLayout() const override { return VK_IMAGE_LAYOUT_GENERAL; } virtual void GetPreferredOutputFormat( uint32_t *pPrimaryPlaneFormat, uint32_t *pOverlayPlaneFormat ) const override { *pPrimaryPlaneFormat = VulkanFormatToDRM( VK_FORMAT_A2B10G10R10_UNORM_PACK32 ); *pOverlayPlaneFormat = VulkanFormatToDRM( VK_FORMAT_B8G8R8A8_UNORM ); } virtual bool ValidPhysicalDevice( VkPhysicalDevice pVkPhysicalDevice ) const override { return true; } virtual void DirtyState( bool bForce, bool bForceModeset ) override { } virtual bool PollState() override { return false; } virtual std::shared_ptr CreateBackendBlob( const std::type_info &type, std::span data ) override { return std::make_shared( data ); } virtual OwningRc ImportDmabufToBackend( wlr_buffer *pBuffer, wlr_dmabuf_attributes *pDmaBuf ) override { return new CBaseBackendFb(); } virtual bool UsesModifiers() const override { return false; } virtual std::span GetSupportedModifiers( uint32_t uDrmFormat ) const override { return std::span{}; } virtual IBackendConnector *GetCurrentConnector() override { return &m_Connector; } virtual IBackendConnector *GetConnector( GamescopeScreenType eScreenType ) override { if ( eScreenType == GAMESCOPE_SCREEN_TYPE_INTERNAL ) return &m_Connector; return nullptr; } virtual bool SupportsPlaneHardwareCursor() const override { return false; } virtual bool SupportsTearing() const override { return false; } virtual bool UsesVulkanSwapchain() const override { return false; } virtual bool IsSessionBased() const override { return false; } virtual bool SupportsExplicitSync() const override { return true; } virtual bool IsPaused() const override { return false; } virtual bool IsVisible() const override { return true; } virtual glm::uvec2 CursorSurfaceSize( glm::uvec2 uvecSize ) const override { return uvecSize; } virtual bool HackTemporarySetDynamicRefresh( int nRefresh ) override { return false; } virtual void HackUpdatePatchedEdid() override { } protected: virtual void OnBackendBlobDestroyed( BackendBlob *pBlob ) override { } private: CHeadlessConnector m_Connector; }; ///////////////////////// // Backend Instantiator ///////////////////////// template <> bool IBackend::Set() { return Set( new CHeadlessBackend{} ); } }ValveSoftware-gamescope-eb620ab/src/Backends/OpenVRBackend.cpp000066400000000000000000002137251502457270500243750ustar00rootroot00000000000000#include #include #define VK_NO_PROTOTYPES #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wnon-virtual-dtor" #include #pragma GCC diagnostic pop #include "backend.h" #include "main.hpp" #include "openvr.h" #include "steamcompmgr.hpp" #include "wlserver.hpp" #include "log.hpp" #include "ime.hpp" #include "refresh_rate.h" #include "edid.h" #include "Ratio.h" #include "LibInputHandler.h" #include #include #include #include struct wlserver_input_method; extern bool steamMode; extern int g_argc; extern char **g_argv; extern int g_nPreferredOutputWidth; extern int g_nPreferredOutputHeight; extern int g_nPreferredOutputWidth; extern int g_nPreferredOutputHeight; extern bool g_bForceHDR10OutputDebug; extern bool g_bBorderlessOutputWindow; extern gamescope::ConVar cv_composite_force; extern bool g_bColorSliderInUse; extern bool fadingOut; extern std::string g_reshade_effect; extern gamescope::ConVar cv_hdr_enabled; extern uint64_t g_SteamCompMgrLimitedAppRefreshCycle; void MakeFocusDirty(); void update_connector_display_info_wl(struct drm_t *drm); static LogScope openvr_log("openvr"); static bool GetVulkanInstanceExtensionsRequired( std::vector< std::string > &outInstanceExtensionList ); static bool GetVulkanDeviceExtensionsRequired( VkPhysicalDevice pPhysicalDevice, std::vector< std::string > &outDeviceExtensionList ); gamescope::ConVar cv_vr_always_warp_cursor( "vr_always_warp_cursor", true, "Whether or not we should always warp the cursor, even if it is invisible so we get hover events." ); gamescope::ConVar cv_vr_use_modifiers( "vr_use_modifiers", true, "Use DMA-BUF modifiers?" ); gamescope::ConVar cv_vr_transparent_backing( "vr_transparent_backing", true, "Should backing be transparent or not?" ); gamescope::ConVar cv_vr_use_window_icons( "vr_use_window_icons", true, "Should we use window icons if they are available?" ); gamescope::ConVar cv_vr_trackpad_hide_laser( "vr_trackpad_hide_laser", false, "Hide laser mouse when we are in trackpad mode." ); gamescope::ConVar cv_vr_trackpad_relative_mouse_mode( "vr_trackpad_relative_mouse_mode", true, "If we are in relative mouse mode, treat the screen like a big trackpad?" ); gamescope::ConVar cv_vr_trackpad_sensitivity( "vr_trackpad_sensitivity", 1500.f, "Sensitivity for VR Trackpad Mode" ); gamescope::ConVar cv_vr_trackpad_click_time( "vr_trackpad_click_time", 250'000'000ul, "Time to consider a 'click' vs a 'drag' when using trackpad mode. In nanoseconds." ); gamescope::ConVar cv_vr_trackpad_click_max_delta( "vr_trackpad_click_max_delta", 0.14f, "Max amount the cursor can move before not clicking." ); gamescope::ConVar cv_vr_debug_force_opaque( "vr_debug_force_opaque", false, "Force textures to be treated as opaque." ); gamescope::ConVar cv_vr_nudge_to_visible_per_connector( "vr_nudge_to_visible_per_connector", false, "" ); // Maximum interval between polling for VR events (normally paced by frame sync) gamescope::ConVar cv_vr_poll_rate( "vr_poll_rate", 50ul, "Max time between input polls. In milliseconds." ); // Not in public headers yet. namespace vr { const EVRButtonId k_EButton_Steam = (EVRButtonId)(50); const EVRButtonId k_EButton_QAM = (EVRButtonId)(51); } uint32_t get_appid_from_pid( pid_t pid ); /////////////////////////////////////////////// // Josh: // GetVulkanInstanceExtensionsRequired and GetVulkanDeviceExtensionsRequired return *space separated* exts :( // I am too lazy to write that myself. // This is stolen verbatim from hellovr_vulkan with the .clear removed. // If it is broken, blame the samples. static bool GetVulkanInstanceExtensionsRequired( std::vector< std::string > &outInstanceExtensionList ) { if ( !vr::VRCompositor() ) { openvr_log.errorf( "GetVulkanInstanceExtensionsRequired: Failed to get VRCompositor" ); return false; } uint32_t nBufferSize = vr::VRCompositor()->GetVulkanInstanceExtensionsRequired( nullptr, 0 ); if ( nBufferSize > 0 ) { // Allocate memory for the space separated list and query for it char *pExtensionStr = new char[ nBufferSize ]; pExtensionStr[0] = 0; vr::VRCompositor()->GetVulkanInstanceExtensionsRequired( pExtensionStr, nBufferSize ); // Break up the space separated list into entries on the CUtlStringList std::string curExtStr; uint32_t nIndex = 0; while ( pExtensionStr[ nIndex ] != 0 && ( nIndex < nBufferSize ) ) { if ( pExtensionStr[ nIndex ] == ' ' ) { outInstanceExtensionList.push_back( curExtStr ); curExtStr.clear(); } else { curExtStr += pExtensionStr[ nIndex ]; } nIndex++; } if ( curExtStr.size() > 0 ) { outInstanceExtensionList.push_back( curExtStr ); } delete [] pExtensionStr; } return true; } static bool GetVulkanDeviceExtensionsRequired( VkPhysicalDevice pPhysicalDevice, std::vector< std::string > &outDeviceExtensionList ) { if ( !vr::VRCompositor() ) { openvr_log.errorf( "GetVulkanDeviceExtensionsRequired: Failed to get VRCompositor" ); return false; } uint32_t nBufferSize = vr::VRCompositor()->GetVulkanDeviceExtensionsRequired( ( VkPhysicalDevice_T * ) pPhysicalDevice, nullptr, 0 ); if ( nBufferSize > 0 ) { // Allocate memory for the space separated list and query for it char *pExtensionStr = new char[ nBufferSize ]; pExtensionStr[0] = 0; vr::VRCompositor()->GetVulkanDeviceExtensionsRequired( ( VkPhysicalDevice_T * ) pPhysicalDevice, pExtensionStr, nBufferSize ); // Break up the space separated list into entries on the CUtlStringList std::string curExtStr; uint32_t nIndex = 0; while ( pExtensionStr[ nIndex ] != 0 && ( nIndex < nBufferSize ) ) { if ( pExtensionStr[ nIndex ] == ' ' ) { outDeviceExtensionList.push_back( curExtStr ); curExtStr.clear(); } else { curExtStr += pExtensionStr[ nIndex ]; } nIndex++; } if ( curExtStr.size() > 0 ) { outDeviceExtensionList.push_back( curExtStr ); } delete [] pExtensionStr; } return true; } namespace gamescope { class COpenVRBackend; class COpenVRPlane; class COpenVRFb; class COpenVRConnector; class COpenVRFb final : public CBaseBackendFb { public: COpenVRFb( COpenVRBackend *pBackend, vr::SharedTextureHandle_t ulHandle ); ~COpenVRFb(); vr::SharedTextureHandle_t GetSharedTextureHandle() const { return m_ulHandle; } private: COpenVRBackend *m_pBackend = nullptr; vr::SharedTextureHandle_t m_ulHandle = 0; }; // TODO: Merge with WaylandPlaneState struct OpenVRPlaneState { CVulkanTexture *pTexture; int32_t nDestX; int32_t nDestY; double flSrcX; double flSrcY; double flSrcWidth; double flSrcHeight; int32_t nDstWidth; int32_t nDstHeight; GamescopeAppTextureColorspace eColorspace; bool bOpaque; float flAlpha = 1.0f; }; class COpenVRPlane { public: COpenVRPlane( COpenVRConnector *pConnector ); ~COpenVRPlane(); bool Init( COpenVRPlane *pParent, COpenVRPlane *pSiblingBelow ); void Present( std::optional oState ); void Present( const FrameInfo_t::Layer_t *pLayer ); vr::VROverlayHandle_t GetOverlay() const { return m_hOverlay; } vr::VROverlayHandle_t GetOverlayThumbnail() const { return m_hOverlayThumbnail; } uint32_t GetSortOrder() const { return m_uSortOrder; } bool IsSubview() const { return m_bIsSubview; } COpenVRBackend *GetBackend() const { return m_pBackend; } COpenVRConnector *GetConnector() const { return m_pConnector; } void OnPageFlip(); bool operator==( const vr::VROverlayHandle_t& hOverlay ) const { return this->m_hOverlay == hOverlay; } private: COpenVRConnector *m_pConnector = nullptr; COpenVRBackend *m_pBackend = nullptr; std::string m_sDashboardOverlayKey; bool m_bIsSubview = false; uint32_t m_uSortOrder = 0; vr::VROverlayHandle_t m_hOverlay = vr::k_ulOverlayHandleInvalid; vr::VROverlayHandle_t m_hOverlayThumbnail = vr::k_ulOverlayHandleInvalid; std::mutex m_mutFbIds; Rc m_pQueuedFbId; Rc m_pVisibleFbId; }; class COpenVRConnector final : public CBaseBackendConnector, public INestedHints { public: COpenVRConnector( COpenVRBackend *pBackend, uint64_t ulVirtualConnectorKey ); ////////////////////// // IBackendConnector ////////////////////// ~COpenVRConnector(); virtual GamescopeScreenType GetScreenType() const override; virtual GamescopePanelOrientation GetCurrentOrientation() const override; virtual bool SupportsHDR() const override; virtual bool IsHDRActive() const override; virtual const BackendConnectorHDRInfo &GetHDRInfo() const override; virtual bool IsVRRActive() const override; virtual std::span GetModes() const override; virtual bool SupportsVRR() const override; virtual std::span GetRawEDID() const override; virtual std::span GetValidDynamicRefreshRates() const override; virtual void GetNativeColorimetry( bool bHDR10, displaycolorimetry_t *displayColorimetry, EOTF *displayEOTF, displaycolorimetry_t *outputEncodingColorimetry, EOTF *outputEncodingEOTF ) const override; virtual const char *GetName() const override; virtual const char *GetMake() const override; virtual const char *GetModel() const override; virtual int Present( const FrameInfo_t *pFrameInfo, bool bAsync ) override; virtual INestedHints *GetNestedHints() override { return this; } /////////////////// // INestedHints /////////////////// virtual void SetCursorImage( std::shared_ptr info ) override; virtual void SetRelativeMouseMode( bool bRelative ) override; virtual void SetVisible( bool bVisible ) override; virtual void SetTitle( std::shared_ptr szTitle ) override; virtual void SetIcon( std::shared_ptr> uIconPixels ) override; virtual void SetSelection( std::shared_ptr szContents, GamescopeSelection eSelection ) override; bool UpdateEdid(); bool Init(); COpenVRBackend *GetBackend() const { return m_pBackend; } COpenVRPlane *GetPrimaryPlane() { return &m_Planes[0]; } std::span GetPlanes() { return std::span( &m_Planes[0], std::size( m_Planes ) ); } COpenVRPlane *GetPlaneByOverlayHandle( vr::VROverlayHandle_t hOverlay ) { auto iter = std::find( std::begin( m_Planes ), std::end( m_Planes ), hOverlay ); if ( iter == std::end( m_Planes ) ) return nullptr; else return &(*iter); } bool ConsumeNudgeToVisible() { return std::exchange( m_bNudgeToVisible, false ); } bool IsRelativeMouse() const { return m_bRelativeMouse; } // Thread safe. bool IsVisible() const { return m_bOverlayShown || m_bSceneAppVisible; } // Only called from event thread void MarkOverlayShown( bool bShown ) { m_bOverlayShown = bShown; UpdateVisibility( "Overlay Visibility" ); } // Only called from event thread void MarkSceneAppShown( bool bShown ) { m_bSceneAppVisible = bShown; UpdateVisibility( "Scene App Visibility" ); } void UpdateVisibility( const char *pszReason ); // XXX std::atomic m_bUsingVRMouse = { true }; bool m_bCurrentlyOverridingPosition = false; private: COpenVRBackend *m_pBackend = nullptr; COpenVRPlane m_Planes[8]; BackendConnectorHDRInfo m_HDRInfo{}; std::vector m_FakeEdid; bool m_bNudgeToVisible = false; std::atomic m_bRelativeMouse = false; bool m_bWasVisible = false; // Event thread only std::atomic m_bOverlayShown = { false }; std::atomic m_bSceneAppVisible = { false }; }; class COpenVRBackend final : public CBaseBackend { public: COpenVRBackend() : m_LibInputWaiter{ "gamescope-libinput" } , m_FlipHandlerThread{ [this](){ this->FlipHandlerThread(); } } { } virtual ~COpenVRBackend() { m_bRunning = false; m_bInitted = true; m_bInitted.notify_all(); m_FlipHandlerThread.join(); } void FlipHandlerThread() { pthread_setname_np( pthread_self(), "gamescope-vrflip" ); m_bInitted.wait( false ); while ( m_bRunning ) { if ( vr::VROverlay()->WaitFrameSync( ~0u ) != vr::VROverlayError_None ) openvr_log.errorf( "WaitFrameSync failed!" ); static constexpr uint64_t k_ulSchedulingFudge = 100'000; // 0.1ms uint64_t ulNow = get_time_in_nanos() - k_ulSchedulingFudge; GetVBlankTimer().MarkVBlank( ulNow, true ); // Nudge so that steamcompmgr releases commits. nudge_steamcompmgr(); // Flush out any pending commits -> visible // and any visible commits -> release. { std::scoped_lock lock{ m_mutActiveConnectors }; for ( COpenVRConnector *pConnector : m_pActiveConnectors ) { for ( COpenVRPlane &plane : pConnector->GetPlanes() ) { plane.OnPageFlip(); } } } ProcessVRInput(); } } ///////////// // IBackend ///////////// virtual bool Init() override { // Setup nested stuff. g_nOutputWidth = g_nPreferredOutputWidth; g_nOutputHeight = g_nPreferredOutputHeight; if ( g_nOutputHeight == 0 ) { if ( g_nOutputWidth != 0 ) { fprintf( stderr, "Cannot specify -W without -H\n" ); return false; } g_nOutputHeight = 720; } if ( g_nOutputWidth == 0 ) g_nOutputWidth = g_nOutputHeight * 16 / 9; vr::EVRInitError error = vr::VRInitError_None; VR_Init( &error, vr::VRApplication_Background ); if ( error != vr::VRInitError_None ) { openvr_log.errorf("Unable to init VR runtime: %s\n", vr::VR_GetVRInitErrorAsEnglishDescription( error )); return false; } if ( !vulkan_init( vulkan_get_instance(), VK_NULL_HANDLE ) ) { return false; } if ( !wlsession_init() ) { fprintf( stderr, "Failed to initialize Wayland session\n" ); return false; } // Reset getopt() state optind = 1; int o; int opt_index = -1; while ((o = getopt_long(g_argc, g_argv, gamescope_optstring, gamescope_options, &opt_index)) != -1) { const char *opt_name; switch (o) { case 0: // long options without a short option opt_name = gamescope_options[opt_index].name; if (strcmp(opt_name, "vr-overlay-key") == 0) { m_szOverlayKey = optarg; } else if (strcmp(opt_name, "vr-app-overlay-key") == 0) { m_szAppOverlayKey = optarg; } else if (strcmp(opt_name, "vr-overlay-explicit-name") == 0) { m_pchOverlayName = optarg; m_bExplicitOverlayName = true; } else if (strcmp(opt_name, "vr-overlay-default-name") == 0) { m_pchOverlayName = optarg; } else if (strcmp(opt_name, "vr-overlay-icon") == 0) { m_pchOverlayIcon = optarg; } else if (strcmp(opt_name, "vr-overlay-show-immediately") == 0) { m_bNudgeToVisible = true; } else if (strcmp(opt_name, "vr-overlay-enable-control-bar") == 0) { m_bEnableControlBar = true; } else if (strcmp(opt_name, "vr-overlay-enable-control-bar-keyboard") == 0) { m_bEnableControlBarKeyboard = true; } else if (strcmp(opt_name, "vr-overlay-enable-control-bar-close") == 0) { m_bEnableControlBarClose = true; } else if (strcmp(opt_name, "vr-overlay-enable-click-stabilization") == 0) { m_bEnableClickStabilization = true; } else if (strcmp(opt_name, "vr-overlay-modal") == 0) { m_bModal = true; } else if (strcmp(opt_name, "vr-overlay-physical-width") == 0) { m_flPhysicalWidth = atof( optarg ); if ( m_flPhysicalWidth <= 0.0f ) m_flPhysicalWidth = 2.0f; } else if (strcmp(opt_name, "vr-overlay-physical-curvature") == 0) { m_flPhysicalCurvature = atof( optarg ); } else if (strcmp(opt_name, "vr-overlay-physical-pre-curve-pitch") == 0) { m_flPhysicalPreCurvePitch = atof( optarg ); } else if (strcmp(opt_name, "vr-scroll-speed") == 0) { m_flScrollSpeed = atof( optarg ); } else if (strcmp(opt_name, "vr-session-manager") == 0) { openvr_log.infof( "Becoming the VR session manager." ); std::unique_ptr pLibInput = std::make_unique(); if ( pLibInput->Init() ) { m_pLibInput = std::move( pLibInput ); m_LibInputWaiter.AddWaitable( m_pLibInput.get() ); } else { openvr_log.errorf( "Could not start libinput for being the vr session manager" ); } } break; case '?': assert(false); // unreachable } } if ( !m_pchOverlayName ) m_pchOverlayName = "Gamescope"; m_pIPCResourceManager = vr::VRIPCResourceManager(); if ( m_pIPCResourceManager ) { uint32_t uFormatCount = 0; m_pIPCResourceManager->GetDmabufFormats( &uFormatCount, nullptr ); if ( uFormatCount ) { std::vector uFormats; uFormats.resize( uFormatCount ); m_pIPCResourceManager->GetDmabufFormats( &uFormatCount, uFormats.data() ); for ( uint32_t i = 0; i < uFormatCount; i++ ) { uint32_t uFormat = uFormats[i]; uint32_t uModifierCount = 0; m_pIPCResourceManager->GetDmabufModifiers( vr::VRApplication_Overlay, uFormat, &uModifierCount, nullptr ); if ( uModifierCount ) { std::vector ulModifiers; ulModifiers.resize( uModifierCount ); m_pIPCResourceManager->GetDmabufModifiers( vr::VRApplication_Overlay, uFormat, &uModifierCount, ulModifiers.data() ); for ( uint64_t ulModifier : ulModifiers ) { if ( ulModifier != DRM_FORMAT_MOD_INVALID ) m_FormatModifiers[uFormat].emplace_back( ulModifier ); } } } } } if ( UsesModifiers() ) { openvr_log.infof( "Using modifiers!" ); } if ( !vr::VROverlay() ) { openvr_log.errorf( "SteamVR runtime version mismatch!\n" ); return false; } // Setup misc. stuff g_nOutputRefresh = (int32_t) ConvertHztomHz( roundf( vr::VRSystem()->GetFloatTrackedDeviceProperty( vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_DisplayFrequency_Float ) ) ); m_bRunning = true; m_bInitted = true; m_bInitted.notify_all(); return true; } virtual bool PostInit() override { if ( m_szOverlayKey.empty() ) m_szOverlayKey = std::string( "gamescope." ) + wlserver_get_wl_display_name(); m_pIME = create_local_ime(); if ( !m_pIME ) return false; // This breaks cursor intersection right now. // Come back to me later. //Ratio aspectRatio{ g_nOutputWidth, g_nOutputHeight }; //m_pBlackTexture = vulkan_create_flat_texture( aspectRatio.Num(), aspectRatio.Denom(), 0, 0, 0, cv_vr_transparent_backing ? 0 : 255 ); m_pBlackTexture = vulkan_create_flat_texture( g_nOutputWidth, g_nOutputHeight, 0, 0, 0, cv_vr_transparent_backing ? 0 : 255 ); if ( !m_pBlackTexture ) { openvr_log.errorf( "Failed to create dummy black texture." ); return false; } return true; } virtual std::span GetInstanceExtensions() const override { static std::vector s_exts; GetVulkanInstanceExtensionsRequired( s_exts ); static std::vector s_extPtrs; for ( const std::string &ext : s_exts ) s_extPtrs.emplace_back( ext.c_str() ); return std::span{ s_extPtrs.begin(), s_extPtrs.end() }; } virtual std::span GetDeviceExtensions( VkPhysicalDevice pVkPhysicalDevice ) const override { static std::vector s_exts; GetVulkanDeviceExtensionsRequired( pVkPhysicalDevice, s_exts ); static std::vector s_extPtrs; for ( const std::string &ext : s_exts ) s_extPtrs.emplace_back( ext.c_str() ); return std::span{ s_extPtrs.begin(), s_extPtrs.end() }; } virtual VkImageLayout GetPresentLayout() const override { return VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; } virtual void GetPreferredOutputFormat( uint32_t *pPrimaryPlaneFormat, uint32_t *pOverlayPlaneFormat ) const override { *pPrimaryPlaneFormat = VulkanFormatToDRM( VK_FORMAT_A2B10G10R10_UNORM_PACK32 ); *pOverlayPlaneFormat = VulkanFormatToDRM( VK_FORMAT_B8G8R8A8_UNORM ); } virtual bool ValidPhysicalDevice( VkPhysicalDevice pVkPhysicalDevice ) const override { return true; } virtual void DirtyState( bool bForce, bool bForceModeset ) override { } virtual bool PollState() override { return false; } virtual std::shared_ptr CreateBackendBlob( const std::type_info &type, std::span data ) override { return std::make_shared( data ); } virtual OwningRc ImportDmabufToBackend( wlr_buffer *pBuffer, wlr_dmabuf_attributes *pDmaBuf ) override { if ( UsesModifiers() ) { vr::DmabufAttributes_t dmabufAttributes = { .unWidth = uint32_t( pDmaBuf->width ), .unHeight = uint32_t( pDmaBuf->height ), .unDepth = 1, .unMipLevels = 1, .unArrayLayers = 1, .unSampleCount = 1, .unFormat = pDmaBuf->format, .ulModifier = pDmaBuf->modifier, .unPlaneCount = uint32_t( pDmaBuf->n_planes ), .plane = { { .unOffset = pDmaBuf->offset[0], .unStride = pDmaBuf->stride[0], .nFd = pDmaBuf->fd[0], }, { .unOffset = pDmaBuf->offset[1], .unStride = pDmaBuf->stride[1], .nFd = pDmaBuf->fd[1], }, { .unOffset = pDmaBuf->offset[2], .unStride = pDmaBuf->stride[2], .nFd = pDmaBuf->fd[2], }, { .unOffset = pDmaBuf->offset[3], .unStride = pDmaBuf->stride[3], .nFd = pDmaBuf->fd[3], }, } }; vr::SharedTextureHandle_t ulSharedHandle = 0; if ( !m_pIPCResourceManager->ImportDmabuf( vr::VRApplication_Overlay, &dmabufAttributes, &ulSharedHandle ) ) return nullptr; assert( ulSharedHandle != 0 ); // Take the first reference! if ( !m_pIPCResourceManager->RefResource( ulSharedHandle, nullptr ) ) return nullptr; return new COpenVRFb{ this, ulSharedHandle }; } else { return new COpenVRFb{ this, 0 }; } } virtual bool UsesModifiers() const override { if ( !cv_vr_use_modifiers ) return false; if ( !m_pIPCResourceManager ) return false; return !m_FormatModifiers.empty(); } virtual std::span GetSupportedModifiers( uint32_t uDrmFormat ) const override { if ( !UsesModifiers() ) return std::span{}; auto iter = m_FormatModifiers.find( uDrmFormat ); if ( iter == m_FormatModifiers.end() ) return std::span{}; return std::span{ iter->second.begin(), iter->second.end() }; } virtual IBackendConnector *GetCurrentConnector() override { return m_pFocusConnector; } virtual IBackendConnector *GetConnector( GamescopeScreenType eScreenType ) override { if ( eScreenType == GAMESCOPE_SCREEN_TYPE_INTERNAL ) return GetCurrentConnector(); return nullptr; } virtual bool SupportsPlaneHardwareCursor() const override { return false; } virtual bool SupportsTearing() const override { return false; } virtual bool UsesVulkanSwapchain() const override { return false; } virtual bool IsSessionBased() const override { return false; } virtual bool SupportsExplicitSync() const override { // We only forward done DMA-BUFs, so this should be fine. // SteamVR does not do any wait/poll/sync on these. return true; } virtual bool IsPaused() const override { return false; } virtual bool IsVisible() const override { if ( ShouldNudgeToVisible() ) return true; return m_nOverlaysVisible.load() != 0; } virtual glm::uvec2 CursorSurfaceSize( glm::uvec2 uvecSize ) const override { return uvecSize; } virtual bool HackTemporarySetDynamicRefresh( int nRefresh ) override { return false; } virtual void HackUpdatePatchedEdid() override { if ( !GetCurrentConnector() ) return; // XXX: We should do this a better way that handles per-window and appid stuff // down the line if ( cv_hdr_enabled && GetCurrentConnector()->GetHDRInfo().bExposeHDRSupport ) { setenv( "DXVK_HDR", "1", true ); } else { setenv( "DXVK_HDR", "0", true ); } WritePatchedEdid( GetCurrentConnector()->GetRawEDID(), GetCurrentConnector()->GetHDRInfo(), false ); } virtual bool NeedsFrameSync() const override { return false; } virtual TouchClickMode GetTouchClickMode() override { COpenVRConnector *pConnector = static_cast( GetCurrentConnector() ); if ( cv_vr_trackpad_relative_mouse_mode && pConnector && pConnector->IsRelativeMouse() ) { return TouchClickModes::Trackpad; } if ( VirtualConnectorInSteamPerAppState() ) { if ( !VirtualConnectorKeyIsSteam( pConnector->GetVirtualConnectorKey() ) ) return TouchClickModes::Left; } return CBaseBackend::GetTouchClickMode(); } bool UsesVirtualConnectors() override { return true; } std::shared_ptr CreateVirtualConnector( uint64_t ulVirtualConnectorKey ) override { std::shared_ptr pConnector = std::make_shared( this, ulVirtualConnectorKey ); bool bSetCurrentConnector = false; { if ( !m_pFocusConnector ) { SetFocus( pConnector.get() ); bSetCurrentConnector = true; } } if ( !pConnector->Init() ) { if ( bSetCurrentConnector ) { SetFocus( nullptr ); } return nullptr; } std::scoped_lock lock{ m_mutActiveConnectors }; m_pActiveConnectors.push_back( pConnector.get() ); return pConnector; } void NotifyPhysicalInput( InputType eInputType ) override { if ( eInputType == InputType::Mouse ) { // TODO: Avoid this lock someday. // Can we make this a shared_mutex for r/w? std::scoped_lock lock{ m_mutActiveConnectors }; COpenVRConnector *pConnector = static_cast( GetCurrentConnector() ); if ( pConnector ) { pConnector->m_bUsingVRMouse = false; } } } vr::IVRIPCResourceManagerClient *GetIPCResourceManager() { return m_pIPCResourceManager; } bool SupportsColorManagement() const { return false; } const char *GetOverlayKey() const { return m_szOverlayKey.c_str(); } const char *GetAppOverlayKey() const { return m_szAppOverlayKey.c_str(); } const char *GetOverlayName() const { return m_pchOverlayName; } const char *GetOverlayIcon() const { return m_pchOverlayIcon; } bool ShouldEnableControlBar() const { return m_bEnableControlBar; } bool ShouldEnableControlBarKeyboard() const { return m_bEnableControlBarKeyboard; } bool ShouldEnableControlBarClose() const { return m_bEnableControlBarClose; } bool ShouldEnableClickStabilization() const { return m_bEnableClickStabilization; } bool IsModal() const { return m_bModal; } float GetPhysicalWidth() const { return m_flPhysicalWidth; } float GetPhysicalCurvature() const { return m_flPhysicalCurvature; } float GetPhysicalPreCurvePitch() const { return m_flPhysicalPreCurvePitch; } float GetScrollSpeed() const { return m_flScrollSpeed; } bool ConsumeNudgeToVisible() { return std::exchange( m_bNudgeToVisible, false ); } bool ShouldNudgeToVisible() const { return m_bNudgeToVisible; } CVulkanTexture *GetBlackTexture() { return m_pBlackTexture.get(); } protected: virtual void OnBackendBlobDestroyed( BackendBlob *pBlob ) override { } private: void WaitUntilVisible() { if ( ShouldNudgeToVisible() ) return; m_nOverlaysVisible.wait( 0 ); } void SetFocus( COpenVRConnector *pFocus ) { COpenVRConnector *pPreviousFocus = m_pFocusConnector.exchange( pFocus ); if ( pPreviousFocus != pFocus ) { MakeFocusDirty(); update_connector_display_info_wl( NULL ); } } COpenVRPlane *GetPlaneByOverlayHandle( vr::VROverlayHandle_t hOverlay ) { COpenVRPlane *pPlane = nullptr; for ( COpenVRConnector *pConnector : m_pActiveConnectors ) { pPlane = pConnector->GetPlaneByOverlayHandle( hOverlay ); if ( pPlane ) break; } return pPlane; } void ProcessVRInput() { std::scoped_lock lock{m_mutActiveConnectors}; vr::VREvent_t vrEvent; bool bDidScrollThisFrame = false; bool bPendingTouchMove = false; float flTouchMoveX, flTouchMoveY; vr::VROverlayHandle_t hOverlay; while (vr::VRSystem()->PollNextEventWithPoseAndOverlays(vr::TrackingUniverseSeated, &vrEvent, sizeof(vrEvent), nullptr, &hOverlay)) { COpenVRPlane *pPlane = nullptr; COpenVRConnector *pConnector = nullptr; bool bIsSteam = false; if (hOverlay != vr::k_ulOverlayHandleInvalid) { pPlane = GetPlaneByOverlayHandle(hOverlay); if (pPlane) { pConnector = pPlane->GetConnector(); bIsSteam = VirtualConnectorKeyIsSteam(pConnector->GetVirtualConnectorKey()); } } switch (vrEvent.eventType) { case vr::VREvent_Quit: { raise(SIGTERM); } break; case vr::VREvent_OverlayClosed: { if (!steamMode || bIsSteam) { if (!pPlane || !pPlane->IsSubview()) { raise(SIGTERM); } } else { // How do we quit a game? // Do we? } break; } case vr::VREvent_SceneApplicationChanged: { if (m_uCurrentScenePid != vrEvent.data.process.pid) { m_uCurrentScenePid = vrEvent.data.process.pid; m_uCurrentSceneAppId = get_appid_from_pid(m_uCurrentScenePid); openvr_log.debugf("SceneApplicationChanged -> pid: %u appid: %u", m_uCurrentScenePid, m_uCurrentSceneAppId); std::optional oulNewSceneAppVirtualConnectorKey; if (cv_backend_virtual_connector_strategy == VirtualConnectorStrategies::PerAppId) { oulNewSceneAppVirtualConnectorKey = m_uCurrentSceneAppId; } if ((oulNewSceneAppVirtualConnectorKey || m_oulCurrentSceneVirtualConnectorKey) && (oulNewSceneAppVirtualConnectorKey != m_oulCurrentSceneVirtualConnectorKey)) { for (COpenVRConnector *pOtherConnector : m_pActiveConnectors) { if (oulNewSceneAppVirtualConnectorKey) { if (pOtherConnector->GetVirtualConnectorKey() == *oulNewSceneAppVirtualConnectorKey) pOtherConnector->MarkSceneAppShown(true); } if (m_oulCurrentSceneVirtualConnectorKey) { if (pOtherConnector->GetVirtualConnectorKey() == *m_oulCurrentSceneVirtualConnectorKey) pOtherConnector->MarkSceneAppShown(false); } } } m_oulCurrentSceneVirtualConnectorKey = oulNewSceneAppVirtualConnectorKey; } break; } case vr::VREvent_KeyboardCharInput: { if (m_pIME) { type_text(m_pIME, vrEvent.data.keyboard.cNewInput); } break; } case vr::VREvent_MouseMove: { if (pConnector && pConnector->m_bUsingVRMouse) { SetFocus(pConnector); float flX = vrEvent.data.mouse.x / float(g_nOutputWidth); float flY = (g_nOutputHeight - vrEvent.data.mouse.y) / float(g_nOutputHeight); TouchClickMode eMode = GetTouchClickMode(); if (eMode == TouchClickModes::Trackpad) { glm::vec2 vOldTrackpadPos = m_vScreenTrackpadPos; m_vScreenTrackpadPos = glm::vec2{flX, flY}; if (m_bMouseDown) { glm::vec2 vDelta = (m_vScreenTrackpadPos - vOldTrackpadPos); // We are based off normalized coords, so we need to fix the aspect ratio // or we get different sensitivities on X and Y. vDelta.y *= ((float)g_nOutputHeight / (float)g_nOutputWidth); vDelta *= float(cv_vr_trackpad_sensitivity); wlserver_lock(); wlserver_mousemotion(vDelta.x, vDelta.y, ++m_uFakeTimestamp); wlserver_unlock(); } } else { // Hold the call to wlserver_touchmotion until we're sure there are no VREvent_ScrollSmooth events this frame bPendingTouchMove = true; flTouchMoveX = flX; flTouchMoveY = flY; } } break; } case vr::VREvent_FocusEnter: if (pConnector) { pConnector->m_bUsingVRMouse = true; SetFocus(pConnector); } break; case vr::VREvent_MouseButtonUp: case vr::VREvent_MouseButtonDown: if (pConnector) { SetFocus(pConnector); if (!pConnector->m_bUsingVRMouse) { pConnector->m_bUsingVRMouse = true; } else { float flX = vrEvent.data.mouse.x / float(g_nOutputWidth); float flY = (g_nOutputHeight - vrEvent.data.mouse.y) / float(g_nOutputHeight); uint64_t ulNow = get_time_in_nanos(); if (vrEvent.eventType == vr::VREvent_MouseButtonDown) { m_ulMouseDownTime = ulNow; m_bMouseDown = true; } else { m_bMouseDown = false; } TouchClickMode eMode = GetTouchClickMode(); if (eMode == TouchClickModes::Trackpad) { m_vScreenTrackpadPos = glm::vec2{flX, flY}; if (vrEvent.eventType == vr::VREvent_MouseButtonUp) { glm::vec2 vTotalDelta = (m_vScreenTrackpadPos - m_vScreenStartTrackpadPos); vTotalDelta.y *= ((float)g_nOutputHeight / (float)g_nOutputWidth); float flMaxAbsTotalDelta = std::max(std::abs(vTotalDelta.x), std::abs(vTotalDelta.y)); uint64_t ulClickTime = ulNow - m_ulMouseDownTime; if (ulClickTime <= cv_vr_trackpad_click_time && flMaxAbsTotalDelta <= cv_vr_trackpad_click_max_delta) { wlserver_lock(); wlserver_mousebutton(BTN_LEFT, true, ++m_uFakeTimestamp); wlserver_unlock(); sleep_for_nanos(g_SteamCompMgrLimitedAppRefreshCycle + 1'000'000); wlserver_lock(); wlserver_mousebutton(BTN_LEFT, false, ++m_uFakeTimestamp); wlserver_unlock(); } else { m_vScreenStartTrackpadPos = m_vScreenTrackpadPos; } } } else { wlserver_lock(); if (vrEvent.eventType == vr::VREvent_MouseButtonDown) wlserver_touchdown(flX, flY, 0, ++m_uFakeTimestamp); else wlserver_touchup(0, ++m_uFakeTimestamp); wlserver_unlock(); } } } break; case vr::VREvent_ScrollSmooth: if (pConnector) { SetFocus(pConnector); float flX = -vrEvent.data.scroll.xdelta * m_flScrollSpeed; float flY = -vrEvent.data.scroll.ydelta * m_flScrollSpeed; wlserver_lock(); wlserver_mousewheel(flX, flY, ++m_uFakeTimestamp); wlserver_unlock(); bDidScrollThisFrame = true; } break; case vr::VREvent_ButtonPress: if (pConnector) { SetFocus(pConnector); vr::EVRButtonId button = (vr::EVRButtonId)vrEvent.data.controller.button; if (button != vr::k_EButton_Steam && button != vr::k_EButton_QAM) break; if (button == vr::k_EButton_Steam) openvr_log.infof("STEAM button pressed."); else openvr_log.infof("QAM button pressed."); wlserver_open_steam_menu(button == vr::k_EButton_QAM); } break; case vr::VREvent_OverlayShown: case vr::VREvent_OverlayHidden: if (pConnector) { // Only handle this for the base plane. // Subviews can be hidden if we hide them ourselves, // or for other reasons. if (!pPlane->IsSubview()) { pConnector->MarkOverlayShown(vrEvent.eventType == vr::VREvent_OverlayShown); } } break; default: break; } } if (bPendingTouchMove) { // Always warp a cursor, even if it's invisible, so we get hover events. // Unless a scroll happened this frame. Then, skip the cursor move because it causes the scroll to be dropped. bool bAlwaysMoveCursor = !bDidScrollThisFrame && (GetTouchClickMode() == TouchClickModes::Passthrough) && cv_vr_always_warp_cursor; wlserver_lock(); wlserver_touchmotion(flTouchMoveX, flTouchMoveY, 0, ++m_uFakeTimestamp, bAlwaysMoveCursor); wlserver_unlock(); } // Process mouse input state. for (COpenVRConnector *pConnector : m_pActiveConnectors) { bool bUsingPhysicalMouse = GetCurrentConnector() == pConnector && !pConnector->m_bUsingVRMouse; bool bShowCursor = !pConnector->IsRelativeMouse(); if (bUsingPhysicalMouse && bShowCursor) { vr::HmdVector2_t vMousePos = { static_cast(wlserver.mouse_surface_cursorx), static_cast(static_cast(g_nOutputHeight) - wlserver.mouse_surface_cursory), }; vr::VROverlay()->SetOverlayCursorPositionOverride(pConnector->GetPrimaryPlane()->GetOverlay(), &vMousePos); pConnector->m_bCurrentlyOverridingPosition = true; } else { if (!pConnector->m_bCurrentlyOverridingPosition) continue; vr::VROverlay()->ClearOverlayCursorPositionOverride(pConnector->GetPrimaryPlane()->GetOverlay()); pConnector->m_bCurrentlyOverridingPosition = false; } } } std::string m_szOverlayKey; std::string m_szAppOverlayKey; const char *m_pchOverlayName = nullptr; const char *m_pchOverlayIcon = nullptr; bool m_bExplicitOverlayName = false; bool m_bNudgeToVisible = false; bool m_bEnableControlBar = false; bool m_bEnableControlBarKeyboard = false; bool m_bEnableControlBarClose = false; bool m_bEnableClickStabilization = false; bool m_bModal = false; float m_flPhysicalWidth = 2.0f; float m_flPhysicalCurvature = 0.0f; float m_flPhysicalPreCurvePitch = 0.0f; float m_flScrollSpeed = 1.0f; // TODO: Restructure and remove the need for this. wlserver_input_method *m_pIME = nullptr; OwningRc m_pBlackTexture; std::atomic m_nOverlaysVisible = { 0 }; vr::IVRIPCResourceManagerClient *m_pIPCResourceManager = nullptr; std::unordered_map> m_FormatModifiers; std::atomic m_uFakeTimestamp = { 0 }; bool m_bMouseDown = false; uint64_t m_ulMouseDownTime = 0; // Fake "trackpad" tracking for the whole overlay panel. glm::vec2 m_vScreenTrackpadPos{}; glm::vec2 m_vScreenStartTrackpadPos{}; uint32_t m_uCurrentScenePid = -1; uint32_t m_uCurrentSceneAppId = 0; std::optional m_oulCurrentSceneVirtualConnectorKey; friend COpenVRConnector; std::vector m_pActiveConnectors; std::mutex m_mutActiveConnectors; std::atomic m_pFocusConnector; std::atomic m_bInitted = { false }; std::atomic m_bRunning = { false }; std::shared_ptr m_pLibInput; CAsyncWaiter, 16> m_LibInputWaiter; // Threads need to go last, as they rely on the other things in the class being constructed before their code is run. std::thread m_FlipHandlerThread; }; //////////////////// // COpenVRConnector //////////////////// COpenVRConnector::COpenVRConnector( COpenVRBackend *pBackend, uint64_t ulVirtualConnectorKey ) : CBaseBackendConnector{ ulVirtualConnectorKey } , m_pBackend{ pBackend } , m_Planes{ this, this, this, this, this, this, this, this } { } COpenVRConnector::~COpenVRConnector() { std::scoped_lock lock{ m_pBackend->m_mutActiveConnectors }; MarkSceneAppShown( false ); MarkOverlayShown( false ); auto iter = m_pBackend->m_pActiveConnectors.begin(); for ( ; iter != m_pBackend->m_pActiveConnectors.end(); iter++ ) { if ( *iter == this ) break; } if ( iter != m_pBackend->m_pActiveConnectors.end() ) m_pBackend->m_pActiveConnectors.erase( iter ); COpenVRConnector *pThis = this; m_pBackend->m_pFocusConnector.compare_exchange_strong( pThis, nullptr ); } GamescopeScreenType COpenVRConnector::GetScreenType() const { return GAMESCOPE_SCREEN_TYPE_INTERNAL; } GamescopePanelOrientation COpenVRConnector::GetCurrentOrientation() const { return GAMESCOPE_PANEL_ORIENTATION_0; } bool COpenVRConnector::SupportsHDR() const { return false; } bool COpenVRConnector::IsHDRActive() const { return false; } const BackendConnectorHDRInfo &COpenVRConnector::GetHDRInfo() const { return m_HDRInfo; } bool COpenVRConnector::IsVRRActive() const { return false; } std::span COpenVRConnector::GetModes() const { return std::span{}; } bool COpenVRConnector::SupportsVRR() const { return false; } std::span COpenVRConnector::GetRawEDID() const { return std::span{ m_FakeEdid.begin(), m_FakeEdid.end() }; } std::span COpenVRConnector::GetValidDynamicRefreshRates() const { return std::span{}; } void COpenVRConnector::GetNativeColorimetry( bool bHDR10, displaycolorimetry_t *displayColorimetry, EOTF *displayEOTF, displaycolorimetry_t *outputEncodingColorimetry, EOTF *outputEncodingEOTF ) const { *displayColorimetry = displaycolorimetry_709; *displayEOTF = EOTF_Gamma22; *outputEncodingColorimetry = displaycolorimetry_709; *outputEncodingEOTF = EOTF_Gamma22; } const char *COpenVRConnector::GetName() const { return "OpenVR"; } const char *COpenVRConnector::GetMake() const { return "Gamescope"; } const char *COpenVRConnector::GetModel() const { return "Virtual Display"; } int COpenVRConnector::Present( const FrameInfo_t *pFrameInfo, bool bAsync ) { bool bNeedsFullComposite = false; // TODO: Dedupe some of this composite check code between us and drm.cpp bool bLayer0ScreenSize = close_enough(pFrameInfo->layers[0].scale.x, 1.0f) && close_enough(pFrameInfo->layers[0].scale.y, 1.0f); bool bNeedsCompositeFromFilter = (g_upscaleFilter == GamescopeUpscaleFilter::NEAREST || g_upscaleFilter == GamescopeUpscaleFilter::PIXEL) && !bLayer0ScreenSize; bNeedsFullComposite |= cv_composite_force; bNeedsFullComposite |= pFrameInfo->useFSRLayer0; bNeedsFullComposite |= pFrameInfo->useNISLayer0; bNeedsFullComposite |= pFrameInfo->blurLayer0; bNeedsFullComposite |= bNeedsCompositeFromFilter; bNeedsFullComposite |= g_bColorSliderInUse; bNeedsFullComposite |= pFrameInfo->bFadingOut; bNeedsFullComposite |= !g_reshade_effect.empty(); bNeedsFullComposite |= !m_pBackend->UsesModifiers(); if ( g_bOutputHDREnabled ) bNeedsFullComposite |= g_bHDRItmEnable; if ( !m_pBackend->SupportsColorManagement() ) bNeedsFullComposite |= ColorspaceIsHDR( pFrameInfo->layers[0].colorspace ); bNeedsFullComposite |= !!(g_uCompositeDebug & CompositeDebugFlag::Heatmap); if ( !bNeedsFullComposite ) { bool bNeedsBacking = true; if ( pFrameInfo->layerCount >= 1 ) { if ( pFrameInfo->layers[0].isScreenSize() && ( !pFrameInfo->layers[0].hasAlpha() || cv_vr_transparent_backing ) ) bNeedsBacking = false; } uint32_t uCurrentPlane = 0; if ( bNeedsBacking ) { COpenVRPlane *pPlane = &m_Planes[uCurrentPlane++]; pPlane->Present( OpenVRPlaneState { .pTexture = m_pBackend->GetBlackTexture(), .flSrcWidth = double( g_nOutputWidth ), .flSrcHeight = double( g_nOutputHeight ), .nDstWidth = int32_t( g_nOutputWidth ), .nDstHeight = int32_t( g_nOutputHeight ), .eColorspace = GAMESCOPE_APP_TEXTURE_COLORSPACE_PASSTHRU, .bOpaque = !cv_vr_transparent_backing, .flAlpha = cv_vr_transparent_backing ? 0.0f : 1.0f, } ); } for ( int i = 0; i < 8 && uCurrentPlane < 8; i++ ) m_Planes[uCurrentPlane++].Present( i < pFrameInfo->layerCount ? &pFrameInfo->layers[i] : nullptr ); } else { std::optional oCompositeResult = vulkan_composite( (FrameInfo_t *)pFrameInfo, nullptr, false ); if ( !oCompositeResult ) { openvr_log.errorf( "vulkan_composite failed" ); return -EINVAL; } vulkan_wait( *oCompositeResult, true ); FrameInfo_t::Layer_t compositeLayer{}; compositeLayer.scale.x = 1.0; compositeLayer.scale.y = 1.0; compositeLayer.opacity = 1.0; compositeLayer.zpos = g_zposBase; compositeLayer.tex = vulkan_get_last_output_image( false, false ); compositeLayer.applyColorMgmt = false; compositeLayer.filter = GamescopeUpscaleFilter::NEAREST; compositeLayer.ctm = nullptr; compositeLayer.colorspace = pFrameInfo->outputEncodingEOTF == EOTF_PQ ? GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ : GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB; GetPrimaryPlane()->Present( &compositeLayer ); for ( int i = 1; i < 8; i++ ) m_Planes[i].Present( nullptr ); } GetVBlankTimer().UpdateWasCompositing( true ); GetVBlankTimer().UpdateLastDrawTime( get_time_in_nanos() - g_SteamCompMgrVBlankTime.ulWakeupTime ); m_pBackend->PollState(); return 0; } /////////////////// // INestedHints /////////////////// void COpenVRConnector::SetCursorImage( std::shared_ptr info ) { } void COpenVRConnector::SetRelativeMouseMode( bool bRelative ) { if ( bRelative != m_bRelativeMouse ) { for ( COpenVRPlane &plane : m_Planes ) { vr::VROverlay()->SetOverlayFlag( plane.GetOverlay(), vr::VROverlayFlags_HideLaserIntersection, cv_vr_trackpad_hide_laser && bRelative ); } m_bRelativeMouse = bRelative; } } void COpenVRConnector::SetVisible( bool bVisible ) { vr::VROverlay()->SetOverlayFlag( GetPrimaryPlane()->GetOverlay(), vr::VROverlayFlags_VisibleInDashboard, bVisible ); } void COpenVRConnector::SetTitle( std::shared_ptr szTitle ) { if ( !m_pBackend->m_bExplicitOverlayName ) vr::VROverlay()->SetOverlayName( GetPrimaryPlane()->GetOverlay(), szTitle ? szTitle->c_str() : m_pBackend->GetOverlayName() ); } void COpenVRConnector::SetIcon( std::shared_ptr> uIconPixels ) { if ( cv_vr_use_window_icons && uIconPixels && uIconPixels->size() >= 3 ) { const uint32_t uWidth = (*uIconPixels)[0]; const uint32_t uHeight = (*uIconPixels)[1]; struct rgba_t { uint8_t r,g,b,a; }; for ( uint32_t& val : *uIconPixels ) { rgba_t rgb = *((rgba_t*)&val); std::swap(rgb.r, rgb.b); val = *((uint32_t*)&rgb); } vr::VROverlay()->SetOverlayRaw( GetPrimaryPlane()->GetOverlayThumbnail(), &(*uIconPixels)[2], uWidth, uHeight, sizeof(uint32_t) ); } else if ( m_pBackend->GetOverlayIcon() ) { vr::VROverlay()->SetOverlayFromFile( GetPrimaryPlane()->GetOverlayThumbnail(), m_pBackend->GetOverlayIcon() ); } else { vr::VROverlay()->ClearOverlayTexture( GetPrimaryPlane()->GetOverlayThumbnail() ); } } void COpenVRConnector::SetSelection( std::shared_ptr szContents, GamescopeSelection eSelection ) { // Do nothing } bool COpenVRConnector::UpdateEdid() { m_FakeEdid = GenerateSimpleEdid( g_nNestedWidth, g_nNestedHeight ); return true; } bool COpenVRConnector::Init() { openvr_log.debugf( "New connector! -> ulKey: %lu", GetVirtualConnectorKey() ); m_bNudgeToVisible = m_pBackend->ShouldNudgeToVisible(); for ( uint32_t i = 0; i < 8; i++ ) { bool bSuccess = m_Planes[i].Init( i == 0 ? nullptr : &m_Planes[0], i == 0 ? nullptr : &m_Planes[ i - 1 ] ); if ( !bSuccess ) return false; } UpdateEdid(); m_pBackend->HackUpdatePatchedEdid(); if ( g_bForceRelativeMouse ) this->SetRelativeMouseMode( true ); if ( m_pBackend->m_oulCurrentSceneVirtualConnectorKey && GetVirtualConnectorKey() == *m_pBackend->m_oulCurrentSceneVirtualConnectorKey ) { MarkSceneAppShown( true ); } // Set the initial overlay visibility MarkOverlayShown( vr::VROverlay()->IsOverlayVisible( GetPrimaryPlane()->GetOverlay() ) ); return true; } void COpenVRConnector::UpdateVisibility( const char *pszReason ) { bool bVisible = IsVisible(); if ( m_bWasVisible != bVisible ) { int nNewOverlayVisibleCount; if ( bVisible ) nNewOverlayVisibleCount = ++m_pBackend->m_nOverlaysVisible; else nNewOverlayVisibleCount = --m_pBackend->m_nOverlaysVisible; m_pBackend->m_nOverlaysVisible.notify_all(); m_bWasVisible = bVisible; openvr_log.debugf( "[%s] ulKey: %lu nNewOverlayVisibleCount: %d -> m_bOverlayShown: %s m_bSceneAppVisible: %s", pszReason, GetVirtualConnectorKey(), nNewOverlayVisibleCount, m_bOverlayShown ? "true" : "false", m_bSceneAppVisible ? "true" : "false" ); } } ///////////////////////// // COpenVRFb ///////////////////////// COpenVRFb::COpenVRFb( COpenVRBackend *pBackend, vr::SharedTextureHandle_t ulHandle ) : CBaseBackendFb{} , m_pBackend{ pBackend } , m_ulHandle{ ulHandle } { } COpenVRFb::~COpenVRFb() { if ( m_ulHandle != 0 ) m_pBackend->GetIPCResourceManager()->UnrefResource( m_ulHandle ); m_ulHandle = 0; } ///////////////////////// // COpenVRPlane ///////////////////////// COpenVRPlane::COpenVRPlane( COpenVRConnector *pConnector ) : m_pConnector{ pConnector } , m_pBackend{ pConnector->GetBackend() } { } COpenVRPlane::~COpenVRPlane() { if ( m_hOverlayThumbnail != vr::k_ulOverlayHandleInvalid ) vr::VROverlay()->DestroyOverlay( m_hOverlayThumbnail ); if ( m_hOverlay != vr::k_ulOverlayHandleInvalid ) vr::VROverlay()->DestroyOverlay( m_hOverlay ); } bool COpenVRPlane::Init( COpenVRPlane *pParent, COpenVRPlane *pSiblingBelow ) { m_bIsSubview = pParent != nullptr; if ( pSiblingBelow ) { m_uSortOrder = pSiblingBelow->GetSortOrder() + 1; } std::string sOverlayKey = m_pBackend->GetOverlayKey(); VirtualConnectorStrategy eStrategy = cv_backend_virtual_connector_strategy; if ( !VirtualConnectorStrategyIsSingleOutput( eStrategy ) ) { uint64_t ulKey = m_pConnector->GetVirtualConnectorKey(); bool bIsSteam = VirtualConnectorKeyIsSteam( ulKey ); if ( !bIsSteam ) { const char *pszAppOverlayKey = m_pBackend->GetAppOverlayKey(); if ( pszAppOverlayKey && *pszAppOverlayKey ) { sOverlayKey = pszAppOverlayKey; sOverlayKey += "."; } else { sOverlayKey += ".app."; } sOverlayKey += std::to_string( m_pConnector->GetVirtualConnectorKey() ); } } if ( !m_bIsSubview ) { m_sDashboardOverlayKey = sOverlayKey; openvr_log.debugf( "Creating new dashboard overlay: %s", m_sDashboardOverlayKey.c_str() ); vr::VROverlay()->CreateDashboardOverlay( sOverlayKey.c_str(), m_pBackend->GetOverlayName(), &m_hOverlay, &m_hOverlayThumbnail ); vr::VROverlay()->SetOverlayFlag( m_hOverlay, vr::VROverlayFlags_EnableControlBar, m_pBackend->ShouldEnableControlBar() ); vr::VROverlay()->SetOverlayFlag( m_hOverlay, vr::VROverlayFlags_EnableControlBarKeyboard, m_pBackend->ShouldEnableControlBarKeyboard() ); vr::VROverlay()->SetOverlayFlag( m_hOverlay, vr::VROverlayFlags_EnableControlBarClose, m_pBackend->ShouldEnableControlBarClose() ); vr::VROverlay()->SetOverlayFlag( m_hOverlay, vr::VROverlayFlags_WantsModalBehavior, m_pBackend->IsModal() ); vr::VROverlay()->SetOverlayFlag( m_hOverlay, vr::VROverlayFlags_SendVRSmoothScrollEvents, true ); vr::VROverlay()->SetOverlayFlag( m_hOverlay, vr::VROverlayFlags_VisibleInDashboard, false ); vr::VROverlay()->SetOverlayFlag( m_hOverlay, vr::VROverlayFlags_HideLaserIntersection, cv_vr_trackpad_hide_laser && m_pConnector->IsRelativeMouse() ); vr::VROverlay()->SetOverlayWidthInMeters( m_hOverlay, m_pBackend->GetPhysicalWidth() ); vr::VROverlay()->SetOverlayCurvature ( m_hOverlay, m_pBackend->GetPhysicalCurvature() ); vr::VROverlay()->SetOverlayPreCurvePitch( m_hOverlay, m_pBackend->GetPhysicalPreCurvePitch() ); if ( m_pBackend->GetOverlayIcon() ) { vr::EVROverlayError err = vr::VROverlay()->SetOverlayFromFile( m_hOverlayThumbnail, m_pBackend->GetOverlayIcon() ); if( err != vr::VROverlayError_None ) { openvr_log.errorf( "Unable to set thumbnail to %s: %s\n", m_pBackend->GetOverlayIcon(), vr::VROverlay()->GetOverlayErrorNameFromEnum( err ) ); } } } else { std::string szSubviewName = sOverlayKey + std::string(".layer") + std::to_string( m_uSortOrder ); vr::VROverlay()->CreateSubviewOverlay( pParent->GetOverlay(), szSubviewName.c_str(), "Gamescope Layer", &m_hOverlay ); } vr::VROverlay()->SetOverlayFlag( m_hOverlay, vr::VROverlayFlags_EnableClickStabilization, m_pBackend->ShouldEnableClickStabilization() ); vr::VROverlay()->SetOverlayFlag( m_hOverlay, vr::VROverlayFlags_IsPremultiplied, true ); vr::VROverlay()->SetOverlayInputMethod( m_hOverlay, vr::VROverlayInputMethod_Mouse ); vr::VROverlay()->SetOverlaySortOrder( m_hOverlay, m_uSortOrder ); return true; } void COpenVRPlane::Present( std::optional oState ) { COpenVRFb *pFb = nullptr; if ( oState ) { vr::VROverlay()->SetOverlayAlpha( m_hOverlay, oState->flAlpha ); if ( m_pBackend->UsesModifiers() ) { vr::VROverlay()->SetOverlayFlag( m_hOverlay, vr::VROverlayFlags_IgnoreTextureAlpha, oState->bOpaque || !DRMFormatHasAlpha( oState->pTexture->drmFormat() ) || cv_vr_debug_force_opaque ); vr::HmdVector2_t vMouseScale = { float( oState->nDstWidth ), float( oState->nDstHeight ), }; vr::VROverlay()->SetOverlayMouseScale( m_hOverlay, &vMouseScale ); vr::VRTextureBounds_t vTextureBounds = { float( ( oState->flSrcX ) / double( oState->pTexture->width() ) ), float( ( oState->flSrcY ) / double( oState->pTexture->height() ) ), float( ( oState->flSrcX + oState->flSrcWidth ) / double( oState->pTexture->width() ) ), float( ( oState->flSrcY + oState->flSrcHeight ) / double( oState->pTexture->height() ) ), }; vr::VROverlay()->SetOverlayTextureBounds( m_hOverlay, &vTextureBounds ); if ( m_bIsSubview ) { vr::VROverlay()->SetSubviewPosition( m_hOverlay, oState->nDestX, oState->nDestY ); vr::VROverlay()->ShowOverlay( m_hOverlay ); } pFb = static_cast( oState->pTexture->GetBackendFb() ); vr::SharedTextureHandle_t ulHandle = pFb->GetSharedTextureHandle(); vr::Texture_t texture = { (void *)&ulHandle, vr::TextureType_SharedTextureHandle, vr::ColorSpace_Gamma }; vr::VROverlay()->SetOverlayTexture( m_hOverlay, &texture ); } else { assert( !m_bIsSubview ); vr::VRVulkanTextureData_t data = { .m_nImage = (uint64_t)(uintptr_t)oState->pTexture->vkImage(), .m_pDevice = g_device.device(), .m_pPhysicalDevice = g_device.physDev(), .m_pInstance = g_device.instance(), .m_pQueue = g_device.queue(), .m_nQueueFamilyIndex = g_device.queueFamily(), .m_nWidth = oState->pTexture->width(), .m_nHeight = oState->pTexture->height(), .m_nFormat = oState->pTexture->format(), .m_nSampleCount = 1, }; vr::Texture_t texture = { &data, vr::TextureType_Vulkan, vr::ColorSpace_Gamma }; vr::VROverlay()->SetOverlayTexture( m_hOverlay, &texture ); } if ( !m_bIsSubview ) { bool bNudgeToVisible = cv_vr_nudge_to_visible_per_connector ? m_pConnector->ConsumeNudgeToVisible() : m_pBackend->ConsumeNudgeToVisible(); if ( bNudgeToVisible ) { vr::VROverlay()->ShowDashboard( m_sDashboardOverlayKey.c_str() ); // Make sure we don't leave any nudges either side. m_pConnector->ConsumeNudgeToVisible(); if ( !cv_vr_nudge_to_visible_per_connector ) m_pBackend->ConsumeNudgeToVisible(); } } } else { if ( m_bIsSubview ) { vr::VROverlay()->HideOverlay( m_hOverlay ); } } { std::scoped_lock lock{ m_mutFbIds }; m_pQueuedFbId = pFb; } } void COpenVRPlane::Present( const FrameInfo_t::Layer_t *pLayer ) { if ( pLayer && pLayer->tex ) { Present( OpenVRPlaneState { .pTexture = pLayer->tex.get(), .nDestX = int32_t( -pLayer->offset.x ), .nDestY = int32_t( -pLayer->offset.y ), .flSrcX = 0.0, .flSrcY = 0.0, .flSrcWidth = double( pLayer->tex->width() ), .flSrcHeight = double( pLayer->tex->height() ), .nDstWidth = int32_t( pLayer->tex->width() / double( pLayer->scale.x ) ), .nDstHeight = int32_t( pLayer->tex->height() / double( pLayer->scale.y ) ), .eColorspace = pLayer->colorspace, .bOpaque = pLayer->zpos == g_zposBase && !cv_vr_transparent_backing, .flAlpha = pLayer->opacity, } ); } else { Present( std::nullopt ); } } void COpenVRPlane::OnPageFlip() { { std::scoped_lock lock{ m_mutFbIds }; // XXX: We have no guarantee for WHAT the sequence is here. This could be total crap. // but this is probably good enough for now? m_pVisibleFbId = std::move( m_pQueuedFbId ); m_pQueuedFbId = nullptr; } } ///////////////////////// // Backend Instantiator ///////////////////////// template <> bool IBackend::Set() { return Set( new COpenVRBackend{} ); } } ValveSoftware-gamescope-eb620ab/src/Backends/SDLBackend.cpp000066400000000000000000000654421502457270500236470ustar00rootroot00000000000000// For the nested case, reads input from the SDL window and send to wayland #include #include #include #include #include #include #include #include "SDL_clipboard.h" #include "SDL_events.h" #include "gamescope_shared.h" #include "main.hpp" #include "wlserver.hpp" #include #include #include "rendervulkan.hpp" #include "steamcompmgr.hpp" #include "Utils/Defer.h" #include "refresh_rate.h" #include "sdlscancodetable.hpp" static int g_nOldNestedRefresh = 0; static bool g_bWindowFocused = true; static int g_nOutputWidthPts = 0; static int g_nOutputHeightPts = 0; extern bool g_bForceHDR10OutputDebug; extern bool steamMode; extern bool g_bFirstFrame; extern int g_nPreferredOutputWidth; extern int g_nPreferredOutputHeight; namespace gamescope { enum class SDLInitState { SDLInit_Waiting, SDLInit_Success, SDLInit_Failure, }; enum SDLCustomEvents { GAMESCOPE_SDL_EVENT_TITLE, GAMESCOPE_SDL_EVENT_ICON, GAMESCOPE_SDL_EVENT_VISIBLE, GAMESCOPE_SDL_EVENT_GRAB, GAMESCOPE_SDL_EVENT_CURSOR, GAMESCOPE_SDL_EVENT_COUNT, }; class CSDLConnector final : public CBaseBackendConnector, public INestedHints { public: CSDLConnector(); virtual bool Init(); virtual ~CSDLConnector(); ///////////////////// // IBackendConnector ///////////////////// virtual gamescope::GamescopeScreenType GetScreenType() const override; virtual GamescopePanelOrientation GetCurrentOrientation() const override; virtual bool SupportsHDR() const override; virtual bool IsHDRActive() const override; virtual const BackendConnectorHDRInfo &GetHDRInfo() const override; virtual bool IsVRRActive() const override; virtual std::span GetModes() const override; virtual bool SupportsVRR() const override; virtual std::span GetRawEDID() const override; virtual std::span GetValidDynamicRefreshRates() const override; virtual void GetNativeColorimetry( bool bHDR10, displaycolorimetry_t *displayColorimetry, EOTF *displayEOTF, displaycolorimetry_t *outputEncodingColorimetry, EOTF *outputEncodingEOTF ) const override; virtual const char *GetName() const override { return "SDLWindow"; } virtual const char *GetMake() const override { return "Gamescope"; } virtual const char *GetModel() const override { return "Virtual Display"; } virtual INestedHints *GetNestedHints() override { return this; } virtual int Present( const FrameInfo_t *pFrameInfo, bool bAsync ) override; /////////////////// // INestedHints /////////////////// virtual void SetCursorImage( std::shared_ptr info ) override; virtual void SetRelativeMouseMode( bool bRelative ) override; virtual void SetVisible( bool bVisible ) override; virtual void SetTitle( std::shared_ptr szTitle ) override; virtual void SetIcon( std::shared_ptr> uIconPixels ) override; virtual void SetSelection( std::shared_ptr szContents, GamescopeSelection eSelection ) override; //-- SDL_Window *GetSDLWindow() const { return m_pWindow; } VkSurfaceKHR GetVulkanSurface() const { return m_pVkSurface; } private: SDL_Window *m_pWindow = nullptr; VkSurfaceKHR m_pVkSurface = VK_NULL_HANDLE; BackendConnectorHDRInfo m_HDRInfo{}; }; class CSDLBackend : public CBaseBackend { public: CSDLBackend(); ///////////// // IBackend ///////////// virtual bool Init() override; virtual bool PostInit() override; virtual std::span GetInstanceExtensions() const override; virtual std::span GetDeviceExtensions( VkPhysicalDevice pVkPhysicalDevice ) const override; virtual VkImageLayout GetPresentLayout() const override; virtual void GetPreferredOutputFormat( uint32_t *pPrimaryPlaneFormat, uint32_t *pOverlayPlaneFormat ) const override; virtual bool ValidPhysicalDevice( VkPhysicalDevice pVkPhysicalDevice ) const override; virtual void DirtyState( bool bForce = false, bool bForceModeset = false ) override; virtual bool PollState() override; virtual std::shared_ptr CreateBackendBlob( const std::type_info &type, std::span data ) override; virtual OwningRc ImportDmabufToBackend( wlr_buffer *pBuffer, wlr_dmabuf_attributes *pDmaBuf ) override; virtual bool UsesModifiers() const override; virtual std::span GetSupportedModifiers( uint32_t uDrmFormat ) const override; virtual IBackendConnector *GetCurrentConnector() override; virtual IBackendConnector *GetConnector( GamescopeScreenType eScreenType ) override; virtual bool SupportsPlaneHardwareCursor() const override; virtual bool SupportsTearing() const override; virtual bool UsesVulkanSwapchain() const override; virtual bool IsSessionBased() const override; virtual bool SupportsExplicitSync() const override; virtual bool IsPaused() const override; virtual bool IsVisible() const override; virtual glm::uvec2 CursorSurfaceSize( glm::uvec2 uvecSize ) const override; //////////////////////// // INestedHints Compat /////////////////////// void SetCursorImage( std::shared_ptr info ); void SetRelativeMouseMode( bool bRelative ); void SetVisible( bool bVisible ); void SetTitle( std::shared_ptr szTitle ); void SetIcon( std::shared_ptr> uIconPixels ); void SetSelection( std::shared_ptr szContents, GamescopeSelection eSelection ); protected: virtual void OnBackendBlobDestroyed( BackendBlob *pBlob ) override; private: void SDLThreadFunc(); uint32_t GetUserEventIndex( SDLCustomEvents eEvent ) const; void PushUserEvent( SDLCustomEvents eEvent ); bool m_bShown = false; CSDLConnector m_Connector; // Window. uint32_t m_uUserEventIdBase = 0u; std::vector m_pszInstanceExtensions; std::thread m_SDLThread; std::atomic m_eSDLInit = { SDLInitState::SDLInit_Waiting }; std::atomic m_bApplicationGrabbed = { false }; std::atomic m_bApplicationVisible = { false }; std::atomic> m_pApplicationCursor; std::atomic> m_pApplicationTitle; std::atomic>> m_pApplicationIcon; SDL_Surface *m_pIconSurface = nullptr; SDL_Surface *m_pCursorSurface = nullptr; SDL_Cursor *m_pCursor = nullptr; }; ////////////////// // CSDLConnector ////////////////// CSDLConnector::CSDLConnector() { } CSDLConnector::~CSDLConnector() { if ( m_pWindow ) SDL_DestroyWindow( m_pWindow ); } bool CSDLConnector::Init() { g_nOutputWidth = g_nPreferredOutputWidth; g_nOutputHeight = g_nPreferredOutputHeight; g_nOutputRefresh = g_nNestedRefresh; if ( g_nOutputHeight == 0 ) { if ( g_nOutputWidth != 0 ) { fprintf( stderr, "Cannot specify -W without -H\n" ); return false; } g_nOutputHeight = 720; } if ( g_nOutputWidth == 0 ) g_nOutputWidth = g_nOutputHeight * 16 / 9; if ( g_nOutputRefresh == 0 ) g_nOutputRefresh = gamescope::ConvertHztomHz( 60 ); uint32_t uSDLWindowFlags = SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN | SDL_WINDOW_ALLOW_HIGHDPI; if ( g_bBorderlessOutputWindow == true ) uSDLWindowFlags |= SDL_WINDOW_BORDERLESS; if ( g_bFullscreen == true ) uSDLWindowFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP; if ( g_bGrabbed == true ) uSDLWindowFlags |= SDL_WINDOW_KEYBOARD_GRABBED; m_pWindow = SDL_CreateWindow( "gamescope", SDL_WINDOWPOS_UNDEFINED_DISPLAY( g_nNestedDisplayIndex ), SDL_WINDOWPOS_UNDEFINED_DISPLAY( g_nNestedDisplayIndex ), g_nOutputWidth, g_nOutputHeight, uSDLWindowFlags ); if ( m_pWindow == nullptr ) return false; if ( !SDL_Vulkan_CreateSurface( m_pWindow, vulkan_get_instance(), &m_pVkSurface ) ) { fprintf(stderr, "SDL_Vulkan_CreateSurface failed: %s", SDL_GetError () ); return false; } return true; } GamescopeScreenType CSDLConnector::GetScreenType() const { return gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL; } GamescopePanelOrientation CSDLConnector::GetCurrentOrientation() const { return GAMESCOPE_PANEL_ORIENTATION_0; } bool CSDLConnector::SupportsHDR() const { return GetHDRInfo().IsHDR10(); } bool CSDLConnector::IsHDRActive() const { // XXX: blah return false; } const BackendConnectorHDRInfo &CSDLConnector::GetHDRInfo() const { return m_HDRInfo; } bool CSDLConnector::IsVRRActive() const { return false; } std::span CSDLConnector::GetModes() const { return std::span{}; } bool CSDLConnector::SupportsVRR() const { return false; } std::span CSDLConnector::GetRawEDID() const { return std::span{}; } std::span CSDLConnector::GetValidDynamicRefreshRates() const { return std::span{}; } void CSDLConnector::GetNativeColorimetry( bool bHDR10, displaycolorimetry_t *displayColorimetry, EOTF *displayEOTF, displaycolorimetry_t *outputEncodingColorimetry, EOTF *outputEncodingEOTF ) const { if ( g_bForceHDR10OutputDebug ) { *displayColorimetry = displaycolorimetry_2020; *displayEOTF = EOTF_PQ; *outputEncodingColorimetry = displaycolorimetry_2020; *outputEncodingEOTF = EOTF_PQ; } else { *displayColorimetry = displaycolorimetry_709; *displayEOTF = EOTF_Gamma22; *outputEncodingColorimetry = displaycolorimetry_709; *outputEncodingEOTF = EOTF_Gamma22; } } int CSDLConnector::Present( const FrameInfo_t *pFrameInfo, bool bAsync ) { // TODO: Resolve const crap std::optional oCompositeResult = vulkan_composite( (FrameInfo_t *)pFrameInfo, nullptr, false ); if ( !oCompositeResult ) return -EINVAL; vulkan_present_to_window(); // TODO: Hook up PresentationFeedback. // Wait for the composite result on our side *after* we // commit the buffer to the compositor to avoid a bubble. vulkan_wait( *oCompositeResult, true ); GetVBlankTimer().UpdateWasCompositing( true ); GetVBlankTimer().UpdateLastDrawTime( get_time_in_nanos() - g_SteamCompMgrVBlankTime.ulWakeupTime ); return 0; } void CSDLConnector::SetCursorImage( std::shared_ptr info ) { CSDLBackend *pBackend = static_cast( GetBackend() ); pBackend->SetCursorImage( std::move( info ) ); } void CSDLConnector::SetRelativeMouseMode( bool bRelative ) { CSDLBackend *pBackend = static_cast( GetBackend() ); pBackend->SetRelativeMouseMode( bRelative ); } void CSDLConnector::SetVisible( bool bVisible ) { CSDLBackend *pBackend = static_cast( GetBackend() ); pBackend->SetVisible( bVisible ); } void CSDLConnector::SetTitle( std::shared_ptr szTitle ) { CSDLBackend *pBackend = static_cast( GetBackend() ); pBackend->SetTitle( std::move( szTitle ) ); } void CSDLConnector::SetIcon( std::shared_ptr> uIconPixels ) { CSDLBackend *pBackend = static_cast( GetBackend() ); pBackend->SetIcon( std::move( uIconPixels ) ); } void CSDLConnector::SetSelection( std::shared_ptr szContents, GamescopeSelection eSelection ) { if (eSelection == GAMESCOPE_SELECTION_CLIPBOARD) SDL_SetClipboardText(szContents->c_str()); else if (eSelection == GAMESCOPE_SELECTION_PRIMARY) SDL_SetPrimarySelectionText(szContents->c_str()); } //////////////// // CSDLBackend //////////////// CSDLBackend::CSDLBackend() : m_SDLThread{ [this](){ this->SDLThreadFunc(); } } { } bool CSDLBackend::Init() { m_eSDLInit.wait( SDLInitState::SDLInit_Waiting ); return m_eSDLInit == SDLInitState::SDLInit_Success; } bool CSDLBackend::PostInit() { return true; } std::span CSDLBackend::GetInstanceExtensions() const { return std::span{ m_pszInstanceExtensions.begin(), m_pszInstanceExtensions.end() }; } std::span CSDLBackend::GetDeviceExtensions( VkPhysicalDevice pVkPhysicalDevice ) const { return std::span{}; } VkImageLayout CSDLBackend::GetPresentLayout() const { return VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; } void CSDLBackend::GetPreferredOutputFormat( uint32_t *pPrimaryPlaneFormat, uint32_t *pOverlayPlaneFormat ) const { *pPrimaryPlaneFormat = VulkanFormatToDRM( VK_FORMAT_A2B10G10R10_UNORM_PACK32 ); *pOverlayPlaneFormat = VulkanFormatToDRM( VK_FORMAT_B8G8R8A8_UNORM ); } bool CSDLBackend::ValidPhysicalDevice( VkPhysicalDevice pVkPhysicalDevice ) const { return true; } void CSDLBackend::DirtyState( bool bForce, bool bForceModeset ) { } bool CSDLBackend::PollState() { return false; } std::shared_ptr CSDLBackend::CreateBackendBlob( const std::type_info &type, std::span data ) { return std::make_shared( data ); } OwningRc CSDLBackend::ImportDmabufToBackend( wlr_buffer *pBuffer, wlr_dmabuf_attributes *pDmaBuf ) { return new CBaseBackendFb(); } bool CSDLBackend::UsesModifiers() const { return false; } std::span CSDLBackend::GetSupportedModifiers( uint32_t uDrmFormat ) const { return std::span{}; } IBackendConnector *CSDLBackend::GetCurrentConnector() { return &m_Connector; } IBackendConnector *CSDLBackend::GetConnector( GamescopeScreenType eScreenType ) { if ( eScreenType == GAMESCOPE_SCREEN_TYPE_INTERNAL ) return &m_Connector; return nullptr; } bool CSDLBackend::SupportsPlaneHardwareCursor() const { // We use the nested hints cursor stuff. // Not our own plane. return false; } bool CSDLBackend::SupportsTearing() const { return false; } bool CSDLBackend::UsesVulkanSwapchain() const { return true; } bool CSDLBackend::IsSessionBased() const { return false; } bool CSDLBackend::SupportsExplicitSync() const { // We use a Vulkan swapchain, so yes. return true; } bool CSDLBackend::IsPaused() const { return false; } bool CSDLBackend::IsVisible() const { return true; } glm::uvec2 CSDLBackend::CursorSurfaceSize( glm::uvec2 uvecSize ) const { return uvecSize; } /////////////////// // INestedHints /////////////////// void CSDLBackend::SetCursorImage( std::shared_ptr info ) { m_pApplicationCursor = info; PushUserEvent( GAMESCOPE_SDL_EVENT_CURSOR ); } void CSDLBackend::SetRelativeMouseMode( bool bRelative ) { m_bApplicationGrabbed = bRelative; PushUserEvent( GAMESCOPE_SDL_EVENT_GRAB ); } void CSDLBackend::SetVisible( bool bVisible ) { m_bApplicationVisible = bVisible; PushUserEvent( GAMESCOPE_SDL_EVENT_VISIBLE ); } void CSDLBackend::SetTitle( std::shared_ptr szTitle ) { m_pApplicationTitle = szTitle; PushUserEvent( GAMESCOPE_SDL_EVENT_TITLE ); } void CSDLBackend::SetIcon( std::shared_ptr> uIconPixels ) { m_pApplicationIcon = uIconPixels; PushUserEvent( GAMESCOPE_SDL_EVENT_ICON ); } void CSDLBackend::SetSelection( std::shared_ptr szContents, GamescopeSelection eSelection ) { if (eSelection == GAMESCOPE_SELECTION_CLIPBOARD) SDL_SetClipboardText(szContents->c_str()); else if (eSelection == GAMESCOPE_SELECTION_PRIMARY) SDL_SetPrimarySelectionText(szContents->c_str()); } void CSDLBackend::OnBackendBlobDestroyed( BackendBlob *pBlob ) { // Do nothing. } void CSDLBackend::SDLThreadFunc() { pthread_setname_np( pthread_self(), "gamescope-sdl" ); m_uUserEventIdBase = SDL_RegisterEvents( GAMESCOPE_SDL_EVENT_COUNT ); SDL_SetHint( SDL_HINT_APP_NAME, "Gamescope" ); SDL_SetHint( SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1" ); if ( SDL_Init( SDL_INIT_VIDEO | SDL_INIT_EVENTS ) != 0 ) { m_eSDLInit = SDLInitState::SDLInit_Failure; m_eSDLInit.notify_all(); return; } if ( SDL_Vulkan_LoadLibrary( nullptr ) != 0 ) { fprintf(stderr, "SDL_Vulkan_LoadLibrary failed: %s\n", SDL_GetError()); m_eSDLInit = SDLInitState::SDLInit_Failure; m_eSDLInit.notify_all(); return; } unsigned int uExtCount = 0; SDL_Vulkan_GetInstanceExtensions( nullptr, &uExtCount, nullptr ); m_pszInstanceExtensions.resize( uExtCount ); SDL_Vulkan_GetInstanceExtensions( nullptr, &uExtCount, m_pszInstanceExtensions.data() ); if ( !m_Connector.Init() ) { m_eSDLInit = SDLInitState::SDLInit_Failure; m_eSDLInit.notify_all(); return; } if ( !vulkan_init( vulkan_get_instance(), m_Connector.GetVulkanSurface() ) ) { m_eSDLInit = SDLInitState::SDLInit_Failure; m_eSDLInit.notify_all(); return; } if ( !wlsession_init() ) { fprintf( stderr, "Failed to initialize Wayland session\n" ); m_eSDLInit = SDLInitState::SDLInit_Failure; m_eSDLInit.notify_all(); return; } // Update g_nOutputWidthPts. { int width, height; SDL_GetWindowSize( m_Connector.GetSDLWindow(), &width, &height ); g_nOutputWidthPts = width; g_nOutputHeightPts = height; #if SDL_VERSION_ATLEAST(2, 26, 0) SDL_GetWindowSizeInPixels( m_Connector.GetSDLWindow(), &width, &height ); #endif g_nOutputWidth = width; g_nOutputHeight = height; } if ( g_bForceRelativeMouse ) { SDL_SetRelativeMouseMode( SDL_TRUE ); m_bApplicationGrabbed = true; } SDL_SetHint( SDL_HINT_TOUCH_MOUSE_EVENTS, "0" ); g_nOldNestedRefresh = g_nNestedRefresh; m_eSDLInit = SDLInitState::SDLInit_Success; m_eSDLInit.notify_all(); static uint32_t fake_timestamp = 0; wlserver.bWaylandServerRunning.wait( false ); SDL_Event event; while( SDL_WaitEvent( &event ) ) { fake_timestamp++; switch( event.type ) { case SDL_CLIPBOARDUPDATE: { char *pClipBoard = SDL_GetClipboardText(); char *pPrimarySelection = SDL_GetPrimarySelectionText(); gamescope_set_selection(pClipBoard, GAMESCOPE_SELECTION_CLIPBOARD); gamescope_set_selection(pPrimarySelection, GAMESCOPE_SELECTION_PRIMARY); SDL_free(pClipBoard); SDL_free(pPrimarySelection); } break; case SDL_MOUSEMOTION: { if ( m_bApplicationGrabbed ) { if ( g_bWindowFocused ) { wlserver_lock(); wlserver_mousemotion( event.motion.xrel, event.motion.yrel, fake_timestamp ); wlserver_unlock(); } } else { wlserver_lock(); wlserver_touchmotion( event.motion.x / float(g_nOutputWidthPts), event.motion.y / float(g_nOutputHeightPts), 0, fake_timestamp ); wlserver_unlock(); } } break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: { wlserver_lock(); wlserver_mousebutton( SDLButtonToLinuxButton( event.button.button ), event.button.state == SDL_PRESSED, fake_timestamp ); wlserver_unlock(); } break; case SDL_MOUSEWHEEL: { wlserver_lock(); wlserver_mousewheel( -event.wheel.x, -event.wheel.y, fake_timestamp ); wlserver_unlock(); } break; case SDL_FINGERMOTION: { wlserver_lock(); wlserver_touchmotion( event.tfinger.x, event.tfinger.y, event.tfinger.fingerId, fake_timestamp ); wlserver_unlock(); } break; case SDL_FINGERDOWN: { wlserver_lock(); wlserver_touchdown( event.tfinger.x, event.tfinger.y, event.tfinger.fingerId, fake_timestamp ); wlserver_unlock(); } break; case SDL_FINGERUP: { wlserver_lock(); wlserver_touchup( event.tfinger.fingerId, fake_timestamp ); wlserver_unlock(); } break; case SDL_KEYDOWN: { // If this keydown event is super + one of the shortcut keys, consume the keydown event, since the corresponding keyup // event will be consumed by the next case statement when the user releases the key if ( event.key.keysym.mod & KMOD_LGUI ) { uint32_t key = SDLScancodeToLinuxKey( event.key.keysym.scancode ); const uint32_t shortcutKeys[] = {KEY_F, KEY_N, KEY_B, KEY_U, KEY_Y, KEY_I, KEY_O, KEY_S, KEY_G}; const bool isShortcutKey = std::find(std::begin(shortcutKeys), std::end(shortcutKeys), key) != std::end(shortcutKeys); if ( isShortcutKey ) { break; } } } [[fallthrough]]; case SDL_KEYUP: { uint32_t key = SDLScancodeToLinuxKey( event.key.keysym.scancode ); if ( event.type == SDL_KEYUP && ( event.key.keysym.mod & KMOD_LGUI ) ) { bool handled = true; switch ( key ) { case KEY_F: g_bFullscreen = !g_bFullscreen; SDL_SetWindowFullscreen( m_Connector.GetSDLWindow(), g_bFullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0 ); break; case KEY_N: g_wantedUpscaleFilter = GamescopeUpscaleFilter::PIXEL; break; case KEY_B: g_wantedUpscaleFilter = GamescopeUpscaleFilter::LINEAR; break; case KEY_U: g_wantedUpscaleFilter = (g_wantedUpscaleFilter == GamescopeUpscaleFilter::FSR) ? GamescopeUpscaleFilter::LINEAR : GamescopeUpscaleFilter::FSR; break; case KEY_Y: g_wantedUpscaleFilter = (g_wantedUpscaleFilter == GamescopeUpscaleFilter::NIS) ? GamescopeUpscaleFilter::LINEAR : GamescopeUpscaleFilter::NIS; break; case KEY_I: g_upscaleFilterSharpness = std::min(20, g_upscaleFilterSharpness + 1); break; case KEY_O: g_upscaleFilterSharpness = std::max(0, g_upscaleFilterSharpness - 1); break; case KEY_S: gamescope::CScreenshotManager::Get().TakeScreenshot( true ); break; case KEY_G: g_bGrabbed = !g_bGrabbed; SDL_SetWindowKeyboardGrab( m_Connector.GetSDLWindow(), g_bGrabbed ? SDL_TRUE : SDL_FALSE ); SDL_Event event; event.type = GetUserEventIndex( GAMESCOPE_SDL_EVENT_TITLE ); SDL_PushEvent( &event ); break; default: handled = false; } if ( handled ) { break; } } // On Wayland, clients handle key repetition if ( event.key.repeat ) break; wlserver_lock(); wlserver_key( key, event.type == SDL_KEYDOWN, fake_timestamp ); wlserver_unlock(); } break; case SDL_WINDOWEVENT: { switch( event.window.event ) { case SDL_WINDOWEVENT_CLOSE: raise( SIGTERM ); break; default: break; case SDL_WINDOWEVENT_SIZE_CHANGED: int width, height; SDL_GetWindowSize( m_Connector.GetSDLWindow(), &width, &height ); g_nOutputWidthPts = width; g_nOutputHeightPts = height; #if SDL_VERSION_ATLEAST(2, 26, 0) SDL_GetWindowSizeInPixels( m_Connector.GetSDLWindow(), &width, &height ); #endif g_nOutputWidth = width; g_nOutputHeight = height; [[fallthrough]]; case SDL_WINDOWEVENT_MOVED: case SDL_WINDOWEVENT_SHOWN: { int display_index = 0; SDL_DisplayMode mode = { SDL_PIXELFORMAT_UNKNOWN, 0, 0, 0, 0 }; display_index = SDL_GetWindowDisplayIndex( m_Connector.GetSDLWindow() ); if ( SDL_GetDesktopDisplayMode( display_index, &mode ) == 0 ) { g_nOutputRefresh = ConvertHztomHz( mode.refresh_rate ); } } break; case SDL_WINDOWEVENT_FOCUS_LOST: g_nNestedRefresh = g_nNestedUnfocusedRefresh; g_bWindowFocused = false; break; case SDL_WINDOWEVENT_FOCUS_GAINED: g_nNestedRefresh = g_nOldNestedRefresh; g_bWindowFocused = true; break; case SDL_WINDOWEVENT_EXPOSED: force_repaint(); break; } } break; default: { if ( event.type == GetUserEventIndex( GAMESCOPE_SDL_EVENT_VISIBLE ) ) { bool bVisible = m_bApplicationVisible; // If we are Steam Mode in nested, show the window // whenever we have had a first frame to match // what we do in embedded with Steam for testing // held commits, etc. if ( steamMode ) bVisible |= !g_bFirstFrame; if ( m_bShown != bVisible ) { m_bShown = bVisible; if ( m_bShown ) { SDL_ShowWindow( m_Connector.GetSDLWindow() ); } else { SDL_HideWindow( m_Connector.GetSDLWindow() ); } } } else if ( event.type == GetUserEventIndex( GAMESCOPE_SDL_EVENT_TITLE ) ) { std::shared_ptr pAppTitle = m_pApplicationTitle; std::string szTitle = pAppTitle ? *pAppTitle : "gamescope"; if ( g_bGrabbed ) szTitle += " (grabbed)"; SDL_SetWindowTitle( m_Connector.GetSDLWindow(), szTitle.c_str() ); szTitle = "Title: " + szTitle; SDL_SetHint(SDL_HINT_SCREENSAVER_INHIBIT_ACTIVITY_NAME, szTitle.c_str() ); SDL_DisableScreenSaver(); } else if ( event.type == GetUserEventIndex( GAMESCOPE_SDL_EVENT_ICON ) ) { std::shared_ptr> pIcon = m_pApplicationIcon; if ( m_pIconSurface ) { SDL_FreeSurface( m_pIconSurface ); m_pIconSurface = nullptr; } if ( pIcon && pIcon->size() >= 3 ) { const uint32_t uWidth = (*pIcon)[0]; const uint32_t uHeight = (*pIcon)[1]; m_pIconSurface = SDL_CreateRGBSurfaceFrom( &(*pIcon)[2], uWidth, uHeight, 32, uWidth * sizeof(uint32_t), 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000); } SDL_SetWindowIcon( m_Connector.GetSDLWindow(), m_pIconSurface ); } else if ( event.type == GetUserEventIndex( GAMESCOPE_SDL_EVENT_GRAB ) ) { SDL_SetRelativeMouseMode( m_bApplicationGrabbed ? SDL_TRUE : SDL_FALSE ); } else if ( event.type == GetUserEventIndex( GAMESCOPE_SDL_EVENT_CURSOR ) ) { std::shared_ptr pCursorInfo = m_pApplicationCursor; if ( m_pCursorSurface ) { SDL_FreeSurface( m_pCursorSurface ); m_pCursorSurface = nullptr; } if ( m_pCursor ) { SDL_FreeCursor( m_pCursor ); m_pCursor = nullptr; } if ( pCursorInfo ) { m_pCursorSurface = SDL_CreateRGBSurfaceFrom( pCursorInfo->pPixels.data(), pCursorInfo->uWidth, pCursorInfo->uHeight, 32, pCursorInfo->uWidth * sizeof(uint32_t), 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000); m_pCursor = SDL_CreateColorCursor( m_pCursorSurface, pCursorInfo->uXHotspot, pCursorInfo->uYHotspot ); } SDL_SetCursor( m_pCursor ); } } break; } } } uint32_t CSDLBackend::GetUserEventIndex( SDLCustomEvents eEvent ) const { return m_uUserEventIdBase + uint32_t( eEvent ); } void CSDLBackend::PushUserEvent( SDLCustomEvents eEvent ) { SDL_Event event = { .user = { .type = GetUserEventIndex( eEvent ), }, }; SDL_PushEvent( &event ); } ///////////////////////// // Backend Instantiator ///////////////////////// template <> bool IBackend::Set() { return Set( new CSDLBackend{} ); } } ValveSoftware-gamescope-eb620ab/src/Backends/WaylandBackend.cpp000066400000000000000000004031171502457270500246170ustar00rootroot00000000000000#include "backend.h" #include "rendervulkan.hpp" #include "wlserver.hpp" #include "vblankmanager.hpp" #include "steamcompmgr.hpp" #include "edid.h" #include "Utils/Defer.h" #include "Utils/Algorithm.h" #include "convar.h" #include "refresh_rate.h" #include "waitable.h" #include "Utils/TempFiles.h" #include #include #include #include #include #include #include #include #include #include "wlr_begin.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "wlr_end.hpp" #include "drm_include.h" #define WL_FRACTIONAL_SCALE_DENOMINATOR 120 extern int g_nPreferredOutputWidth; extern int g_nPreferredOutputHeight; extern bool g_bForceHDR10OutputDebug; extern bool g_bBorderlessOutputWindow; extern gamescope::ConVar cv_adaptive_sync; extern gamescope::ConVar cv_composite_force; extern bool g_bColorSliderInUse; extern bool fadingOut; extern std::string g_reshade_effect; using namespace std::literals; static LogScope xdg_log( "xdg_backend" ); static const char *GAMESCOPE_plane_tag = "gamescope-plane"; template auto CallWithAllButLast(Func pFunc, Args&&... args) { auto Forwarder = [&] (Tuple&& tuple, std::index_sequence) { return pFunc(std::get(std::forward(tuple))...); }; return Forwarder(std::forward_as_tuple(args...), std::make_index_sequence()); } static inline uint32_t WaylandScaleToPhysical( uint32_t pValue, uint32_t pFactor ) { return pValue * pFactor / WL_FRACTIONAL_SCALE_DENOMINATOR; } static inline uint32_t WaylandScaleToLogical( uint32_t pValue, uint32_t pFactor ) { return div_roundup( pValue * WL_FRACTIONAL_SCALE_DENOMINATOR, pFactor ); } static bool IsSurfacePlane( wl_surface *pSurface ) { // HACK: this probably should never be called with a null pointer, but it // was happening after a window was closed. return pSurface && (wl_proxy_get_tag( (wl_proxy *)pSurface ) == &GAMESCOPE_plane_tag); } #define WAYLAND_NULL() [] ( void *pData, Args... args ) { } #define WAYLAND_USERDATA_TO_THIS(type, name) [] ( void *pData, Args... args ) { type *pThing = (type *)pData; pThing->name( std::forward(args)... ); } // Libdecor puts its userdata ptr at the end... how fun! I shouldn't have spent so long writing this total atrocity to mankind. #define LIBDECOR_USERDATA_TO_THIS(type, name) [] ( Args... args ) { type *pThing = (type *)std::get(std::forward_as_tuple(args...)); CallWithAllButLast([&](Args2... args2){ pThing->name(std::forward(args2)...); }, std::forward(args)...); } extern gamescope::ConVar cv_hdr_enabled; namespace gamescope { extern std::shared_ptr GetX11HostCursor(); gamescope::ConVar cv_wayland_mouse_warp_without_keyboard_focus( "wayland_mouse_warp_without_keyboard_focus", true, "Should we only forward mouse warps to the app when we have keyboard focus?" ); gamescope::ConVar cv_wayland_mouse_relmotion_without_keyboard_focus( "wayland_mouse_relmotion_without_keyboard_focus", false, "Should we only forward mouse relative motion to the app when we have keyboard focus?" ); gamescope::ConVar cv_wayland_use_modifiers( "wayland_use_modifiers", true, "Use DMA-BUF modifiers?" ); gamescope::ConVar cv_wayland_hdr10_saturation_scale( "wayland_hdr10_saturation_scale", 1.0, "Saturation scale for HDR10 content by gamut expansion. 1.0 - 1.2 is a good range to play with." ); class CWaylandConnector; class CWaylandPlane; class CWaylandBackend; class CWaylandFb; struct WaylandPlaneState { wl_buffer *pBuffer; int32_t nDestX; int32_t nDestY; double flSrcX; double flSrcY; double flSrcWidth; double flSrcHeight; int32_t nDstWidth; int32_t nDstHeight; GamescopeAppTextureColorspace eColorspace; std::shared_ptr pHDRMetadata; bool bOpaque; uint32_t uFractionalScale; }; inline WaylandPlaneState ClipPlane( const WaylandPlaneState &state ) { int32_t nClippedDstWidth = std::min( g_nOutputWidth, state.nDstWidth + state.nDestX ) - state.nDestX; int32_t nClippedDstHeight = std::min( g_nOutputHeight, state.nDstHeight + state.nDestY ) - state.nDestY; double flClippedSrcWidth = state.flSrcWidth * ( nClippedDstWidth / double( state.nDstWidth ) ); double flClippedSrcHeight = state.flSrcHeight * ( nClippedDstHeight / double( state.nDstHeight ) ); WaylandPlaneState outState = state; outState.nDstWidth = nClippedDstWidth; outState.nDstHeight = nClippedDstHeight; outState.flSrcWidth = flClippedSrcWidth; outState.flSrcHeight = flClippedSrcHeight; return outState; } static int CreateShmBuffer( uint32_t uSize, void *pData ) { char szShmBufferPath[ PATH_MAX ]; int nFd = MakeTempFile( szShmBufferPath, k_szGamescopeTempShmTemplate ); if ( nFd < 0 ) return -1; if ( ftruncate( nFd, uSize ) < 0 ) { close( nFd ); return -1; } if ( pData ) { void *pMappedData = mmap( nullptr, uSize, PROT_READ | PROT_WRITE, MAP_SHARED, nFd, 0 ); if ( pMappedData == MAP_FAILED ) return -1; defer( munmap( pMappedData, uSize ) ); memcpy( pMappedData, pData, uSize ); } return nFd; } struct WaylandPlaneColorState { GamescopeAppTextureColorspace eColorspace; std::shared_ptr pHDRMetadata; bool operator ==( const WaylandPlaneColorState &other ) const = default; bool operator !=( const WaylandPlaneColorState &other ) const = default; }; class CWaylandPlane { public: CWaylandPlane( CWaylandConnector *pBackend ); ~CWaylandPlane(); bool Init( CWaylandPlane *pParent, CWaylandPlane *pSiblingBelow ); uint32_t GetScale() const; void Present( std::optional oState ); void Present( const FrameInfo_t::Layer_t *pLayer ); void CommitLibDecor( libdecor_configuration *pConfiguration ); void Commit(); wl_surface *GetSurface() const { return m_pSurface; } libdecor_frame *GetFrame() const { return m_pFrame; } xdg_toplevel *GetXdgToplevel() const; std::optional GetCurrentState() { std::unique_lock lock( m_PlaneStateLock ); return m_oCurrentPlaneState; } void UpdateVRRRefreshRate(); private: void Wayland_Surface_Enter( wl_surface *pSurface, wl_output *pOutput ); void Wayland_Surface_Leave( wl_surface *pSurface, wl_output *pOutput ); static const wl_surface_listener s_SurfaceListener; void LibDecor_Frame_Configure( libdecor_frame *pFrame, libdecor_configuration *pConfiguration ); void LibDecor_Frame_Close( libdecor_frame *pFrame ); void LibDecor_Frame_Commit( libdecor_frame *pFrame ); void LibDecor_Frame_DismissPopup( libdecor_frame *pFrame, const char *pSeatName ); static libdecor_frame_interface s_LibDecorFrameInterface; void Wayland_PresentationFeedback_SyncOutput( struct wp_presentation_feedback *pFeedback, wl_output *pOutput ); void Wayland_PresentationFeedback_Presented( struct wp_presentation_feedback *pFeedback, uint32_t uTVSecHi, uint32_t uTVSecLo, uint32_t uTVNSec, uint32_t uRefresh, uint32_t uSeqHi, uint32_t uSeqLo, uint32_t uFlags ); void Wayland_PresentationFeedback_Discarded( struct wp_presentation_feedback *pFeedback ); static const wp_presentation_feedback_listener s_PresentationFeedbackListener; void Wayland_FrogColorManagedSurface_PreferredMetadata( frog_color_managed_surface *pFrogSurface, uint32_t uTransferFunction, uint32_t uOutputDisplayPrimaryRedX, uint32_t uOutputDisplayPrimaryRedY, uint32_t uOutputDisplayPrimaryGreenX, uint32_t uOutputDisplayPrimaryGreenY, uint32_t uOutputDisplayPrimaryBlueX, uint32_t uOutputDisplayPrimaryBlueY, uint32_t uOutputWhitePointX, uint32_t uOutputWhitePointY, uint32_t uMaxLuminance, uint32_t uMinLuminance, uint32_t uMaxFullFrameLuminance ); static const frog_color_managed_surface_listener s_FrogColorManagedSurfaceListener; void Wayland_WPColorManagementSurfaceFeedback_PreferredChanged( wp_color_management_surface_feedback_v1 *pColorManagementSurface, unsigned int data ); static const wp_color_management_surface_feedback_v1_listener s_WPColorManagementSurfaceListener; void UpdateWPPreferredColorManagement(); void Wayland_WPImageDescriptionInfo_Done( wp_image_description_info_v1 *pImageDescInfo ); void Wayland_WPImageDescriptionInfo_ICCFile( wp_image_description_info_v1 *pImageDescInfo, int32_t nICCFd, uint32_t uICCSize ); void Wayland_WPImageDescriptionInfo_Primaries( wp_image_description_info_v1 *pImageDescInfo, int32_t nRedX, int32_t nRedY, int32_t nGreenX, int32_t nGreenY, int32_t nBlueX, int32_t nBlueY, int32_t nWhiteX, int32_t nWhiteY ); void Wayland_WPImageDescriptionInfo_PrimariesNamed( wp_image_description_info_v1 *pImageDescInfo, uint32_t uPrimaries ); void Wayland_WPImageDescriptionInfo_TFPower( wp_image_description_info_v1 *pImageDescInfo, uint32_t uExp); void Wayland_WPImageDescriptionInfo_TFNamed( wp_image_description_info_v1 *pImageDescInfo, uint32_t uTF); void Wayland_WPImageDescriptionInfo_Luminances( wp_image_description_info_v1 *pImageDescInfo, uint32_t uMinLum, uint32_t uMaxLum, uint32_t uRefLum ); void Wayland_WPImageDescriptionInfo_TargetPrimaries( wp_image_description_info_v1 *pImageDescInfo, int32_t nRedX, int32_t nRedY, int32_t nGreenX, int32_t nGreenY, int32_t nBlueX, int32_t nBlueY, int32_t nWhiteX, int32_t nWhiteY ); void Wayland_WPImageDescriptionInfo_TargetLuminance( wp_image_description_info_v1 *pImageDescInfo, uint32_t uMinLum, uint32_t uMaxLum ); void Wayland_WPImageDescriptionInfo_Target_MaxCLL( wp_image_description_info_v1 *pImageDescInfo, uint32_t uMaxCLL ); void Wayland_WPImageDescriptionInfo_Target_MaxFALL( wp_image_description_info_v1 *pImageDescInfo, uint32_t uMaxFALL ); static const wp_image_description_info_v1_listener s_ImageDescriptionInfoListener; void Wayland_FractionalScale_PreferredScale( wp_fractional_scale_v1 *pFractionalScale, uint32_t uScale ); static const wp_fractional_scale_v1_listener s_FractionalScaleListener; CWaylandConnector *m_pConnector = nullptr; CWaylandBackend *m_pBackend = nullptr; CWaylandPlane *m_pParent = nullptr; wl_surface *m_pSurface = nullptr; wp_viewport *m_pViewport = nullptr; frog_color_managed_surface *m_pFrogColorManagedSurface = nullptr; wp_color_management_surface_v1 *m_pWPColorManagedSurface = nullptr; wp_color_management_surface_feedback_v1 *m_pWPColorManagedSurfaceFeedback = nullptr; wp_fractional_scale_v1 *m_pFractionalScale = nullptr; wl_subsurface *m_pSubsurface = nullptr; libdecor_frame *m_pFrame = nullptr; libdecor_window_state m_eWindowState = LIBDECOR_WINDOW_STATE_NONE; std::vector m_pOutputs; bool m_bNeedsDecorCommit = false; uint32_t m_uFractionalScale = 120; bool m_bHasRecievedScale = false; std::optional m_ColorState{}; wp_image_description_v1 *m_pCurrentImageDescription = nullptr; std::mutex m_PlaneStateLock; std::optional m_oCurrentPlaneState; }; const wl_surface_listener CWaylandPlane::s_SurfaceListener = { .enter = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_Surface_Enter ), .leave = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_Surface_Leave ), .preferred_buffer_scale = WAYLAND_NULL(), .preferred_buffer_transform = WAYLAND_NULL(), }; // Can't be const, libdecor api bad... libdecor_frame_interface CWaylandPlane::s_LibDecorFrameInterface = { .configure = LIBDECOR_USERDATA_TO_THIS( CWaylandPlane, LibDecor_Frame_Configure ), .close = LIBDECOR_USERDATA_TO_THIS( CWaylandPlane, LibDecor_Frame_Close ), .commit = LIBDECOR_USERDATA_TO_THIS( CWaylandPlane, LibDecor_Frame_Commit ), .dismiss_popup = LIBDECOR_USERDATA_TO_THIS( CWaylandPlane, LibDecor_Frame_DismissPopup ), }; const wp_presentation_feedback_listener CWaylandPlane::s_PresentationFeedbackListener = { .sync_output = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_PresentationFeedback_SyncOutput ), .presented = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_PresentationFeedback_Presented ), .discarded = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_PresentationFeedback_Discarded ), }; const frog_color_managed_surface_listener CWaylandPlane::s_FrogColorManagedSurfaceListener = { .preferred_metadata = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_FrogColorManagedSurface_PreferredMetadata ), }; const wp_color_management_surface_feedback_v1_listener CWaylandPlane::s_WPColorManagementSurfaceListener = { .preferred_changed = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_WPColorManagementSurfaceFeedback_PreferredChanged ), }; const wp_image_description_info_v1_listener CWaylandPlane::s_ImageDescriptionInfoListener = { .done = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_WPImageDescriptionInfo_Done ), .icc_file = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_WPImageDescriptionInfo_ICCFile ), .primaries = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_WPImageDescriptionInfo_Primaries ), .primaries_named = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_WPImageDescriptionInfo_PrimariesNamed ), .tf_power = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_WPImageDescriptionInfo_TFPower ), .tf_named = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_WPImageDescriptionInfo_TFNamed ), .luminances = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_WPImageDescriptionInfo_Luminances ), .target_primaries = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_WPImageDescriptionInfo_TargetPrimaries ), .target_luminance = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_WPImageDescriptionInfo_TargetLuminance ), .target_max_cll = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_WPImageDescriptionInfo_Target_MaxCLL ), .target_max_fall = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_WPImageDescriptionInfo_Target_MaxFALL ), }; const wp_fractional_scale_v1_listener CWaylandPlane::s_FractionalScaleListener = { .preferred_scale = WAYLAND_USERDATA_TO_THIS( CWaylandPlane, Wayland_FractionalScale_PreferredScale ), }; enum WaylandModifierIndex { GAMESCOPE_WAYLAND_MOD_CTRL, GAMESCOPE_WAYLAND_MOD_SHIFT, GAMESCOPE_WAYLAND_MOD_ALT, GAMESCOPE_WAYLAND_MOD_META, // Super GAMESCOPE_WAYLAND_MOD_NUM, GAMESCOPE_WAYLAND_MOD_CAPS, GAMESCOPE_WAYLAND_MOD_COUNT, }; constexpr const char *WaylandModifierToXkbModifierName( WaylandModifierIndex eIndex ) { switch ( eIndex ) { case GAMESCOPE_WAYLAND_MOD_CTRL: return XKB_MOD_NAME_CTRL; case GAMESCOPE_WAYLAND_MOD_SHIFT: return XKB_MOD_NAME_SHIFT; case GAMESCOPE_WAYLAND_MOD_ALT: return XKB_MOD_NAME_ALT; case GAMESCOPE_WAYLAND_MOD_META: return XKB_MOD_NAME_LOGO; case GAMESCOPE_WAYLAND_MOD_NUM: return XKB_MOD_NAME_NUM; case GAMESCOPE_WAYLAND_MOD_CAPS: return XKB_MOD_NAME_CAPS; default: return "Unknown"; } } struct WaylandOutputInfo { int32_t nRefresh = 60; int32_t nScale = 1; }; class CWaylandConnector final : public CBaseBackendConnector, public INestedHints { public: CWaylandConnector( CWaylandBackend *pBackend, uint64_t ulVirtualConnectorKey ); virtual ~CWaylandConnector(); bool UpdateEdid(); bool Init(); void SetFullscreen( bool bFullscreen ); // Thread safe, can be called from the input thread. void UpdateFullscreenState(); bool HostCompositorIsCurrentlyVRR() const { return m_bHostCompositorIsCurrentlyVRR; } void SetHostCompositorIsCurrentlyVRR( bool bActive ) { m_bHostCompositorIsCurrentlyVRR = bActive; } bool CurrentDisplaySupportsVRR() const { return HostCompositorIsCurrentlyVRR(); } CWaylandBackend *GetBackend() const { return m_pBackend; } ///////////////////// // IBackendConnector ///////////////////// virtual int Present( const FrameInfo_t *pFrameInfo, bool bAsync ) override; virtual gamescope::GamescopeScreenType GetScreenType() const override; virtual GamescopePanelOrientation GetCurrentOrientation() const override; virtual bool SupportsHDR() const override; virtual bool IsHDRActive() const override; virtual const BackendConnectorHDRInfo &GetHDRInfo() const override; virtual bool IsVRRActive() const override; virtual std::span GetModes() const override; virtual bool SupportsVRR() const override; virtual std::span GetRawEDID() const override; virtual std::span GetValidDynamicRefreshRates() const override; virtual void GetNativeColorimetry( bool bHDR10, displaycolorimetry_t *displayColorimetry, EOTF *displayEOTF, displaycolorimetry_t *outputEncodingColorimetry, EOTF *outputEncodingEOTF ) const override; virtual const char *GetName() const override { return "Wayland"; } virtual const char *GetMake() const override { return "Gamescope"; } virtual const char *GetModel() const override { return "Virtual Display"; } virtual INestedHints *GetNestedHints() override { return this; } /////////////////// // INestedHints /////////////////// virtual void SetCursorImage( std::shared_ptr info ) override; virtual void SetRelativeMouseMode( bool bRelative ) override; virtual void SetVisible( bool bVisible ) override; virtual void SetTitle( std::shared_ptr szTitle ) override; virtual void SetIcon( std::shared_ptr> uIconPixels ) override; virtual void SetSelection( std::shared_ptr szContents, GamescopeSelection eSelection ) override; private: friend CWaylandPlane; BackendConnectorHDRInfo m_HDRInfo{}; displaycolorimetry_t m_DisplayColorimetry = displaycolorimetry_709; std::vector m_FakeEdid; CWaylandBackend *m_pBackend = nullptr; CWaylandPlane m_Planes[8]; bool m_bVisible = true; std::atomic m_bDesiredFullscreenState = { false }; bool m_bHostCompositorIsCurrentlyVRR = false; }; class CWaylandFb final : public CBaseBackendFb { public: CWaylandFb( CWaylandBackend *pBackend, wl_buffer *pHostBuffer ); ~CWaylandFb(); void OnCompositorAcquire(); void OnCompositorRelease(); wl_buffer *GetHostBuffer() const { return m_pHostBuffer; } wlr_buffer *GetClientBuffer() const { return m_pClientBuffer; } void Wayland_Buffer_Release( wl_buffer *pBuffer ); static const wl_buffer_listener s_BufferListener; private: CWaylandBackend *m_pBackend = nullptr; wl_buffer *m_pHostBuffer = nullptr; wlr_buffer *m_pClientBuffer = nullptr; bool m_bCompositorAcquired = false; }; const wl_buffer_listener CWaylandFb::s_BufferListener = { .release = WAYLAND_USERDATA_TO_THIS( CWaylandFb, Wayland_Buffer_Release ), }; class CWaylandInputThread { public: CWaylandInputThread(); ~CWaylandInputThread(); bool Init( CWaylandBackend *pBackend ); void ThreadFunc(); // This is only shared_ptr because it works nicely // with std::atomic, std::any and such and makes it very easy. // // It could be a std::unique_ptr if you added a mutex, // but I didn't seem it worth it. template std::shared_ptr QueueLaunder( T* pObject ); void SetRelativePointer( bool bRelative ); private: void HandleKey( uint32_t uKey, bool bPressed ); CWaylandBackend *m_pBackend = nullptr; CWaiter<4> m_Waiter; std::thread m_Thread; std::atomic m_bInitted = { false }; uint32_t m_uPointerEnterSerial = 0; bool m_bMouseEntered = false; bool m_bKeyboardEntered = false; wl_event_queue *m_pQueue = nullptr; std::shared_ptr m_pDisplayWrapper; wl_seat *m_pSeat = nullptr; wl_keyboard *m_pKeyboard = nullptr; wl_pointer *m_pPointer = nullptr; wl_touch *m_pTouch = nullptr; zwp_relative_pointer_manager_v1 *m_pRelativePointerManager = nullptr; uint32_t m_uFakeTimestamp = 0; xkb_context *m_pXkbContext = nullptr; xkb_keymap *m_pXkbKeymap = nullptr; uint32_t m_uKeyModifiers = 0; uint32_t m_uModMask[ GAMESCOPE_WAYLAND_MOD_COUNT ]; double m_flScrollAccum[2] = { 0.0, 0.0 }; uint32_t m_uAxisSource = WL_POINTER_AXIS_SOURCE_WHEEL; CWaylandPlane *m_pCurrentCursorPlane = nullptr; std::optional m_ofPendingCursorX; std::optional m_ofPendingCursorY; std::atomic> m_pRelativePointer = nullptr; std::unordered_set m_uScancodesHeld; void Wayland_Registry_Global( wl_registry *pRegistry, uint32_t uName, const char *pInterface, uint32_t uVersion ); static const wl_registry_listener s_RegistryListener; void Wayland_Seat_Capabilities( wl_seat *pSeat, uint32_t uCapabilities ); void Wayland_Seat_Name( wl_seat *pSeat, const char *pName ); static const wl_seat_listener s_SeatListener; void Wayland_Pointer_Enter( wl_pointer *pPointer, uint32_t uSerial, wl_surface *pSurface, wl_fixed_t fSurfaceX, wl_fixed_t fSurfaceY ); void Wayland_Pointer_Leave( wl_pointer *pPointer, uint32_t uSerial, wl_surface *pSurface ); void Wayland_Pointer_Motion( wl_pointer *pPointer, uint32_t uTime, wl_fixed_t fSurfaceX, wl_fixed_t fSurfaceY ); void Wayland_Pointer_Button( wl_pointer *pPointer, uint32_t uSerial, uint32_t uTime, uint32_t uButton, uint32_t uState ); void Wayland_Pointer_Axis( wl_pointer *pPointer, uint32_t uTime, uint32_t uAxis, wl_fixed_t fValue ); void Wayland_Pointer_Axis_Source( wl_pointer *pPointer, uint32_t uAxisSource ); void Wayland_Pointer_Axis_Stop( wl_pointer *pPointer, uint32_t uTime, uint32_t uAxis ); void Wayland_Pointer_Axis_Discrete( wl_pointer *pPointer, uint32_t uAxis, int32_t nDiscrete ); void Wayland_Pointer_Axis_Value120( wl_pointer *pPointer, uint32_t uAxis, int32_t nValue120 ); void Wayland_Pointer_Frame( wl_pointer *pPointer ); static const wl_pointer_listener s_PointerListener; void Wayland_Keyboard_Keymap( wl_keyboard *pKeyboard, uint32_t uFormat, int32_t nFd, uint32_t uSize ); void Wayland_Keyboard_Enter( wl_keyboard *pKeyboard, uint32_t uSerial, wl_surface *pSurface, wl_array *pKeys ); void Wayland_Keyboard_Leave( wl_keyboard *pKeyboard, uint32_t uSerial, wl_surface *pSurface ); void Wayland_Keyboard_Key( wl_keyboard *pKeyboard, uint32_t uSerial, uint32_t uTime, uint32_t uKey, uint32_t uState ); void Wayland_Keyboard_Modifiers( wl_keyboard *pKeyboard, uint32_t uSerial, uint32_t uModsDepressed, uint32_t uModsLatched, uint32_t uModsLocked, uint32_t uGroup ); void Wayland_Keyboard_RepeatInfo( wl_keyboard *pKeyboard, int32_t nRate, int32_t nDelay ); static const wl_keyboard_listener s_KeyboardListener; void Wayland_RelativePointer_RelativeMotion( zwp_relative_pointer_v1 *pRelativePointer, uint32_t uTimeHi, uint32_t uTimeLo, wl_fixed_t fDx, wl_fixed_t fDy, wl_fixed_t fDxUnaccel, wl_fixed_t fDyUnaccel ); static const zwp_relative_pointer_v1_listener s_RelativePointerListener; }; const wl_registry_listener CWaylandInputThread::s_RegistryListener = { .global = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Registry_Global ), .global_remove = WAYLAND_NULL(), }; const wl_seat_listener CWaylandInputThread::s_SeatListener = { .capabilities = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Seat_Capabilities ), .name = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Seat_Name ), }; const wl_pointer_listener CWaylandInputThread::s_PointerListener = { .enter = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Pointer_Enter ), .leave = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Pointer_Leave ), .motion = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Pointer_Motion ), .button = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Pointer_Button ), .axis = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Pointer_Axis ), .frame = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Pointer_Frame ), .axis_source = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Pointer_Axis_Source ), .axis_stop = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Pointer_Axis_Stop ), .axis_discrete = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Pointer_Axis_Discrete ), .axis_value120 = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Pointer_Axis_Value120 ), }; const wl_keyboard_listener CWaylandInputThread::s_KeyboardListener = { .keymap = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Keyboard_Keymap ), .enter = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Keyboard_Enter ), .leave = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Keyboard_Leave ), .key = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Keyboard_Key ), .modifiers = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Keyboard_Modifiers ), .repeat_info = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_Keyboard_RepeatInfo ), }; const zwp_relative_pointer_v1_listener CWaylandInputThread::s_RelativePointerListener = { .relative_motion = WAYLAND_USERDATA_TO_THIS( CWaylandInputThread, Wayland_RelativePointer_RelativeMotion ), }; class CWaylandBackend : public CBaseBackend { public: CWaylandBackend(); ///////////// // IBackend ///////////// virtual bool Init() override; virtual bool PostInit() override; virtual std::span GetInstanceExtensions() const override; virtual std::span GetDeviceExtensions( VkPhysicalDevice pVkPhysicalDevice ) const override; virtual VkImageLayout GetPresentLayout() const override; virtual void GetPreferredOutputFormat( uint32_t *pPrimaryPlaneFormat, uint32_t *pOverlayPlaneFormat ) const override; virtual bool ValidPhysicalDevice( VkPhysicalDevice pVkPhysicalDevice ) const override; virtual void DirtyState( bool bForce = false, bool bForceModeset = false ) override; virtual bool PollState() override; virtual std::shared_ptr CreateBackendBlob( const std::type_info &type, std::span data ) override; virtual OwningRc ImportDmabufToBackend( wlr_buffer *pBuffer, wlr_dmabuf_attributes *pDmaBuf ) override; virtual bool UsesModifiers() const override; virtual std::span GetSupportedModifiers( uint32_t uDrmFormat ) const override; virtual IBackendConnector *GetCurrentConnector() override; virtual IBackendConnector *GetConnector( GamescopeScreenType eScreenType ) override; virtual bool SupportsPlaneHardwareCursor() const override; virtual bool SupportsTearing() const override; virtual bool UsesVulkanSwapchain() const override; virtual bool IsSessionBased() const override; virtual bool SupportsExplicitSync() const override; virtual bool IsPaused() const override; virtual bool IsVisible() const override; virtual glm::uvec2 CursorSurfaceSize( glm::uvec2 uvecSize ) const override; virtual void HackUpdatePatchedEdid() override; virtual bool UsesVirtualConnectors() override; virtual std::shared_ptr CreateVirtualConnector( uint64_t ulVirtualConnectorKey ) override; protected: virtual void OnBackendBlobDestroyed( BackendBlob *pBlob ) override; wl_surface *CursorInfoToSurface( const std::shared_ptr &info ); bool SupportsColorManagement() const; void SetCursorImage( std::shared_ptr info ); void SetRelativeMouseMode( wl_surface *pSurface, bool bRelative ); void UpdateCursor(); friend CWaylandConnector; friend CWaylandPlane; friend CWaylandInputThread; friend CWaylandFb; wl_display *GetDisplay() const { return m_pDisplay; } wl_shm *GetShm() const { return m_pShm; } wl_compositor *GetCompositor() const { return m_pCompositor; } wp_single_pixel_buffer_manager_v1 *GetSinglePixelBufferManager() const { return m_pSinglePixelBufferManager; } wl_subcompositor *GetSubcompositor() const { return m_pSubcompositor; } zwp_linux_dmabuf_v1 *GetLinuxDmabuf() const { return m_pLinuxDmabuf; } xdg_wm_base *GetXDGWMBase() const { return m_pXdgWmBase; } wp_viewporter *GetViewporter() const { return m_pViewporter; } wp_presentation *GetPresentation() const { return m_pPresentation; } frog_color_management_factory_v1 *GetFrogColorManagementFactory() const { return m_pFrogColorMgmtFactory; } wp_color_manager_v1 *GetWPColorManager() const { return m_pWPColorManager; } wp_image_description_v1 *GetWPImageDescription( GamescopeAppTextureColorspace eColorspace ) const { return m_pWPImageDescriptions[ (uint32_t)eColorspace ]; } wp_fractional_scale_manager_v1 *GetFractionalScaleManager() const { return m_pFractionalScaleManager; } xdg_toplevel_icon_manager_v1 *GetToplevelIconManager() const { return m_pToplevelIconManager; } libdecor *GetLibDecor() const { return m_pLibDecor; } void UpdateFullscreenState(); WaylandOutputInfo *GetOutputInfo( wl_output *pOutput ) { auto iter = m_pOutputs.find( pOutput ); if ( iter == m_pOutputs.end() ) return nullptr; return &iter->second; } wl_region *GetFullRegion() const { return m_pFullRegion; } CWaylandFb *GetBlackFb() const { return m_BlackFb.get(); } void OnConnectorDestroyed( CWaylandConnector *pConnector ) { m_pFocusConnector.compare_exchange_strong( pConnector, nullptr ); } private: void Wayland_Registry_Global( wl_registry *pRegistry, uint32_t uName, const char *pInterface, uint32_t uVersion ); static const wl_registry_listener s_RegistryListener; void Wayland_Modifier( zwp_linux_dmabuf_v1 *pDmabuf, uint32_t uFormat, uint32_t uModifierHi, uint32_t uModifierLo ); void Wayland_Output_Geometry( wl_output *pOutput, int32_t nX, int32_t nY, int32_t nPhysicalWidth, int32_t nPhysicalHeight, int32_t nSubpixel, const char *pMake, const char *pModel, int32_t nTransform ); void Wayland_Output_Mode( wl_output *pOutput, uint32_t uFlags, int32_t nWidth, int32_t nHeight, int32_t nRefresh ); void Wayland_Output_Done( wl_output *pOutput ); void Wayland_Output_Scale( wl_output *pOutput, int32_t nFactor ); void Wayland_Output_Name( wl_output *pOutput, const char *pName ); void Wayland_Output_Description( wl_output *pOutput, const char *pDescription ); static const wl_output_listener s_OutputListener; void Wayland_Seat_Capabilities( wl_seat *pSeat, uint32_t uCapabilities ); static const wl_seat_listener s_SeatListener; void Wayland_Pointer_Enter( wl_pointer *pPointer, uint32_t uSerial, wl_surface *pSurface, wl_fixed_t fSurfaceX, wl_fixed_t fSurfaceY ); void Wayland_Pointer_Leave( wl_pointer *pPointer, uint32_t uSerial, wl_surface *pSurface ); static const wl_pointer_listener s_PointerListener; void Wayland_Keyboard_Enter( wl_keyboard *pKeyboard, uint32_t uSerial, wl_surface *pSurface, wl_array *pKeys ); void Wayland_Keyboard_Leave( wl_keyboard *pKeyboard, uint32_t uSerial, wl_surface *pSurface ); static const wl_keyboard_listener s_KeyboardListener; void Wayland_WPColorManager_SupportedIntent( wp_color_manager_v1 *pWPColorManager, uint32_t uRenderIntent ); void Wayland_WPColorManager_SupportedFeature( wp_color_manager_v1 *pWPColorManager, uint32_t uFeature ); void Wayland_WPColorManager_SupportedTFNamed( wp_color_manager_v1 *pWPColorManager, uint32_t uTF ); void Wayland_WPColorManager_SupportedPrimariesNamed( wp_color_manager_v1 *pWPColorManager, uint32_t uPrimaries ); void Wayland_WPColorManager_ColorManagerDone( wp_color_manager_v1 *pWPColorManager ); static const wp_color_manager_v1_listener s_WPColorManagerListener; void Wayland_DataSource_Send( struct wl_data_source *pSource, const char *pMime, int nFd ); void Wayland_DataSource_Cancelled( struct wl_data_source *pSource ); static const wl_data_source_listener s_DataSourceListener; void Wayland_PrimarySelectionSource_Send( struct zwp_primary_selection_source_v1 *pSource, const char *pMime, int nFd ); void Wayland_PrimarySelectionSource_Cancelled( struct zwp_primary_selection_source_v1 *pSource ); static const zwp_primary_selection_source_v1_listener s_PrimarySelectionSourceListener; CWaylandInputThread m_InputThread; wl_display *m_pDisplay = nullptr; wl_shm *m_pShm = nullptr; wl_compositor *m_pCompositor = nullptr; wp_single_pixel_buffer_manager_v1 *m_pSinglePixelBufferManager = nullptr; wl_subcompositor *m_pSubcompositor = nullptr; zwp_linux_dmabuf_v1 *m_pLinuxDmabuf = nullptr; xdg_wm_base *m_pXdgWmBase = nullptr; wp_viewporter *m_pViewporter = nullptr; wl_region *m_pFullRegion = nullptr; Rc m_BlackFb; OwningRc m_pOwnedBlackFb; OwningRc m_pBlackTexture; wp_presentation *m_pPresentation = nullptr; frog_color_management_factory_v1 *m_pFrogColorMgmtFactory = nullptr; wp_color_manager_v1 *m_pWPColorManager = nullptr; wp_image_description_v1 *m_pWPImageDescriptions[ GamescopeAppTextureColorspace_Count ]{}; zwp_pointer_constraints_v1 *m_pPointerConstraints = nullptr; zwp_relative_pointer_manager_v1 *m_pRelativePointerManager = nullptr; wp_fractional_scale_manager_v1 *m_pFractionalScaleManager = nullptr; xdg_toplevel_icon_manager_v1 *m_pToplevelIconManager = nullptr; // TODO: Restructure and remove the need for this. std::atomic m_pFocusConnector; wl_data_device_manager *m_pDataDeviceManager = nullptr; wl_data_device *m_pDataDevice = nullptr; std::shared_ptr m_pClipboard = nullptr; zwp_primary_selection_device_manager_v1 *m_pPrimarySelectionDeviceManager = nullptr; zwp_primary_selection_device_v1 *m_pPrimarySelectionDevice = nullptr; std::shared_ptr m_pPrimarySelection = nullptr; struct { std::vector ePrimaries; std::vector eTransferFunctions; std::vector eRenderIntents; std::vector eFeatures; bool bSupportsGamescopeColorManagement = false; // Has everything we want and need? } m_WPColorManagerFeatures; std::unordered_map m_pOutputs; libdecor *m_pLibDecor = nullptr; wl_seat *m_pSeat = nullptr; wl_keyboard *m_pKeyboard = nullptr; wl_pointer *m_pPointer = nullptr; wl_touch *m_pTouch = nullptr; zwp_locked_pointer_v1 *m_pLockedPointer = nullptr; wl_surface *m_pLockedSurface = nullptr; zwp_relative_pointer_v1 *m_pRelativePointer = nullptr; bool m_bCanUseModifiers = false; std::unordered_map> m_FormatModifiers; std::unordered_map m_ImportedFbs; uint32_t m_uPointerEnterSerial = 0; bool m_bMouseEntered = false; uint32_t m_uKeyboardEnterSerial = 0; bool m_bKeyboardEntered = false; std::shared_ptr m_pCursorInfo; wl_surface *m_pCursorSurface = nullptr; std::shared_ptr m_pDefaultCursorInfo; wl_surface *m_pDefaultCursorSurface = nullptr; }; const wl_registry_listener CWaylandBackend::s_RegistryListener = { .global = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_Registry_Global ), .global_remove = WAYLAND_NULL(), }; const wl_output_listener CWaylandBackend::s_OutputListener = { .geometry = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_Output_Geometry ), .mode = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_Output_Mode ), .done = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_Output_Done ), .scale = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_Output_Scale ), .name = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_Output_Name ), .description = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_Output_Description ), }; const wl_seat_listener CWaylandBackend::s_SeatListener = { .capabilities = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_Seat_Capabilities ), .name = WAYLAND_NULL(), }; const wl_pointer_listener CWaylandBackend::s_PointerListener = { .enter = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_Pointer_Enter ), .leave = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_Pointer_Leave ), .motion = WAYLAND_NULL(), .button = WAYLAND_NULL(), .axis = WAYLAND_NULL(), .frame = WAYLAND_NULL(), .axis_source = WAYLAND_NULL(), .axis_stop = WAYLAND_NULL(), .axis_discrete = WAYLAND_NULL(), .axis_value120 = WAYLAND_NULL(), }; const wl_keyboard_listener CWaylandBackend::s_KeyboardListener = { .keymap = WAYLAND_NULL(), .enter = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_Keyboard_Enter ), .leave = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_Keyboard_Leave ), .key = WAYLAND_NULL(), .modifiers = WAYLAND_NULL(), .repeat_info = WAYLAND_NULL(), }; const wp_color_manager_v1_listener CWaylandBackend::s_WPColorManagerListener { .supported_intent = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_WPColorManager_SupportedIntent ), .supported_feature = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_WPColorManager_SupportedFeature ), .supported_tf_named = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_WPColorManager_SupportedTFNamed ), .supported_primaries_named = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_WPColorManager_SupportedPrimariesNamed ), .done = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_WPColorManager_ColorManagerDone ), }; const wl_data_source_listener CWaylandBackend::s_DataSourceListener = { .target = WAYLAND_NULL(), .send = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_DataSource_Send ), .cancelled = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_DataSource_Cancelled ), .dnd_drop_performed = WAYLAND_NULL(), .dnd_finished = WAYLAND_NULL(), .action = WAYLAND_NULL(), }; const zwp_primary_selection_source_v1_listener CWaylandBackend::s_PrimarySelectionSourceListener = { .send = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_PrimarySelectionSource_Send ), .cancelled = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_PrimarySelectionSource_Cancelled ), }; ////////////////// // CWaylandFb ////////////////// CWaylandFb::CWaylandFb( CWaylandBackend *pBackend, wl_buffer *pHostBuffer ) : CBaseBackendFb() , m_pBackend { pBackend } , m_pHostBuffer { pHostBuffer } { wl_buffer_add_listener( pHostBuffer, &s_BufferListener, this ); } CWaylandFb::~CWaylandFb() { // I own the pHostBuffer. wl_buffer_destroy( m_pHostBuffer ); m_pHostBuffer = nullptr; } void CWaylandFb::OnCompositorAcquire() { // If the compositor has acquired us, track that // and increment the ref count. if ( !m_bCompositorAcquired ) { m_bCompositorAcquired = true; IncRef(); } } void CWaylandFb::OnCompositorRelease() { // Compositor has released us, decrement rc. //assert( m_bCompositorAcquired ); if ( m_bCompositorAcquired ) { m_bCompositorAcquired = false; DecRef(); } else { xdg_log.errorf( "Compositor released us but we were not acquired. Oh no." ); } } void CWaylandFb::Wayland_Buffer_Release( wl_buffer *pBuffer ) { assert( m_pHostBuffer ); assert( m_pHostBuffer == pBuffer ); xdg_log.debugf( "buffer_release: %p", pBuffer ); OnCompositorRelease(); } ////////////////// // CWaylandConnector ////////////////// CWaylandConnector::CWaylandConnector( CWaylandBackend *pBackend, uint64_t ulVirtualConnectorKey ) : CBaseBackendConnector{ ulVirtualConnectorKey } , m_pBackend( pBackend ) , m_Planes{ this, this, this, this, this, this, this, this } { m_HDRInfo.bAlwaysPatchEdid = true; } CWaylandConnector::~CWaylandConnector() { m_pBackend->OnConnectorDestroyed( this ); } bool CWaylandConnector::UpdateEdid() { m_FakeEdid = GenerateSimpleEdid( g_nNestedWidth, g_nNestedHeight ); return true; } bool CWaylandConnector::Init() { for ( uint32_t i = 0; i < 8; i++ ) { bool bSuccess = m_Planes[i].Init( i == 0 ? nullptr : &m_Planes[0], i == 0 ? nullptr : &m_Planes[ i - 1 ] ); if ( !bSuccess ) return false; } if ( g_bFullscreen ) { m_bDesiredFullscreenState = true; g_bFullscreen = false; UpdateFullscreenState(); } UpdateEdid(); m_pBackend->HackUpdatePatchedEdid(); if ( g_bForceRelativeMouse ) this->SetRelativeMouseMode( true ); return true; } void CWaylandConnector::SetFullscreen( bool bFullscreen ) { m_bDesiredFullscreenState = bFullscreen; } void CWaylandConnector::UpdateFullscreenState() { if ( !m_bVisible ) g_bFullscreen = false; if ( m_bDesiredFullscreenState != g_bFullscreen && m_bVisible ) { if ( m_bDesiredFullscreenState ) libdecor_frame_set_fullscreen( m_Planes[0].GetFrame(), nullptr ); else libdecor_frame_unset_fullscreen( m_Planes[0].GetFrame() ); g_bFullscreen = m_bDesiredFullscreenState; } } int CWaylandConnector::Present( const FrameInfo_t *pFrameInfo, bool bAsync ) { UpdateFullscreenState(); bool bNeedsFullComposite = false; if ( !m_bVisible ) { uint32_t uCurrentPlane = 0; for ( int i = 0; i < 8 && uCurrentPlane < 8; i++ ) m_Planes[uCurrentPlane++].Present( nullptr ); } else { // TODO: Dedupe some of this composite check code between us and drm.cpp bool bLayer0ScreenSize = close_enough(pFrameInfo->layers[0].scale.x, 1.0f) && close_enough(pFrameInfo->layers[0].scale.y, 1.0f); bool bNeedsCompositeFromFilter = (g_upscaleFilter == GamescopeUpscaleFilter::NEAREST || g_upscaleFilter == GamescopeUpscaleFilter::PIXEL) && !bLayer0ScreenSize; bNeedsFullComposite |= cv_composite_force; bNeedsFullComposite |= pFrameInfo->useFSRLayer0; bNeedsFullComposite |= pFrameInfo->useNISLayer0; bNeedsFullComposite |= pFrameInfo->blurLayer0; bNeedsFullComposite |= bNeedsCompositeFromFilter; bNeedsFullComposite |= g_bColorSliderInUse; bNeedsFullComposite |= pFrameInfo->bFadingOut; bNeedsFullComposite |= !g_reshade_effect.empty(); if ( g_bOutputHDREnabled ) bNeedsFullComposite |= g_bHDRItmEnable; if ( !m_pBackend->SupportsColorManagement() ) bNeedsFullComposite |= ColorspaceIsHDR( pFrameInfo->layers[0].colorspace ); bNeedsFullComposite |= !!(g_uCompositeDebug & CompositeDebugFlag::Heatmap); if ( !bNeedsFullComposite ) { bool bNeedsBacking = true; if ( pFrameInfo->layerCount >= 1 ) { if ( pFrameInfo->layers[0].isScreenSize() && !pFrameInfo->layers[0].hasAlpha() ) bNeedsBacking = false; } uint32_t uCurrentPlane = 0; if ( bNeedsBacking ) { m_pBackend->GetBlackFb()->OnCompositorAcquire(); CWaylandPlane *pPlane = &m_Planes[uCurrentPlane++]; pPlane->Present( WaylandPlaneState { .pBuffer = m_pBackend->GetBlackFb()->GetHostBuffer(), .flSrcWidth = 1.0, .flSrcHeight = 1.0, .nDstWidth = int32_t( g_nOutputWidth ), .nDstHeight = int32_t( g_nOutputHeight ), .eColorspace = GAMESCOPE_APP_TEXTURE_COLORSPACE_PASSTHRU, .bOpaque = true, .uFractionalScale = pPlane->GetScale(), } ); } for ( int i = 0; i < 8 && uCurrentPlane < 8; i++ ) m_Planes[uCurrentPlane++].Present( i < pFrameInfo->layerCount ? &pFrameInfo->layers[i] : nullptr ); } else { std::optional oCompositeResult = vulkan_composite( (FrameInfo_t *)pFrameInfo, nullptr, false ); if ( !oCompositeResult ) { xdg_log.errorf( "vulkan_composite failed" ); return -EINVAL; } vulkan_wait( *oCompositeResult, true ); FrameInfo_t::Layer_t compositeLayer{}; compositeLayer.scale.x = 1.0; compositeLayer.scale.y = 1.0; compositeLayer.opacity = 1.0; compositeLayer.zpos = g_zposBase; compositeLayer.tex = vulkan_get_last_output_image( false, false ); compositeLayer.applyColorMgmt = false; compositeLayer.filter = GamescopeUpscaleFilter::NEAREST; compositeLayer.ctm = nullptr; compositeLayer.colorspace = pFrameInfo->outputEncodingEOTF == EOTF_PQ ? GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ : GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB; m_Planes[0].Present( &compositeLayer ); for ( int i = 1; i < 8; i++ ) m_Planes[i].Present( nullptr ); } } for ( int i = 7; i >= 0; i-- ) m_Planes[i].Commit(); wl_display_flush( m_pBackend->GetDisplay() ); GetVBlankTimer().UpdateWasCompositing( bNeedsFullComposite ); GetVBlankTimer().UpdateLastDrawTime( get_time_in_nanos() - g_SteamCompMgrVBlankTime.ulWakeupTime ); m_pBackend->PollState(); return 0; } GamescopeScreenType CWaylandConnector::GetScreenType() const { return gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL; } GamescopePanelOrientation CWaylandConnector::GetCurrentOrientation() const { return GAMESCOPE_PANEL_ORIENTATION_0; } bool CWaylandConnector::SupportsHDR() const { return GetHDRInfo().IsHDR10(); } bool CWaylandConnector::IsHDRActive() const { // XXX: blah return false; } const BackendConnectorHDRInfo &CWaylandConnector::GetHDRInfo() const { return m_HDRInfo; } bool CWaylandConnector::IsVRRActive() const { return cv_adaptive_sync && m_bHostCompositorIsCurrentlyVRR; } std::span CWaylandConnector::GetModes() const { return std::span{}; } bool CWaylandConnector::SupportsVRR() const { return CurrentDisplaySupportsVRR(); } std::span CWaylandConnector::GetRawEDID() const { return std::span{ m_FakeEdid.begin(), m_FakeEdid.end() }; } std::span CWaylandConnector::GetValidDynamicRefreshRates() const { return std::span{}; } void CWaylandConnector::GetNativeColorimetry( bool bHDR10, displaycolorimetry_t *displayColorimetry, EOTF *displayEOTF, displaycolorimetry_t *outputEncodingColorimetry, EOTF *outputEncodingEOTF ) const { *displayColorimetry = m_DisplayColorimetry; *displayEOTF = EOTF_Gamma22; if ( bHDR10 && GetHDRInfo().IsHDR10() ) { // For HDR10 output, expected content colorspace != native colorspace. *outputEncodingColorimetry = displaycolorimetry_2020; *outputEncodingEOTF = GetHDRInfo().eOutputEncodingEOTF; } else { // We always use default 'perceptual' intent, so // this should be correct for SDR content. *outputEncodingColorimetry = m_DisplayColorimetry; *outputEncodingEOTF = EOTF_Gamma22; } } void CWaylandConnector::SetCursorImage( std::shared_ptr info ) { m_pBackend->SetCursorImage( std::move( info ) ); } void CWaylandConnector::SetRelativeMouseMode( bool bRelative ) { // TODO: Do more tracking across multiple connectors, and activity here if we ever want to use this. m_pBackend->SetRelativeMouseMode( m_Planes[0].GetSurface(), bRelative ); } void CWaylandConnector::SetVisible( bool bVisible ) { if ( m_bVisible == bVisible ) return; m_bVisible = bVisible; force_repaint(); } void CWaylandConnector::SetTitle( std::shared_ptr pAppTitle ) { std::string szTitle = pAppTitle ? *pAppTitle : "gamescope"; if ( g_bGrabbed ) szTitle += " (grabbed)"; libdecor_frame_set_title( m_Planes[0].GetFrame(), szTitle.c_str() ); } void CWaylandConnector::SetIcon( std::shared_ptr> uIconPixels ) { if ( !m_pBackend->GetToplevelIconManager() ) return; if ( uIconPixels && uIconPixels->size() >= 3 ) { xdg_toplevel_icon_v1 *pIcon = xdg_toplevel_icon_manager_v1_create_icon( m_pBackend->GetToplevelIconManager() ); if ( !pIcon ) { xdg_log.errorf( "Failed to create xdg_toplevel_icon_v1" ); return; } defer( xdg_toplevel_icon_v1_destroy( pIcon ) ); const uint32_t uWidth = ( *uIconPixels )[0]; const uint32_t uHeight = ( *uIconPixels )[1]; const uint32_t uStride = uWidth * 4; const uint32_t uSize = uStride * uHeight; int32_t nFd = CreateShmBuffer( uSize, &( *uIconPixels )[2] ); if ( nFd < 0 ) { xdg_log.errorf( "Failed to create/map shm buffer" ); return; } defer( close( nFd ) ); wl_shm_pool *pPool = wl_shm_create_pool( m_pBackend->GetShm(), nFd, uSize ); defer( wl_shm_pool_destroy( pPool ) ); wl_buffer *pBuffer = wl_shm_pool_create_buffer( pPool, 0, uWidth, uHeight, uStride, WL_SHM_FORMAT_ARGB8888 ); defer( wl_buffer_destroy( pBuffer ) ); xdg_toplevel_icon_v1_add_buffer( pIcon, pBuffer, 1 ); xdg_toplevel_icon_manager_v1_set_icon( m_pBackend->GetToplevelIconManager(), m_Planes[0].GetXdgToplevel(), pIcon ); } else { xdg_toplevel_icon_manager_v1_set_icon( m_pBackend->GetToplevelIconManager(), m_Planes[0].GetXdgToplevel(), nullptr ); } } void CWaylandConnector::SetSelection( std::shared_ptr szContents, GamescopeSelection eSelection ) { if ( m_pBackend->m_pDataDeviceManager && !m_pBackend->m_pDataDevice ) m_pBackend->m_pDataDevice = wl_data_device_manager_get_data_device( m_pBackend->m_pDataDeviceManager, m_pBackend->m_pSeat ); if ( m_pBackend->m_pPrimarySelectionDeviceManager && !m_pBackend->m_pPrimarySelectionDevice ) m_pBackend->m_pPrimarySelectionDevice = zwp_primary_selection_device_manager_v1_get_device( m_pBackend->m_pPrimarySelectionDeviceManager, m_pBackend->m_pSeat ); if ( eSelection == GAMESCOPE_SELECTION_CLIPBOARD && m_pBackend->m_pDataDevice ) { m_pBackend->m_pClipboard = szContents; wl_data_source *source = wl_data_device_manager_create_data_source( m_pBackend->m_pDataDeviceManager ); wl_data_source_add_listener( source, &m_pBackend->s_DataSourceListener, m_pBackend ); wl_data_source_offer( source, "text/plain" ); wl_data_source_offer( source, "text/plain;charset=utf-8" ); wl_data_source_offer( source, "TEXT" ); wl_data_source_offer( source, "STRING" ); wl_data_source_offer( source, "UTF8_STRING" ); wl_data_device_set_selection( m_pBackend->m_pDataDevice, source, m_pBackend->m_uKeyboardEnterSerial ); } else if ( eSelection == GAMESCOPE_SELECTION_PRIMARY && m_pBackend->m_pPrimarySelectionDevice ) { m_pBackend->m_pPrimarySelection = szContents; zwp_primary_selection_source_v1 *source = zwp_primary_selection_device_manager_v1_create_source( m_pBackend->m_pPrimarySelectionDeviceManager ); zwp_primary_selection_source_v1_add_listener( source, &m_pBackend->s_PrimarySelectionSourceListener, m_pBackend ); zwp_primary_selection_source_v1_offer( source, "text/plain" ); zwp_primary_selection_source_v1_offer( source, "text/plain;charset=utf-8" ); zwp_primary_selection_source_v1_offer( source, "TEXT" ); zwp_primary_selection_source_v1_offer( source, "STRING" ); zwp_primary_selection_source_v1_offer( source, "UTF8_STRING" ); zwp_primary_selection_device_v1_set_selection( m_pBackend->m_pPrimarySelectionDevice, source, m_pBackend->m_uPointerEnterSerial ); } } ////////////////// // CWaylandPlane ////////////////// CWaylandPlane::CWaylandPlane( CWaylandConnector *pConnector ) : m_pConnector{ pConnector } , m_pBackend{ pConnector->GetBackend() } { } CWaylandPlane::~CWaylandPlane() { std::scoped_lock lock{ m_PlaneStateLock }; m_eWindowState = LIBDECOR_WINDOW_STATE_NONE; m_pOutputs.clear(); m_bNeedsDecorCommit = false; m_oCurrentPlaneState = std::nullopt; if ( m_pFrame ) libdecor_frame_unref( m_pFrame ); // Ew. if ( m_pSubsurface ) wl_subsurface_destroy( m_pSubsurface ); if ( m_pFractionalScale ) wp_fractional_scale_v1_destroy( m_pFractionalScale ); if ( m_pWPColorManagedSurface ) wp_color_management_surface_v1_destroy( m_pWPColorManagedSurface ); if ( m_pWPColorManagedSurfaceFeedback ) wp_color_management_surface_feedback_v1_destroy( m_pWPColorManagedSurfaceFeedback ); if ( m_pFrogColorManagedSurface ) frog_color_managed_surface_destroy( m_pFrogColorManagedSurface ); if ( m_pViewport ) wp_viewport_destroy( m_pViewport ); if ( m_pSurface ) wl_surface_destroy( m_pSurface ); } bool CWaylandPlane::Init( CWaylandPlane *pParent, CWaylandPlane *pSiblingBelow ) { m_pParent = pParent; m_pSurface = wl_compositor_create_surface( m_pBackend->GetCompositor() ); wl_proxy_set_tag( (wl_proxy *)m_pSurface, &GAMESCOPE_plane_tag ); wl_surface_set_user_data( m_pSurface, this ); wl_surface_add_listener( m_pSurface, &s_SurfaceListener, this ); m_pViewport = wp_viewporter_get_viewport( m_pBackend->GetViewporter(), m_pSurface ); if ( m_pBackend->GetWPColorManager() ) { m_pWPColorManagedSurface = wp_color_manager_v1_get_surface( m_pBackend->GetWPColorManager(), m_pSurface ); m_pWPColorManagedSurfaceFeedback = wp_color_manager_v1_get_surface_feedback( m_pBackend->GetWPColorManager(), m_pSurface ); // Only add the listener for the toplevel to avoid useless spam. if ( !pParent ) wp_color_management_surface_feedback_v1_add_listener( m_pWPColorManagedSurfaceFeedback, &s_WPColorManagementSurfaceListener, this ); UpdateWPPreferredColorManagement(); } else if ( m_pBackend->GetFrogColorManagementFactory() ) { m_pFrogColorManagedSurface = frog_color_management_factory_v1_get_color_managed_surface( m_pBackend->GetFrogColorManagementFactory(), m_pSurface ); // Only add the listener for the toplevel to avoid useless spam. if ( !pParent ) frog_color_managed_surface_add_listener( m_pFrogColorManagedSurface, &s_FrogColorManagedSurfaceListener, this ); } if ( m_pBackend->GetFractionalScaleManager() ) { m_pFractionalScale = wp_fractional_scale_manager_v1_get_fractional_scale( m_pBackend->GetFractionalScaleManager(), m_pSurface ); if ( !pParent ) wp_fractional_scale_v1_add_listener( m_pFractionalScale, &s_FractionalScaleListener, this ); } if ( !pParent ) { m_pFrame = libdecor_decorate( m_pBackend->GetLibDecor(), m_pSurface, &s_LibDecorFrameInterface, this ); libdecor_frame_set_title( m_pFrame, "Gamescope" ); libdecor_frame_set_app_id( m_pFrame, "gamescope" ); libdecor_frame_map( m_pFrame ); } else { m_pSubsurface = wl_subcompositor_get_subsurface( m_pBackend->GetSubcompositor(), m_pSurface, pParent->GetSurface() ); wl_subsurface_place_above( m_pSubsurface, pSiblingBelow->GetSurface() ); wl_subsurface_set_sync( m_pSubsurface ); } wl_surface_commit( m_pSurface ); wl_display_roundtrip( m_pBackend->GetDisplay() ); if ( m_pFrame ) libdecor_frame_set_visibility( m_pFrame, !g_bBorderlessOutputWindow ); return true; } uint32_t CWaylandPlane::GetScale() const { if ( m_pParent ) return m_pParent->GetScale(); return m_uFractionalScale; } void CWaylandPlane::Present( std::optional oState ) { { std::unique_lock lock( m_PlaneStateLock ); m_oCurrentPlaneState = oState; } if ( oState ) { assert( oState->pBuffer ); if ( m_pFrame ) { struct wp_presentation_feedback *pFeedback = wp_presentation_feedback( m_pBackend->GetPresentation(), m_pSurface ); wp_presentation_feedback_add_listener( pFeedback, &s_PresentationFeedbackListener, this ); } if ( m_pWPColorManagedSurface ) { WaylandPlaneColorState colorState = { .eColorspace = oState->eColorspace, .pHDRMetadata = oState->pHDRMetadata, }; if ( !m_ColorState || *m_ColorState != colorState ) { m_ColorState = colorState; if ( m_pCurrentImageDescription ) { wp_image_description_v1_destroy( m_pCurrentImageDescription ); m_pCurrentImageDescription = nullptr; } if ( oState->eColorspace == GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB ) { m_pCurrentImageDescription = wp_color_manager_v1_create_windows_scrgb( m_pBackend->GetWPColorManager() ); } else if ( oState->eColorspace == GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ ) { wp_image_description_creator_params_v1 *pParams = wp_color_manager_v1_create_parametric_creator( m_pBackend->GetWPColorManager() ); double flScale = cv_wayland_hdr10_saturation_scale; if ( close_enough( flScale, 1.0f ) ) { wp_image_description_creator_params_v1_set_primaries_named( pParams, WP_COLOR_MANAGER_V1_PRIMARIES_BT2020 ); } else { wp_image_description_creator_params_v1_set_primaries( pParams, (int32_t)(0.708 * flScale * 1'000'000.0), (int32_t)(0.292 / flScale * 1'000'000.0), (int32_t)(0.170 / flScale * 1'000'000.0), (int32_t)(0.797 * flScale * 1'000'000.0), (int32_t)(0.131 / flScale * 1'000'000.0), (int32_t)(0.046 / flScale * 1'000'000.0), (int32_t)(0.3127 * 1'000'000.0), (int32_t)(0.3290 * 1'000'000.0) ); } wp_image_description_creator_params_v1_set_tf_named( pParams, WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ ); if ( m_ColorState->pHDRMetadata ) { const hdr_metadata_infoframe *pInfoframe = &m_ColorState->pHDRMetadata->View().hdmi_metadata_type1; wp_image_description_creator_params_v1_set_mastering_display_primaries( pParams, // Rescale... (((int32_t)pInfoframe->display_primaries[0].x) * 1'000'000) / 0xC350, (((int32_t)pInfoframe->display_primaries[0].y) * 1'000'000) / 0xC350, (((int32_t)pInfoframe->display_primaries[1].x) * 1'000'000) / 0xC350, (((int32_t)pInfoframe->display_primaries[1].y) * 1'000'000) / 0xC350, (((int32_t)pInfoframe->display_primaries[2].x) * 1'000'000) / 0xC350, (((int32_t)pInfoframe->display_primaries[2].y) * 1'000'000) / 0xC350, (((int32_t)pInfoframe->white_point.x) * 1'000'000) / 0xC350, (((int32_t)pInfoframe->white_point.y) * 1'000'000) / 0xC350); wp_image_description_creator_params_v1_set_mastering_luminance( pParams, pInfoframe->min_display_mastering_luminance, pInfoframe->max_display_mastering_luminance ); wp_image_description_creator_params_v1_set_max_cll( pParams, pInfoframe->max_cll ); wp_image_description_creator_params_v1_set_max_fall( pParams, pInfoframe->max_fall ); } m_pCurrentImageDescription = wp_image_description_creator_params_v1_create( pParams ); } } if ( m_pCurrentImageDescription ) { wp_color_management_surface_v1_set_image_description( m_pWPColorManagedSurface, m_pCurrentImageDescription, WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL ); } else { wp_color_management_surface_v1_unset_image_description( m_pWPColorManagedSurface ); } } else if ( m_pFrogColorManagedSurface ) { frog_color_managed_surface_set_render_intent( m_pFrogColorManagedSurface, FROG_COLOR_MANAGED_SURFACE_RENDER_INTENT_PERCEPTUAL ); switch ( oState->eColorspace ) { default: case GAMESCOPE_APP_TEXTURE_COLORSPACE_PASSTHRU: frog_color_managed_surface_set_known_container_color_volume( m_pFrogColorManagedSurface, FROG_COLOR_MANAGED_SURFACE_PRIMARIES_UNDEFINED ); frog_color_managed_surface_set_known_container_color_volume( m_pFrogColorManagedSurface, FROG_COLOR_MANAGED_SURFACE_TRANSFER_FUNCTION_UNDEFINED ); break; case GAMESCOPE_APP_TEXTURE_COLORSPACE_LINEAR: case GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB: frog_color_managed_surface_set_known_container_color_volume( m_pFrogColorManagedSurface, FROG_COLOR_MANAGED_SURFACE_PRIMARIES_REC709 ); frog_color_managed_surface_set_known_transfer_function( m_pFrogColorManagedSurface, FROG_COLOR_MANAGED_SURFACE_TRANSFER_FUNCTION_GAMMA_22 ); break; case GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ: frog_color_managed_surface_set_known_container_color_volume( m_pFrogColorManagedSurface, FROG_COLOR_MANAGED_SURFACE_PRIMARIES_REC2020 ); frog_color_managed_surface_set_known_transfer_function( m_pFrogColorManagedSurface, FROG_COLOR_MANAGED_SURFACE_TRANSFER_FUNCTION_ST2084_PQ ); break; case GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB: frog_color_managed_surface_set_known_container_color_volume( m_pFrogColorManagedSurface, FROG_COLOR_MANAGED_SURFACE_PRIMARIES_REC709 ); frog_color_managed_surface_set_known_transfer_function( m_pFrogColorManagedSurface, FROG_COLOR_MANAGED_SURFACE_TRANSFER_FUNCTION_SCRGB_LINEAR ); break; } } // Fraction with denominator of 120 per. spec const uint32_t uScale = oState->uFractionalScale; wp_viewport_set_source( m_pViewport, wl_fixed_from_double( oState->flSrcX ), wl_fixed_from_double( oState->flSrcY ), wl_fixed_from_double( oState->flSrcWidth ), wl_fixed_from_double( oState->flSrcHeight ) ); wp_viewport_set_destination( m_pViewport, WaylandScaleToLogical( oState->nDstWidth, uScale ), WaylandScaleToLogical( oState->nDstHeight, uScale ) ); if ( m_pSubsurface ) { wl_subsurface_set_position( m_pSubsurface, WaylandScaleToLogical( oState->nDestX, uScale ), WaylandScaleToLogical( oState->nDestY, uScale ) ); } // The x/y here does nothing? Why? What is it for... // Use the subsurface set_position thing instead. wl_surface_attach( m_pSurface, oState->pBuffer, 0, 0 ); wl_surface_damage( m_pSurface, 0, 0, INT32_MAX, INT32_MAX ); wl_surface_set_opaque_region( m_pSurface, oState->bOpaque ? m_pBackend->GetFullRegion() : nullptr ); wl_surface_set_buffer_scale( m_pSurface, 1 ); } else { wl_surface_attach( m_pSurface, nullptr, 0, 0 ); wl_surface_damage( m_pSurface, 0, 0, INT32_MAX, INT32_MAX ); } } void CWaylandPlane::CommitLibDecor( libdecor_configuration *pConfiguration ) { int32_t uScale = GetScale(); libdecor_state *pState = libdecor_state_new( WaylandScaleToLogical( g_nOutputWidth, uScale ), WaylandScaleToLogical( g_nOutputHeight, uScale ) ); libdecor_frame_commit( m_pFrame, pState, pConfiguration ); libdecor_state_free( pState ); } void CWaylandPlane::Commit() { if ( m_bNeedsDecorCommit ) { CommitLibDecor( nullptr ); m_bNeedsDecorCommit = false; } wl_surface_commit( m_pSurface ); } xdg_toplevel *CWaylandPlane::GetXdgToplevel() const { if ( !m_pFrame ) return nullptr; return libdecor_frame_get_xdg_toplevel( m_pFrame ); } void CWaylandPlane::Present( const FrameInfo_t::Layer_t *pLayer ) { CWaylandFb *pWaylandFb = pLayer && pLayer->tex != nullptr ? static_cast( pLayer->tex->GetBackendFb() ) : nullptr; wl_buffer *pBuffer = pWaylandFb ? pWaylandFb->GetHostBuffer() : nullptr; if ( pBuffer ) { pWaylandFb->OnCompositorAcquire(); Present( ClipPlane( WaylandPlaneState { .pBuffer = pBuffer, .nDestX = int32_t( -pLayer->offset.x ), .nDestY = int32_t( -pLayer->offset.y ), .flSrcX = 0.0, .flSrcY = 0.0, .flSrcWidth = double( pLayer->tex->width() ), .flSrcHeight = double( pLayer->tex->height() ), .nDstWidth = int32_t( ceil( pLayer->tex->width() / double( pLayer->scale.x ) ) ), .nDstHeight = int32_t( ceil( pLayer->tex->height() / double( pLayer->scale.y ) ) ), .eColorspace = pLayer->colorspace, .pHDRMetadata = pLayer->hdr_metadata_blob, .bOpaque = pLayer->zpos == g_zposBase, .uFractionalScale = GetScale(), } ) ); } else { Present( std::nullopt ); } } void CWaylandPlane::UpdateVRRRefreshRate() { if ( m_pParent ) return; if ( !m_pConnector->HostCompositorIsCurrentlyVRR() ) return; if ( m_pOutputs.empty() ) return; int32_t nLargestRefreshRateMhz = 0; for ( wl_output *pOutput : m_pOutputs ) { WaylandOutputInfo *pOutputInfo = m_pBackend->GetOutputInfo( pOutput ); if ( !pOutputInfo ) continue; nLargestRefreshRateMhz = std::max( nLargestRefreshRateMhz, pOutputInfo->nRefresh ); } if ( nLargestRefreshRateMhz && nLargestRefreshRateMhz != g_nOutputRefresh ) { // TODO(strategy): We should pick the largest refresh rate. xdg_log.infof( "Changed refresh to: %.3fhz", ConvertmHzToHz( (float) nLargestRefreshRateMhz ) ); g_nOutputRefresh = nLargestRefreshRateMhz; } } void CWaylandPlane::Wayland_Surface_Enter( wl_surface *pSurface, wl_output *pOutput ) { if ( !IsSurfacePlane( pSurface ) ) return; m_pOutputs.emplace_back( pOutput ); UpdateVRRRefreshRate(); } void CWaylandPlane::Wayland_Surface_Leave( wl_surface *pSurface, wl_output *pOutput ) { if ( !IsSurfacePlane( pSurface ) ) return; std::erase( m_pOutputs, pOutput ); UpdateVRRRefreshRate(); } void CWaylandPlane::LibDecor_Frame_Configure( libdecor_frame *pFrame, libdecor_configuration *pConfiguration ) { if ( !libdecor_configuration_get_window_state( pConfiguration, &m_eWindowState ) ) m_eWindowState = LIBDECOR_WINDOW_STATE_NONE; int32_t uScale = GetScale(); int nWidth, nHeight; if ( !libdecor_configuration_get_content_size( pConfiguration, m_pFrame, &nWidth, &nHeight ) ) { // XXX(virtual connector): Move g_nOutputWidth etc to connector. // Right now we are doubling this up when we should not be. // // Which is causing problems. nWidth = WaylandScaleToLogical( g_nOutputWidth, uScale ); nHeight = WaylandScaleToLogical( g_nOutputHeight, uScale ); } g_nOutputWidth = WaylandScaleToPhysical( nWidth, uScale ); g_nOutputHeight = WaylandScaleToPhysical( nHeight, uScale ); CommitLibDecor( pConfiguration ); force_repaint(); } void CWaylandPlane::LibDecor_Frame_Close( libdecor_frame *pFrame ) { raise( SIGTERM ); } void CWaylandPlane::LibDecor_Frame_Commit( libdecor_frame *pFrame ) { m_bNeedsDecorCommit = true; force_repaint(); } void CWaylandPlane::LibDecor_Frame_DismissPopup( libdecor_frame *pFrame, const char *pSeatName ) { } void CWaylandPlane::Wayland_PresentationFeedback_SyncOutput( struct wp_presentation_feedback *pFeedback, wl_output *pOutput ) { } void CWaylandPlane::Wayland_PresentationFeedback_Presented( struct wp_presentation_feedback *pFeedback, uint32_t uTVSecHi, uint32_t uTVSecLo, uint32_t uTVNSec, uint32_t uRefreshCycle, uint32_t uSeqHi, uint32_t uSeqLo, uint32_t uFlags ) { uint64_t ulTime = ( ( ( uint64_t( uTVSecHi ) << 32ul ) | uTVSecLo ) * 1'000'000'000lu ) + ( uint64_t( uTVNSec ) ); if ( uRefreshCycle ) { int32_t nRefresh = RefreshCycleTomHz( uRefreshCycle ); if ( nRefresh && nRefresh != g_nOutputRefresh ) { xdg_log.infof( "Changed refresh to: %.3fhz", ConvertmHzToHz( (float) nRefresh ) ); g_nOutputRefresh = nRefresh; } m_pConnector->SetHostCompositorIsCurrentlyVRR( false ); } else { m_pConnector->SetHostCompositorIsCurrentlyVRR( true ); UpdateVRRRefreshRate(); } GetVBlankTimer().MarkVBlank( ulTime, true ); wp_presentation_feedback_destroy( pFeedback ); // Nudge so that steamcompmgr releases commits. nudge_steamcompmgr(); } void CWaylandPlane::Wayland_PresentationFeedback_Discarded( struct wp_presentation_feedback *pFeedback ) { wp_presentation_feedback_destroy( pFeedback ); // Nudge so that steamcompmgr releases commits. nudge_steamcompmgr(); } void CWaylandPlane::Wayland_FrogColorManagedSurface_PreferredMetadata( frog_color_managed_surface *pFrogSurface, uint32_t uTransferFunction, uint32_t uOutputDisplayPrimaryRedX, uint32_t uOutputDisplayPrimaryRedY, uint32_t uOutputDisplayPrimaryGreenX, uint32_t uOutputDisplayPrimaryGreenY, uint32_t uOutputDisplayPrimaryBlueX, uint32_t uOutputDisplayPrimaryBlueY, uint32_t uOutputWhitePointX, uint32_t uOutputWhitePointY, uint32_t uMaxLuminance, uint32_t uMinLuminance, uint32_t uMaxFullFrameLuminance ) { auto *pHDRInfo = &m_pConnector->m_HDRInfo; pHDRInfo->bExposeHDRSupport = ( cv_hdr_enabled && uTransferFunction == FROG_COLOR_MANAGED_SURFACE_TRANSFER_FUNCTION_ST2084_PQ ); pHDRInfo->eOutputEncodingEOTF = ( cv_hdr_enabled && uTransferFunction == FROG_COLOR_MANAGED_SURFACE_TRANSFER_FUNCTION_ST2084_PQ ) ? EOTF_PQ : EOTF_Gamma22; pHDRInfo->uMaxContentLightLevel = uMaxLuminance; pHDRInfo->uMaxFrameAverageLuminance = uMaxFullFrameLuminance; pHDRInfo->uMinContentLightLevel = uMinLuminance; auto *pDisplayColorimetry = &m_pConnector->m_DisplayColorimetry; pDisplayColorimetry->primaries.r = glm::vec2{ uOutputDisplayPrimaryRedX * 0.00002f, uOutputDisplayPrimaryRedY * 0.00002f }; pDisplayColorimetry->primaries.g = glm::vec2{ uOutputDisplayPrimaryGreenX * 0.00002f, uOutputDisplayPrimaryGreenY * 0.00002f }; pDisplayColorimetry->primaries.b = glm::vec2{ uOutputDisplayPrimaryBlueX * 0.00002f, uOutputDisplayPrimaryBlueY * 0.00002f }; pDisplayColorimetry->white = glm::vec2{ uOutputWhitePointX * 0.00002f, uOutputWhitePointY * 0.00002f }; xdg_log.infof( "PreferredMetadata: Red: %g %g, Green: %g %g, Blue: %g %g, White: %g %g, Max Luminance: %u nits, Min Luminance: %g nits, Max Full Frame Luminance: %u nits", uOutputDisplayPrimaryRedX * 0.00002, uOutputDisplayPrimaryRedY * 0.00002, uOutputDisplayPrimaryGreenX * 0.00002, uOutputDisplayPrimaryGreenY * 0.00002, uOutputDisplayPrimaryBlueX * 0.00002, uOutputDisplayPrimaryBlueY * 0.00002, uOutputWhitePointX * 0.00002, uOutputWhitePointY * 0.00002, uint32_t( uMaxLuminance ), uMinLuminance * 0.0001, uint32_t( uMaxFullFrameLuminance ) ); } // void CWaylandPlane::Wayland_WPColorManagementSurfaceFeedback_PreferredChanged( wp_color_management_surface_feedback_v1 *pColorManagementSurface, unsigned int data) { UpdateWPPreferredColorManagement(); } void CWaylandPlane::UpdateWPPreferredColorManagement() { if ( m_pParent ) return; wp_image_description_v1 *pImageDescription = wp_color_management_surface_feedback_v1_get_preferred( m_pWPColorManagedSurfaceFeedback ); wp_image_description_info_v1 *pImageDescInfo = wp_image_description_v1_get_information( pImageDescription ); wp_image_description_info_v1_add_listener( pImageDescInfo, &s_ImageDescriptionInfoListener, this ); wl_display_roundtrip( m_pBackend->GetDisplay() ); wp_image_description_info_v1_destroy( pImageDescInfo ); wp_image_description_v1_destroy( pImageDescription ); } void CWaylandPlane::Wayland_WPImageDescriptionInfo_Done( wp_image_description_info_v1 *pImageDescInfo ) { } void CWaylandPlane::Wayland_WPImageDescriptionInfo_ICCFile( wp_image_description_info_v1 *pImageDescInfo, int32_t nICCFd, uint32_t uICCSize ) { if ( nICCFd >= 0 ) close( nICCFd ); } void CWaylandPlane::Wayland_WPImageDescriptionInfo_Primaries( wp_image_description_info_v1 *pImageDescInfo, int32_t nRedX, int32_t nRedY, int32_t nGreenX, int32_t nGreenY, int32_t nBlueX, int32_t nBlueY, int32_t nWhiteX, int32_t nWhiteY ) { } void CWaylandPlane::Wayland_WPImageDescriptionInfo_PrimariesNamed( wp_image_description_info_v1 *pImageDescInfo, uint32_t uPrimaries ) { } void CWaylandPlane::Wayland_WPImageDescriptionInfo_TFPower( wp_image_description_info_v1 *pImageDescInfo, uint32_t uExp) { } static const char *TFToString( uint32_t uTF ) { switch ( (wp_color_manager_v1_transfer_function) uTF ) { case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_BT1886: return "BT1886"; case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA22: return "GAMMA22"; case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA28: return "GAMMA28"; case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST240: return "ST240"; case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR: return "EXT_LINEAR"; case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_LOG_100: return "LOG_100"; case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_LOG_316: return "LOG_316"; case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_XVYCC: return "XVYCC"; case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB: return "SRGB"; case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB: return "EXT_SRGB"; case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ: return "ST2084_PQ"; case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST428: return "ST428"; case WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG: return "HLG"; default: return "Unknown"; } } void CWaylandPlane::Wayland_WPImageDescriptionInfo_TFNamed( wp_image_description_info_v1 *pImageDescInfo, uint32_t uTF) { auto *pHDRInfo = &m_pConnector->m_HDRInfo; pHDRInfo->bExposeHDRSupport = ( cv_hdr_enabled && uTF == WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ ); pHDRInfo->eOutputEncodingEOTF = ( cv_hdr_enabled && uTF == WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ ) ? EOTF_PQ : EOTF_Gamma22; xdg_log.infof( "HDR INFO" ); xdg_log.infof( " cv_hdr_enabled: %s", cv_hdr_enabled ? "true" : "false" ); xdg_log.infof( " uTF: %s", TFToString( uTF ) ); xdg_log.infof( " bExposeHDRSupport: %s", pHDRInfo->bExposeHDRSupport ? "true" : "false" ); } void CWaylandPlane::Wayland_WPImageDescriptionInfo_Luminances( wp_image_description_info_v1 *pImageDescInfo, uint32_t uMinLum, uint32_t uMaxLum, uint32_t uRefLum ) { } void CWaylandPlane::Wayland_WPImageDescriptionInfo_TargetPrimaries( wp_image_description_info_v1 *pImageDescInfo, int32_t nRedX, int32_t nRedY, int32_t nGreenX, int32_t nGreenY, int32_t nBlueX, int32_t nBlueY, int32_t nWhiteX, int32_t nWhiteY ) { auto *pDisplayColorimetry = &m_pConnector->m_DisplayColorimetry; pDisplayColorimetry->primaries.r = glm::vec2{ nRedX / 1000000.0f, nRedY / 1000000.0f }; pDisplayColorimetry->primaries.g = glm::vec2{ nGreenX / 1000000.0f, nGreenY / 1000000.0f }; pDisplayColorimetry->primaries.b = glm::vec2{ nBlueX / 1000000.0f, nBlueY / 1000000.0f }; pDisplayColorimetry->white = glm::vec2{ nWhiteX / 1000000.0f, nWhiteY / 1000000.0f }; } void CWaylandPlane::Wayland_WPImageDescriptionInfo_TargetLuminance( wp_image_description_info_v1 *pImageDescInfo, uint32_t uMinLum, uint32_t uMaxLum ) { } void CWaylandPlane::Wayland_WPImageDescriptionInfo_Target_MaxCLL( wp_image_description_info_v1 *pImageDescInfo, uint32_t uMaxCLL ) { auto *pHDRInfo = &m_pConnector->m_HDRInfo; pHDRInfo->uMaxContentLightLevel = uMaxCLL; xdg_log.infof( "uMaxContentLightLevel: %u", uMaxCLL ); } void CWaylandPlane::Wayland_WPImageDescriptionInfo_Target_MaxFALL( wp_image_description_info_v1 *pImageDescInfo, uint32_t uMaxFALL ) { auto *pHDRInfo = &m_pConnector->m_HDRInfo; pHDRInfo->uMaxFrameAverageLuminance = uMaxFALL; } // void CWaylandPlane::Wayland_FractionalScale_PreferredScale( wp_fractional_scale_v1 *pFractionalScale, uint32_t uScale ) { bool bDirty = false; static uint32_t s_uGlobalFractionalScale = 120; if ( s_uGlobalFractionalScale != uScale ) { if ( m_bHasRecievedScale ) { g_nOutputWidth = ( g_nOutputWidth * uScale ) / m_uFractionalScale; g_nOutputHeight = ( g_nOutputHeight * uScale ) / m_uFractionalScale; } s_uGlobalFractionalScale = uScale; bDirty = true; } if ( m_uFractionalScale != uScale ) { m_uFractionalScale = uScale; bDirty = true; } m_bHasRecievedScale = true; if ( bDirty ) force_repaint(); } //////////////// // CWaylandBackend //////////////// // Not const... weird. static libdecor_interface s_LibDecorInterface = { .error = []( libdecor *pContext, libdecor_error eError, const char *pMessage ) { xdg_log.errorf( "libdecor: %s", pMessage ); }, }; CWaylandBackend::CWaylandBackend() { } bool CWaylandBackend::Init() { g_nOutputWidth = g_nPreferredOutputWidth; g_nOutputHeight = g_nPreferredOutputHeight; g_nOutputRefresh = g_nNestedRefresh; // TODO: Dedupe the init of this stuff, // maybe move it away from globals for multi-display... if ( g_nOutputHeight == 0 ) { if ( g_nOutputWidth != 0 ) { fprintf( stderr, "Cannot specify -W without -H\n" ); return false; } g_nOutputHeight = 720; } if ( g_nOutputWidth == 0 ) g_nOutputWidth = g_nOutputHeight * 16 / 9; if ( g_nOutputRefresh == 0 ) g_nOutputRefresh = ConvertHztomHz( 60 ); if ( !( m_pDisplay = wl_display_connect( nullptr ) ) ) { xdg_log.errorf( "Couldn't connect to Wayland display." ); return false; } wl_registry *pRegistry; if ( !( pRegistry = wl_display_get_registry( m_pDisplay ) ) ) { xdg_log.errorf( "Couldn't create Wayland registry." ); return false; } wl_registry_add_listener( pRegistry, &s_RegistryListener, this ); wl_display_roundtrip( m_pDisplay ); if ( !m_pCompositor || !m_pSubcompositor || !m_pXdgWmBase || !m_pLinuxDmabuf || !m_pViewporter || !m_pPresentation || !m_pRelativePointerManager || !m_pPointerConstraints || !m_pShm ) { xdg_log.errorf( "Couldn't create Wayland objects." ); return false; } // Grab stuff from any extra bindings/listeners we set up, eg. format/modifiers. wl_display_roundtrip( m_pDisplay ); wl_registry_destroy( pRegistry ); pRegistry = nullptr; if ( m_pWPColorManager ) { m_WPColorManagerFeatures.bSupportsGamescopeColorManagement = [this]() -> bool { // Features if ( !Algorithm::Contains( m_WPColorManagerFeatures.eFeatures, WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC ) ) return false; if ( !Algorithm::Contains( m_WPColorManagerFeatures.eFeatures, WP_COLOR_MANAGER_V1_FEATURE_SET_PRIMARIES ) ) return false; if ( !Algorithm::Contains( m_WPColorManagerFeatures.eFeatures, WP_COLOR_MANAGER_V1_FEATURE_SET_MASTERING_DISPLAY_PRIMARIES ) ) return false; if ( !Algorithm::Contains( m_WPColorManagerFeatures.eFeatures, WP_COLOR_MANAGER_V1_FEATURE_EXTENDED_TARGET_VOLUME ) ) return false; if ( !Algorithm::Contains( m_WPColorManagerFeatures.eFeatures, WP_COLOR_MANAGER_V1_FEATURE_SET_LUMINANCES ) ) return false; // Transfer Functions if ( !Algorithm::Contains( m_WPColorManagerFeatures.eTransferFunctions, WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB ) ) return false; if ( !Algorithm::Contains( m_WPColorManagerFeatures.eTransferFunctions, WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ ) ) return false; // TODO: Need scRGB // Primaries if ( !Algorithm::Contains( m_WPColorManagerFeatures.ePrimaries, WP_COLOR_MANAGER_V1_PRIMARIES_SRGB ) ) return false; if ( !Algorithm::Contains( m_WPColorManagerFeatures.ePrimaries, WP_COLOR_MANAGER_V1_PRIMARIES_BT2020 ) ) return false; return true; }(); } m_pLibDecor = libdecor_new( m_pDisplay, &s_LibDecorInterface ); if ( !m_pLibDecor ) { xdg_log.errorf( "Failed to init libdecor." ); return false; } if ( !vulkan_init( vulkan_get_instance(), VK_NULL_HANDLE ) ) { return false; } if ( !wlsession_init() ) { xdg_log.errorf( "Failed to initialize Wayland session" ); return false; } if ( !m_InputThread.Init( this ) ) { xdg_log.errorf( "Failed to initialize input thread" ); return false; } return true; } bool CWaylandBackend::PostInit() { m_pFullRegion = wl_compositor_create_region( m_pCompositor ); wl_region_add( m_pFullRegion, 0, 0, INT32_MAX, INT32_MAX ); if ( m_pSinglePixelBufferManager ) { wl_buffer *pBlackBuffer = wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer( m_pSinglePixelBufferManager, 0, 0, 0, ~0u ); m_pOwnedBlackFb = new CWaylandFb( this, pBlackBuffer ); m_BlackFb = m_pOwnedBlackFb.get(); } else { m_pBlackTexture = vulkan_create_flat_texture( 1, 1, 0, 0, 0, 255 ); if ( !m_pBlackTexture ) { xdg_log.errorf( "Failed to create dummy black texture." ); return false; } m_BlackFb = static_cast( m_pBlackTexture->GetBackendFb() ); } if ( m_BlackFb == nullptr ) { xdg_log.errorf( "Failed to create 1x1 black buffer." ); return false; } m_pDefaultCursorInfo = GetX11HostCursor(); m_pDefaultCursorSurface = CursorInfoToSurface( m_pDefaultCursorInfo ); return true; } std::span CWaylandBackend::GetInstanceExtensions() const { return std::span{}; } std::span CWaylandBackend::GetDeviceExtensions( VkPhysicalDevice pVkPhysicalDevice ) const { return std::span{}; } VkImageLayout CWaylandBackend::GetPresentLayout() const { return VK_IMAGE_LAYOUT_GENERAL; } void CWaylandBackend::GetPreferredOutputFormat( uint32_t *pPrimaryPlaneFormat, uint32_t *pOverlayPlaneFormat ) const { // Prefer opaque for composition on the Wayland backend. uint32_t u8BitFormat = DRM_FORMAT_INVALID; if ( SupportsFormat( DRM_FORMAT_XRGB8888 ) ) u8BitFormat = DRM_FORMAT_XRGB8888; else if ( SupportsFormat( DRM_FORMAT_XBGR8888 ) ) u8BitFormat = DRM_FORMAT_XBGR8888; else if ( SupportsFormat( DRM_FORMAT_ARGB8888 ) ) u8BitFormat = DRM_FORMAT_ARGB8888; else if ( SupportsFormat( DRM_FORMAT_ABGR8888 ) ) u8BitFormat = DRM_FORMAT_ABGR8888; uint32_t u10BitFormat = DRM_FORMAT_INVALID; if ( SupportsFormat( DRM_FORMAT_XBGR2101010 ) ) u10BitFormat = DRM_FORMAT_XBGR2101010; else if ( SupportsFormat( DRM_FORMAT_XRGB2101010 ) ) u10BitFormat = DRM_FORMAT_XRGB2101010; else if ( SupportsFormat( DRM_FORMAT_ABGR2101010 ) ) u10BitFormat = DRM_FORMAT_ABGR2101010; else if ( SupportsFormat( DRM_FORMAT_ARGB2101010 ) ) u10BitFormat = DRM_FORMAT_ARGB2101010; assert( u8BitFormat != DRM_FORMAT_INVALID ); *pPrimaryPlaneFormat = u10BitFormat != DRM_FORMAT_INVALID ? u10BitFormat : u8BitFormat; *pOverlayPlaneFormat = u8BitFormat; } bool CWaylandBackend::ValidPhysicalDevice( VkPhysicalDevice pVkPhysicalDevice ) const { return true; } void CWaylandBackend::DirtyState( bool bForce, bool bForceModeset ) { } bool CWaylandBackend::PollState() { wl_display_flush( m_pDisplay ); if ( wl_display_prepare_read( m_pDisplay ) == 0 ) { int nRet = 0; pollfd pollfd = { .fd = wl_display_get_fd( m_pDisplay ), .events = POLLIN, }; do { nRet = poll( &pollfd, 1, 0 ); } while ( nRet < 0 && ( errno == EINTR || errno == EAGAIN ) ); if ( nRet > 0 ) wl_display_read_events( m_pDisplay ); else wl_display_cancel_read( m_pDisplay ); } wl_display_dispatch_pending( m_pDisplay ); return false; } std::shared_ptr CWaylandBackend::CreateBackendBlob( const std::type_info &type, std::span data ) { return std::make_shared( data ); } OwningRc CWaylandBackend::ImportDmabufToBackend( wlr_buffer *pClientBuffer, wlr_dmabuf_attributes *pDmaBuf ) { zwp_linux_buffer_params_v1 *pBufferParams = zwp_linux_dmabuf_v1_create_params( m_pLinuxDmabuf ); if ( !pBufferParams ) { xdg_log.errorf( "Failed to create imported dmabuf params" ); return nullptr; } for ( int i = 0; i < pDmaBuf->n_planes; i++ ) { zwp_linux_buffer_params_v1_add( pBufferParams, pDmaBuf->fd[i], i, pDmaBuf->offset[i], pDmaBuf->stride[i], pDmaBuf->modifier >> 32, pDmaBuf->modifier & 0xffffffff); } wl_buffer *pImportedBuffer = zwp_linux_buffer_params_v1_create_immed( pBufferParams, pDmaBuf->width, pDmaBuf->height, pDmaBuf->format, 0u ); if ( !pImportedBuffer ) { xdg_log.errorf( "Failed to import dmabuf" ); return nullptr; } zwp_linux_buffer_params_v1_destroy( pBufferParams ); return new CWaylandFb{ this, pImportedBuffer }; } bool CWaylandBackend::UsesModifiers() const { if ( !cv_wayland_use_modifiers ) return false; return m_bCanUseModifiers; } std::span CWaylandBackend::GetSupportedModifiers( uint32_t uDrmFormat ) const { auto iter = m_FormatModifiers.find( uDrmFormat ); if ( iter == m_FormatModifiers.end() ) return std::span{}; return std::span{ iter->second.begin(), iter->second.end() }; } IBackendConnector *CWaylandBackend::GetCurrentConnector() { return m_pFocusConnector; } IBackendConnector *CWaylandBackend::GetConnector( GamescopeScreenType eScreenType ) { if ( eScreenType == GAMESCOPE_SCREEN_TYPE_INTERNAL ) return GetCurrentConnector(); return nullptr; } bool CWaylandBackend::SupportsPlaneHardwareCursor() const { // We use the nested hints cursor stuff. // Not our own plane. return false; } bool CWaylandBackend::SupportsTearing() const { return false; } bool CWaylandBackend::UsesVulkanSwapchain() const { return false; } bool CWaylandBackend::IsSessionBased() const { return false; } bool CWaylandBackend::SupportsExplicitSync() const { return true; } bool CWaylandBackend::IsPaused() const { return false; } bool CWaylandBackend::IsVisible() const { return true; } glm::uvec2 CWaylandBackend::CursorSurfaceSize( glm::uvec2 uvecSize ) const { return uvecSize; } void CWaylandBackend::HackUpdatePatchedEdid() { if ( !GetCurrentConnector() ) return; // XXX: We should do this a better way that handles per-window and appid stuff // down the line if ( cv_hdr_enabled && GetCurrentConnector()->GetHDRInfo().bExposeHDRSupport ) { setenv( "DXVK_HDR", "1", true ); } else { setenv( "DXVK_HDR", "0", true ); } WritePatchedEdid( GetCurrentConnector()->GetRawEDID(), GetCurrentConnector()->GetHDRInfo(), false ); } bool CWaylandBackend::UsesVirtualConnectors() { return true; } std::shared_ptr CWaylandBackend::CreateVirtualConnector( uint64_t ulVirtualConnectorKey ) { std::shared_ptr pConnector = std::make_shared( this, ulVirtualConnectorKey ); m_pFocusConnector = pConnector.get(); if ( !pConnector->Init() ) { return nullptr; } return pConnector; } /////////////////// // INestedHints /////////////////// void CWaylandBackend::OnBackendBlobDestroyed( BackendBlob *pBlob ) { // Do nothing. } wl_surface *CWaylandBackend::CursorInfoToSurface( const std::shared_ptr &info ) { if ( !info ) return nullptr; uint32_t uStride = info->uWidth * 4; uint32_t uSize = uStride * info->uHeight; int32_t nFd = CreateShmBuffer( uSize, info->pPixels.data() ); if ( nFd < 0 ) return nullptr; defer( close( nFd ) ); wl_shm_pool *pPool = wl_shm_create_pool( m_pShm, nFd, uSize ); defer( wl_shm_pool_destroy( pPool ) ); wl_buffer *pBuffer = wl_shm_pool_create_buffer( pPool, 0, info->uWidth, info->uHeight, uStride, WL_SHM_FORMAT_ARGB8888 ); defer( wl_buffer_destroy( pBuffer ) ); wl_surface *pCursorSurface = wl_compositor_create_surface( m_pCompositor ); wl_surface_attach( pCursorSurface, pBuffer, 0, 0 ); wl_surface_damage( pCursorSurface, 0, 0, INT32_MAX, INT32_MAX ); wl_surface_commit( pCursorSurface ); return pCursorSurface; } bool CWaylandBackend::SupportsColorManagement() const { return m_pFrogColorMgmtFactory != nullptr || ( m_pWPColorManager != nullptr && m_WPColorManagerFeatures.bSupportsGamescopeColorManagement ); } void CWaylandBackend::SetCursorImage( std::shared_ptr info ) { m_pCursorInfo = info; if ( m_pCursorSurface ) { wl_surface_destroy( m_pCursorSurface ); m_pCursorSurface = nullptr; } m_pCursorSurface = CursorInfoToSurface( info ); UpdateCursor(); } void CWaylandBackend::SetRelativeMouseMode( wl_surface *pSurface, bool bRelative ) { if ( !m_pPointer ) return; if ( !!bRelative != !!m_pLockedPointer || ( pSurface != m_pLockedSurface && bRelative ) ) { if ( m_pLockedPointer ) { assert( m_pRelativePointer ); zwp_locked_pointer_v1_destroy( m_pLockedPointer ); m_pLockedPointer = nullptr; zwp_relative_pointer_v1_destroy( m_pRelativePointer ); m_pRelativePointer = nullptr; } if ( bRelative ) { m_pLockedPointer = zwp_pointer_constraints_v1_lock_pointer( m_pPointerConstraints, pSurface, m_pPointer, nullptr, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT ); m_pRelativePointer = zwp_relative_pointer_manager_v1_get_relative_pointer( m_pRelativePointerManager, m_pPointer ); } m_InputThread.SetRelativePointer( bRelative ); UpdateCursor(); } } void CWaylandBackend::UpdateCursor() { bool bUseHostCursor = false; if ( cv_wayland_mouse_warp_without_keyboard_focus ) bUseHostCursor = m_pRelativePointer && !m_bKeyboardEntered && m_pDefaultCursorSurface; else bUseHostCursor = !m_bKeyboardEntered && m_pDefaultCursorSurface; if ( bUseHostCursor ) { wl_pointer_set_cursor( m_pPointer, m_uPointerEnterSerial, m_pDefaultCursorSurface, m_pDefaultCursorInfo->uXHotspot, m_pDefaultCursorInfo->uYHotspot ); } else { bool bHideCursor = m_pLockedPointer || !m_pCursorSurface; if ( bHideCursor ) wl_pointer_set_cursor( m_pPointer, m_uPointerEnterSerial, nullptr, 0, 0 ); else wl_pointer_set_cursor( m_pPointer, m_uPointerEnterSerial, m_pCursorSurface, m_pCursorInfo->uXHotspot, m_pCursorInfo->uYHotspot ); } } ///////////////////// // Wayland Callbacks ///////////////////// void CWaylandBackend::Wayland_Registry_Global( wl_registry *pRegistry, uint32_t uName, const char *pInterface, uint32_t uVersion ) { if ( !strcmp( pInterface, wl_compositor_interface.name ) && uVersion >= 4u ) { m_pCompositor = (wl_compositor *)wl_registry_bind( pRegistry, uName, &wl_compositor_interface, 4u ); } if ( !strcmp( pInterface, wp_single_pixel_buffer_manager_v1_interface.name ) ) { m_pSinglePixelBufferManager = (wp_single_pixel_buffer_manager_v1 *)wl_registry_bind( pRegistry, uName, &wp_single_pixel_buffer_manager_v1_interface, 1u ); } else if ( !strcmp( pInterface, wl_subcompositor_interface.name ) ) { m_pSubcompositor = (wl_subcompositor *)wl_registry_bind( pRegistry, uName, &wl_subcompositor_interface, 1u ); } else if ( !strcmp( pInterface, xdg_wm_base_interface.name ) && uVersion >= 1u ) { static constexpr xdg_wm_base_listener s_Listener = { .ping = []( void *pData, xdg_wm_base *pXdgWmBase, uint32_t uSerial ) { xdg_wm_base_pong( pXdgWmBase, uSerial ); } }; m_pXdgWmBase = (xdg_wm_base *)wl_registry_bind( pRegistry, uName, &xdg_wm_base_interface, 1u ); xdg_wm_base_add_listener( m_pXdgWmBase, &s_Listener, this ); } else if ( !strcmp( pInterface, zwp_linux_dmabuf_v1_interface.name ) && uVersion >= 3 ) { m_pLinuxDmabuf = (zwp_linux_dmabuf_v1 *)wl_registry_bind( pRegistry, uName, &zwp_linux_dmabuf_v1_interface, 3u ); static constexpr zwp_linux_dmabuf_v1_listener s_Listener = { .format = WAYLAND_NULL(), // Formats are also advertised by the modifier event, ignore them here. .modifier = WAYLAND_USERDATA_TO_THIS( CWaylandBackend, Wayland_Modifier ), }; zwp_linux_dmabuf_v1_add_listener( m_pLinuxDmabuf, &s_Listener, this ); } else if ( !strcmp( pInterface, wp_viewporter_interface.name ) ) { m_pViewporter = (wp_viewporter *)wl_registry_bind( pRegistry, uName, &wp_viewporter_interface, 1u ); } else if ( !strcmp( pInterface, wl_seat_interface.name ) && uVersion >= 8u ) { m_pSeat = (wl_seat *)wl_registry_bind( pRegistry, uName, &wl_seat_interface, 8u ); wl_seat_add_listener( m_pSeat, &s_SeatListener, this ); } else if ( !strcmp( pInterface, wp_presentation_interface.name ) ) { m_pPresentation = (wp_presentation *)wl_registry_bind( pRegistry, uName, &wp_presentation_interface, 1u ); } else if ( !strcmp( pInterface, wl_output_interface.name ) ) { wl_output *pOutput = (wl_output *)wl_registry_bind( pRegistry, uName, &wl_output_interface, 4u ); wl_output_add_listener( pOutput , &s_OutputListener, this ); m_pOutputs.emplace( std::make_pair( std::move( pOutput ), WaylandOutputInfo{} ) ); } else if ( !strcmp( pInterface, frog_color_management_factory_v1_interface.name ) ) { m_pFrogColorMgmtFactory = (frog_color_management_factory_v1 *)wl_registry_bind( pRegistry, uName, &frog_color_management_factory_v1_interface, 1u ); } else if ( !strcmp( pInterface, wp_color_manager_v1_interface.name ) ) { m_pWPColorManager = (wp_color_manager_v1 *)wl_registry_bind( pRegistry, uName, &wp_color_manager_v1_interface, 1u ); wp_color_manager_v1_add_listener( m_pWPColorManager, &s_WPColorManagerListener, this ); // HDR10. { wp_image_description_creator_params_v1 *pParams = wp_color_manager_v1_create_parametric_creator( m_pWPColorManager ); wp_image_description_creator_params_v1_set_primaries_named( pParams, WP_COLOR_MANAGER_V1_PRIMARIES_BT2020 ); wp_image_description_creator_params_v1_set_tf_named( pParams, WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ ); m_pWPImageDescriptions[ GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ ] = wp_image_description_creator_params_v1_create( pParams ); } // scRGB { m_pWPImageDescriptions[ GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB ] = wp_color_manager_v1_create_windows_scrgb( m_pWPColorManager ); } } else if ( !strcmp( pInterface, zwp_pointer_constraints_v1_interface.name ) ) { m_pPointerConstraints = (zwp_pointer_constraints_v1 *)wl_registry_bind( pRegistry, uName, &zwp_pointer_constraints_v1_interface, 1u ); } else if ( !strcmp( pInterface, zwp_relative_pointer_manager_v1_interface.name ) ) { m_pRelativePointerManager = (zwp_relative_pointer_manager_v1 *)wl_registry_bind( pRegistry, uName, &zwp_relative_pointer_manager_v1_interface, 1u ); } else if ( !strcmp( pInterface, wp_fractional_scale_manager_v1_interface.name ) ) { m_pFractionalScaleManager = (wp_fractional_scale_manager_v1 *)wl_registry_bind( pRegistry, uName, &wp_fractional_scale_manager_v1_interface, 1u ); } else if ( !strcmp( pInterface, wl_shm_interface.name ) ) { m_pShm = (wl_shm *)wl_registry_bind( pRegistry, uName, &wl_shm_interface, 1u ); } else if ( !strcmp( pInterface, xdg_toplevel_icon_manager_v1_interface.name ) ) { m_pToplevelIconManager = (xdg_toplevel_icon_manager_v1 *)wl_registry_bind( pRegistry, uName, &xdg_toplevel_icon_manager_v1_interface, 1u ); } else if ( !strcmp( pInterface, wl_data_device_manager_interface.name ) ) { m_pDataDeviceManager = (wl_data_device_manager *)wl_registry_bind( pRegistry, uName, &wl_data_device_manager_interface, 3u ); } else if ( !strcmp( pInterface, zwp_primary_selection_device_manager_v1_interface.name ) ) { m_pPrimarySelectionDeviceManager = (zwp_primary_selection_device_manager_v1 *)wl_registry_bind( pRegistry, uName, &zwp_primary_selection_device_manager_v1_interface, 1u ); } } void CWaylandBackend::Wayland_Modifier( zwp_linux_dmabuf_v1 *pDmabuf, uint32_t uFormat, uint32_t uModifierHi, uint32_t uModifierLo ) { uint64_t ulModifier = ( uint64_t( uModifierHi ) << 32 ) | uModifierLo; #if 0 const char *pszExtraModifierName = ""; if ( ulModifier == DRM_FORMAT_MOD_INVALID ) pszExtraModifierName = " (Invalid)"; if ( ulModifier == DRM_FORMAT_MOD_LINEAR ) pszExtraModifierName = " (Invalid)"; xdg_log.infof( "Modifier: %s (0x%" PRIX32 ") %lx%s", drmGetFormatName( uFormat ), uFormat, ulModifier, pszExtraModifierName ); #endif if ( ulModifier != DRM_FORMAT_MOD_INVALID ) m_bCanUseModifiers = true; m_FormatModifiers[uFormat].emplace_back( ulModifier ); } // Output void CWaylandBackend::Wayland_Output_Geometry( wl_output *pOutput, int32_t nX, int32_t nY, int32_t nPhysicalWidth, int32_t nPhysicalHeight, int32_t nSubpixel, const char *pMake, const char *pModel, int32_t nTransform ) { } void CWaylandBackend::Wayland_Output_Mode( wl_output *pOutput, uint32_t uFlags, int32_t nWidth, int32_t nHeight, int32_t nRefresh ) { m_pOutputs[ pOutput ].nRefresh = nRefresh; } void CWaylandBackend::Wayland_Output_Done( wl_output *pOutput ) { } void CWaylandBackend::Wayland_Output_Scale( wl_output *pOutput, int32_t nFactor ) { m_pOutputs[ pOutput ].nScale = nFactor; } void CWaylandBackend::Wayland_Output_Name( wl_output *pOutput, const char *pName ) { } void CWaylandBackend::Wayland_Output_Description( wl_output *pOutput, const char *pDescription ) { } // Seat void CWaylandBackend::Wayland_Seat_Capabilities( wl_seat *pSeat, uint32_t uCapabilities ) { if ( !!( uCapabilities & WL_SEAT_CAPABILITY_POINTER ) != !!m_pPointer ) { if ( m_pPointer ) { wl_pointer_release( m_pPointer ); m_pPointer = nullptr; } else { m_pPointer = wl_seat_get_pointer( m_pSeat ); wl_pointer_add_listener( m_pPointer, &s_PointerListener, this ); } } if ( !!( uCapabilities & WL_SEAT_CAPABILITY_KEYBOARD ) != !!m_pKeyboard ) { if ( m_pKeyboard ) { wl_keyboard_release( m_pKeyboard ); m_pKeyboard = nullptr; } else { m_pKeyboard = wl_seat_get_keyboard( m_pSeat ); wl_keyboard_add_listener( m_pKeyboard, &s_KeyboardListener, this ); } } } // Pointer void CWaylandBackend::Wayland_Pointer_Enter( wl_pointer *pPointer, uint32_t uSerial, wl_surface *pSurface, wl_fixed_t fSurfaceX, wl_fixed_t fSurfaceY ) { if ( !IsSurfacePlane( pSurface ) ) return; m_uPointerEnterSerial = uSerial; m_bMouseEntered = true; UpdateCursor(); } void CWaylandBackend::Wayland_Pointer_Leave( wl_pointer *pPointer, uint32_t uSerial, wl_surface *pSurface ) { if ( !IsSurfacePlane( pSurface ) ) return; m_bMouseEntered = false; } // Keyboard void CWaylandBackend::Wayland_Keyboard_Enter( wl_keyboard *pKeyboard, uint32_t uSerial, wl_surface *pSurface, wl_array *pKeys ) { if ( !IsSurfacePlane( pSurface ) ) return; m_uKeyboardEnterSerial = uSerial; m_bKeyboardEntered = true; UpdateCursor(); } void CWaylandBackend::Wayland_Keyboard_Leave( wl_keyboard *pKeyboard, uint32_t uSerial, wl_surface *pSurface ) { if ( !IsSurfacePlane( pSurface ) ) return; m_bKeyboardEntered = false; UpdateCursor(); } // WP Color Manager void CWaylandBackend::Wayland_WPColorManager_SupportedIntent( wp_color_manager_v1 *pWPColorManager, uint32_t uRenderIntent ) { m_WPColorManagerFeatures.eRenderIntents.push_back( static_cast( uRenderIntent ) ); } void CWaylandBackend::Wayland_WPColorManager_SupportedFeature( wp_color_manager_v1 *pWPColorManager, uint32_t uFeature ) { m_WPColorManagerFeatures.eFeatures.push_back( static_cast( uFeature ) ); } void CWaylandBackend::Wayland_WPColorManager_SupportedTFNamed( wp_color_manager_v1 *pWPColorManager, uint32_t uTF ) { m_WPColorManagerFeatures.eTransferFunctions.push_back( static_cast( uTF ) ); } void CWaylandBackend::Wayland_WPColorManager_SupportedPrimariesNamed( wp_color_manager_v1 *pWPColorManager, uint32_t uPrimaries ) { m_WPColorManagerFeatures.ePrimaries.push_back( static_cast( uPrimaries ) ); } void CWaylandBackend::Wayland_WPColorManager_ColorManagerDone( wp_color_manager_v1 *pWPColorManager ) { } // Data Source void CWaylandBackend::Wayland_DataSource_Send( struct wl_data_source *pSource, const char *pMime, int nFd ) { ssize_t len = m_pClipboard->length(); if ( write( nFd, m_pClipboard->c_str(), len ) != len ) xdg_log.infof( "Failed to write all %zd bytes to clipboard", len ); close( nFd ); } void CWaylandBackend::Wayland_DataSource_Cancelled( struct wl_data_source *pSource ) { wl_data_source_destroy( pSource ); } // Primary Selection Source void CWaylandBackend::Wayland_PrimarySelectionSource_Send( struct zwp_primary_selection_source_v1 *pSource, const char *pMime, int nFd ) { ssize_t len = m_pPrimarySelection->length(); if ( write( nFd, m_pPrimarySelection->c_str(), len ) != len ) xdg_log.infof( "Failed to write all %zd bytes to clipboard", len ); close( nFd ); } void CWaylandBackend::Wayland_PrimarySelectionSource_Cancelled( struct zwp_primary_selection_source_v1 *pSource) { zwp_primary_selection_source_v1_destroy( pSource ); } /////////////////////// // CWaylandInputThread /////////////////////// CWaylandInputThread::CWaylandInputThread() : m_Thread{ [this](){ this->ThreadFunc(); } } { } CWaylandInputThread::~CWaylandInputThread() { m_bInitted = true; m_bInitted.notify_all(); m_Waiter.Shutdown(); m_Thread.join(); } bool CWaylandInputThread::Init( CWaylandBackend *pBackend ) { m_pBackend = pBackend; if ( !( m_pXkbContext = xkb_context_new( XKB_CONTEXT_NO_FLAGS ) ) ) { xdg_log.errorf( "Couldn't create xkb context." ); return false; } if ( !( m_pQueue = wl_display_create_queue( m_pBackend->GetDisplay() ) ) ) { xdg_log.errorf( "Couldn't create input thread queue." ); return false; } if ( !( m_pDisplayWrapper = QueueLaunder( m_pBackend->GetDisplay() ) ) ) { xdg_log.errorf( "Couldn't create display proxy for input thread" ); return false; } wl_registry *pRegistry; if ( !( pRegistry = wl_display_get_registry( m_pDisplayWrapper.get() ) ) ) { xdg_log.errorf( "Couldn't create registry for input thread" ); return false; } wl_registry_add_listener( pRegistry, &s_RegistryListener, this ); wl_display_roundtrip_queue( pBackend->GetDisplay(), m_pQueue ); wl_display_roundtrip_queue( pBackend->GetDisplay(), m_pQueue ); wl_registry_destroy( pRegistry ); pRegistry = nullptr; if ( !m_pSeat || !m_pRelativePointerManager ) { xdg_log.errorf( "Couldn't create Wayland input objects." ); return false; } m_bInitted = true; m_bInitted.notify_all(); return true; } void CWaylandInputThread::ThreadFunc() { m_bInitted.wait( false ); if ( !m_Waiter.IsRunning() ) return; int nFD = wl_display_get_fd( m_pBackend->GetDisplay() ); if ( nFD < 0 ) { abort(); } CFunctionWaitable waitable( nFD ); m_Waiter.AddWaitable( &waitable ); int nRet = 0; while ( m_Waiter.IsRunning() ) { if ( ( nRet = wl_display_dispatch_queue_pending( m_pBackend->GetDisplay(), m_pQueue ) ) < 0 ) { abort(); } if ( ( nRet = wl_display_prepare_read_queue( m_pBackend->GetDisplay(), m_pQueue ) ) < 0 ) { if ( errno == EAGAIN || errno == EINTR ) continue; abort(); } if ( ( nRet = m_Waiter.PollEvents() ) <= 0 ) { wl_display_cancel_read( m_pBackend->GetDisplay() ); if ( nRet < 0 ) abort(); assert( nRet == 0 ); continue; } if ( ( nRet = wl_display_read_events( m_pBackend->GetDisplay() ) ) < 0 ) { abort(); } } } template std::shared_ptr CWaylandInputThread::QueueLaunder( T* pObject ) { if ( !pObject ) return nullptr; T *pObjectWrapper = (T*)wl_proxy_create_wrapper( (void *)pObject ); if ( !pObjectWrapper ) return nullptr; wl_proxy_set_queue( (wl_proxy *)pObjectWrapper, m_pQueue ); return std::shared_ptr{ pObjectWrapper, []( T *pThing ){ wl_proxy_wrapper_destroy( (void *)pThing ); } }; } void CWaylandInputThread::SetRelativePointer( bool bRelative ) { if ( bRelative == !!m_pRelativePointer.load() ) return; // This constructors/destructors the display's mutex, so should be safe to do across threads. if ( !bRelative ) { m_pRelativePointer = nullptr; } else { zwp_relative_pointer_v1 *pRelativePointer = zwp_relative_pointer_manager_v1_get_relative_pointer( m_pRelativePointerManager, m_pPointer ); m_pRelativePointer = std::shared_ptr{ pRelativePointer, []( zwp_relative_pointer_v1 *pObject ){ zwp_relative_pointer_v1_destroy( pObject ); } }; zwp_relative_pointer_v1_add_listener( pRelativePointer, &s_RelativePointerListener, this ); } } void CWaylandInputThread::HandleKey( uint32_t uKey, bool bPressed ) { if ( m_uKeyModifiers & m_uModMask[ GAMESCOPE_WAYLAND_MOD_META ] ) { switch ( uKey ) { case KEY_F: { if ( !bPressed ) { static_cast< CWaylandConnector * >( m_pBackend->GetCurrentConnector() )->SetFullscreen( !g_bFullscreen ); } return; } case KEY_N: { if ( !bPressed ) { g_wantedUpscaleFilter = GamescopeUpscaleFilter::PIXEL; } return; } case KEY_B: { if ( !bPressed ) { g_wantedUpscaleFilter = GamescopeUpscaleFilter::LINEAR; } return; } case KEY_U: { if ( !bPressed ) { g_wantedUpscaleFilter = ( g_wantedUpscaleFilter == GamescopeUpscaleFilter::FSR ) ? GamescopeUpscaleFilter::LINEAR : GamescopeUpscaleFilter::FSR; } return; } case KEY_Y: { if ( !bPressed ) { g_wantedUpscaleFilter = ( g_wantedUpscaleFilter == GamescopeUpscaleFilter::NIS ) ? GamescopeUpscaleFilter::LINEAR : GamescopeUpscaleFilter::NIS; } return; } case KEY_I: { if ( !bPressed ) { g_upscaleFilterSharpness = std::min( 20, g_upscaleFilterSharpness + 1 ); } return; } case KEY_O: { if ( !bPressed ) { g_upscaleFilterSharpness = std::max( 0, g_upscaleFilterSharpness - 1 ); } return; } case KEY_S: { if ( !bPressed ) { gamescope::CScreenshotManager::Get().TakeScreenshot( true ); } return; } default: break; } } wlserver_lock(); wlserver_key( uKey, bPressed, ++m_uFakeTimestamp ); wlserver_unlock(); } // Registry void CWaylandInputThread::Wayland_Registry_Global( wl_registry *pRegistry, uint32_t uName, const char *pInterface, uint32_t uVersion ) { if ( !strcmp( pInterface, wl_seat_interface.name ) && uVersion >= 8u ) { m_pSeat = (wl_seat *)wl_registry_bind( pRegistry, uName, &wl_seat_interface, 8u ); wl_seat_add_listener( m_pSeat, &s_SeatListener, this ); } else if ( !strcmp( pInterface, zwp_relative_pointer_manager_v1_interface.name ) ) { m_pRelativePointerManager = (zwp_relative_pointer_manager_v1 *)wl_registry_bind( pRegistry, uName, &zwp_relative_pointer_manager_v1_interface, 1u ); } } // Seat void CWaylandInputThread::Wayland_Seat_Capabilities( wl_seat *pSeat, uint32_t uCapabilities ) { if ( !!( uCapabilities & WL_SEAT_CAPABILITY_POINTER ) != !!m_pPointer ) { if ( m_pPointer ) { wl_pointer_release( m_pPointer ); m_pPointer = nullptr; } else { m_pPointer = wl_seat_get_pointer( m_pSeat ); wl_pointer_add_listener( m_pPointer, &s_PointerListener, this ); } } if ( !!( uCapabilities & WL_SEAT_CAPABILITY_KEYBOARD ) != !!m_pKeyboard ) { if ( m_pKeyboard ) { wl_keyboard_release( m_pKeyboard ); m_pKeyboard = nullptr; } else { m_pKeyboard = wl_seat_get_keyboard( m_pSeat ); wl_keyboard_add_listener( m_pKeyboard, &s_KeyboardListener, this ); } } } void CWaylandInputThread::Wayland_Seat_Name( wl_seat *pSeat, const char *pName ) { xdg_log.infof( "Seat name: %s", pName ); } // Pointer void CWaylandInputThread::Wayland_Pointer_Enter( wl_pointer *pPointer, uint32_t uSerial, wl_surface *pSurface, wl_fixed_t fSurfaceX, wl_fixed_t fSurfaceY ) { if ( !IsSurfacePlane( pSurface ) ) return; CWaylandPlane *pPlane = (CWaylandPlane *)wl_surface_get_user_data( pSurface ); if ( !pPlane ) return; m_pCurrentCursorPlane = pPlane; m_bMouseEntered = true; m_uPointerEnterSerial = uSerial; Wayland_Pointer_Motion( pPointer, 0, fSurfaceX, fSurfaceY ); } void CWaylandInputThread::Wayland_Pointer_Leave( wl_pointer *pPointer, uint32_t uSerial, wl_surface *pSurface ) { if ( !IsSurfacePlane( pSurface ) ) return; CWaylandPlane *pPlane = (CWaylandPlane *)wl_surface_get_user_data( pSurface ); if ( !pPlane ) return; if ( pPlane != m_pCurrentCursorPlane ) return; m_pCurrentCursorPlane = nullptr; m_bMouseEntered = false; } void CWaylandInputThread::Wayland_Pointer_Motion( wl_pointer *pPointer, uint32_t uTime, wl_fixed_t fSurfaceX, wl_fixed_t fSurfaceY ) { if ( m_pRelativePointer.load() != nullptr ) return; if ( !cv_wayland_mouse_warp_without_keyboard_focus && !m_bKeyboardEntered ) { // Don't do any motion/movement stuff if we don't have kb focus m_ofPendingCursorX = fSurfaceX; m_ofPendingCursorY = fSurfaceY; return; } if ( !m_pCurrentCursorPlane ) return; auto oState = m_pCurrentCursorPlane->GetCurrentState(); if ( !oState ) return; uint32_t uScale = oState->uFractionalScale; double flX = ( wl_fixed_to_double( fSurfaceX ) * uScale / 120.0 + oState->nDestX ) / g_nOutputWidth; double flY = ( wl_fixed_to_double( fSurfaceY ) * uScale / 120.0 + oState->nDestY ) / g_nOutputHeight; wlserver_lock(); wlserver_touchmotion( flX, flY, 0, ++m_uFakeTimestamp ); wlserver_unlock(); } void CWaylandInputThread::Wayland_Pointer_Button( wl_pointer *pPointer, uint32_t uSerial, uint32_t uTime, uint32_t uButton, uint32_t uState ) { // Don't do any motion/movement stuff if we don't have kb focus if ( !cv_wayland_mouse_warp_without_keyboard_focus && !m_bKeyboardEntered ) return; wlserver_lock(); wlserver_mousebutton( uButton, uState == WL_POINTER_BUTTON_STATE_PRESSED, ++m_uFakeTimestamp ); wlserver_unlock(); } void CWaylandInputThread::Wayland_Pointer_Axis( wl_pointer *pPointer, uint32_t uTime, uint32_t uAxis, wl_fixed_t fValue ) { } void CWaylandInputThread::Wayland_Pointer_Axis_Source( wl_pointer *pPointer, uint32_t uAxisSource ) { m_uAxisSource = uAxisSource; } void CWaylandInputThread::Wayland_Pointer_Axis_Stop( wl_pointer *pPointer, uint32_t uTime, uint32_t uAxis ) { } void CWaylandInputThread::Wayland_Pointer_Axis_Discrete( wl_pointer *pPointer, uint32_t uAxis, int32_t nDiscrete ) { } void CWaylandInputThread::Wayland_Pointer_Axis_Value120( wl_pointer *pPointer, uint32_t uAxis, int32_t nValue120 ) { if ( !cv_wayland_mouse_warp_without_keyboard_focus && !m_bKeyboardEntered ) return; assert( uAxis == WL_POINTER_AXIS_VERTICAL_SCROLL || uAxis == WL_POINTER_AXIS_HORIZONTAL_SCROLL ); // Vertical is first in the wl_pointer_axis enum, flip y,x -> x,y m_flScrollAccum[ !uAxis ] += nValue120 / 120.0; } void CWaylandInputThread::Wayland_Pointer_Frame( wl_pointer *pPointer ) { defer( m_uAxisSource = WL_POINTER_AXIS_SOURCE_WHEEL ); double flX = m_flScrollAccum[0]; double flY = m_flScrollAccum[1]; m_flScrollAccum[0] = 0.0; m_flScrollAccum[1] = 0.0; if ( !cv_wayland_mouse_warp_without_keyboard_focus && !m_bKeyboardEntered ) return; if ( m_uAxisSource != WL_POINTER_AXIS_SOURCE_WHEEL ) return; if ( flX == 0.0 && flY == 0.0 ) return; wlserver_lock(); wlserver_mousewheel( flX, flY, ++m_uFakeTimestamp ); wlserver_unlock(); } // Keyboard void CWaylandInputThread::Wayland_Keyboard_Keymap( wl_keyboard *pKeyboard, uint32_t uFormat, int32_t nFd, uint32_t uSize ) { // We are not doing much with the keymap, we pass keycodes thru. // Ideally we'd use this to influence our keymap to clients, eg. x server. defer( close( nFd ) ); if ( uFormat != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 ) return; char *pMap = (char *)mmap( nullptr, uSize, PROT_READ, MAP_PRIVATE, nFd, 0 ); if ( !pMap || pMap == MAP_FAILED ) { xdg_log.errorf( "Failed to map keymap fd." ); return; } defer( munmap( pMap, uSize ) ); xkb_keymap *pKeymap = xkb_keymap_new_from_string( m_pXkbContext, pMap, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS ); if ( !pKeymap ) { xdg_log.errorf( "Failed to create xkb_keymap" ); return; } xkb_keymap_unref( m_pXkbKeymap ); m_pXkbKeymap = pKeymap; for ( uint32_t i = 0; i < GAMESCOPE_WAYLAND_MOD_COUNT; i++ ) m_uModMask[ i ] = 1u << xkb_keymap_mod_get_index( m_pXkbKeymap, WaylandModifierToXkbModifierName( ( WaylandModifierIndex ) i ) ); } void CWaylandInputThread::Wayland_Keyboard_Enter( wl_keyboard *pKeyboard, uint32_t uSerial, wl_surface *pSurface, wl_array *pKeys ) { m_bKeyboardEntered = true; m_uScancodesHeld.clear(); const uint32_t *pBegin = (uint32_t *)pKeys->data; const uint32_t *pEnd = pBegin + ( pKeys->size / sizeof(uint32_t) ); std::span keys{ pBegin, pEnd }; for ( uint32_t uKey : keys ) { HandleKey( uKey, true ); m_uScancodesHeld.insert( uKey ); } if ( m_ofPendingCursorX ) { assert( m_ofPendingCursorY.has_value() ); Wayland_Pointer_Motion( m_pPointer, 0, *m_ofPendingCursorX, *m_ofPendingCursorY ); m_ofPendingCursorX = std::nullopt; m_ofPendingCursorY = std::nullopt; } } void CWaylandInputThread::Wayland_Keyboard_Leave( wl_keyboard *pKeyboard, uint32_t uSerial, wl_surface *pSurface ) { m_bKeyboardEntered = false; m_uKeyModifiers = 0; for ( uint32_t uKey : m_uScancodesHeld ) HandleKey( uKey, false ); m_uScancodesHeld.clear(); } void CWaylandInputThread::Wayland_Keyboard_Key( wl_keyboard *pKeyboard, uint32_t uSerial, uint32_t uTime, uint32_t uKey, uint32_t uState ) { if ( !m_bKeyboardEntered ) return; const bool bPressed = uState == WL_KEYBOARD_KEY_STATE_PRESSED; const bool bWasPressed = m_uScancodesHeld.contains( uKey ); if ( bWasPressed == bPressed ) return; HandleKey( uKey, bPressed ); if ( bWasPressed ) m_uScancodesHeld.erase( uKey ); else m_uScancodesHeld.emplace( uKey ); } void CWaylandInputThread::Wayland_Keyboard_Modifiers( wl_keyboard *pKeyboard, uint32_t uSerial, uint32_t uModsDepressed, uint32_t uModsLatched, uint32_t uModsLocked, uint32_t uGroup ) { m_uKeyModifiers = uModsDepressed | uModsLatched | uModsLocked; } void CWaylandInputThread::Wayland_Keyboard_RepeatInfo( wl_keyboard *pKeyboard, int32_t nRate, int32_t nDelay ) { } // Relative Pointer void CWaylandInputThread::Wayland_RelativePointer_RelativeMotion( zwp_relative_pointer_v1 *pRelativePointer, uint32_t uTimeHi, uint32_t uTimeLo, wl_fixed_t fDx, wl_fixed_t fDy, wl_fixed_t fDxUnaccel, wl_fixed_t fDyUnaccel ) { // Don't do any motion/movement stuff if we don't have kb focus if ( !cv_wayland_mouse_relmotion_without_keyboard_focus && !m_bKeyboardEntered ) return; wlserver_lock(); wlserver_mousemotion( wl_fixed_to_double( fDxUnaccel ), wl_fixed_to_double( fDyUnaccel ), ++m_uFakeTimestamp ); wlserver_unlock(); } ///////////////////////// // Backend Instantiator ///////////////////////// template <> bool IBackend::Set() { return Set( new CWaylandBackend{} ); } } ValveSoftware-gamescope-eb620ab/src/BufferMemo.cpp000066400000000000000000000050151502457270500222600ustar00rootroot00000000000000#include "BufferMemo.h" #include "wlserver.hpp" namespace gamescope { static LogScope memo_log{ "BufferMemo" }; ///////////////// // CBufferMemo ///////////////// CBufferMemo::CBufferMemo( CBufferMemoizer *pMemoizer, wlr_buffer *pBuffer, OwningRc pTexture ) : m_pMemoizer{ pMemoizer } , m_pBuffer{ pBuffer } , m_pVulkanTexture{ std::move( pTexture ) } { } CBufferMemo::~CBufferMemo() { wl_list_remove( &m_DeleteListener.link ); } void CBufferMemo::Finalize() { wlserver_lock(); wl_signal_add( &m_pBuffer->events.destroy, &m_DeleteListener ); wlserver_unlock(); } void CBufferMemo::OnBufferDestroyed( void *pUserData ) { assert( m_pVulkanTexture->GetRefCount() == 0 ); m_pMemoizer->UnmemoizeBuffer( m_pBuffer ); } /////////////////// // CBufferMemoizer /////////////////// OwningRc CBufferMemoizer::LookupVulkanTexture( wlr_buffer *pBuffer ) const { std::scoped_lock lock{ m_mutBufferMemos }; auto iter = m_BufferMemos.find( pBuffer ); if ( iter == m_BufferMemos.end() ) return nullptr; return iter->second.GetVulkanTexture(); } void CBufferMemoizer::MemoizeBuffer( wlr_buffer *pBuffer, OwningRc pTexture ) { memo_log.debugf( "Memoizing new buffer: wlr_buffer %p -> texture: %p", pBuffer, pTexture.get() ); // Can't hold m_mutBufferMemos while we finalize link from pMemo to buffer // as we can't have wlserver_lock held otherwise we can deadlock when // adding the wl_signal. // // This is fine as the lookups only happen on one thread, that calls this // or LookupVulkanTexture. CBufferMemo *pMemo = nullptr; { std::scoped_lock lock{ m_mutBufferMemos }; auto [ iter, bSuccess ] = m_BufferMemos.emplace( std::piecewise_construct, std::forward_as_tuple( pBuffer ), std::forward_as_tuple( this, pBuffer, std::move( pTexture ) ) ); assert( bSuccess ); pMemo = &iter->second; } pMemo->Finalize(); } void CBufferMemoizer::UnmemoizeBuffer( wlr_buffer *pBuffer ) { memo_log.debugf( "Unmemoizing buffer: wlr_buffer %p", pBuffer ); std::scoped_lock lock{ m_mutBufferMemos }; auto iter = m_BufferMemos.find( pBuffer ); assert( iter != m_BufferMemos.end() ); m_BufferMemos.erase( iter ); } }ValveSoftware-gamescope-eb620ab/src/BufferMemo.h000066400000000000000000000034771502457270500217370ustar00rootroot00000000000000#pragma once #include "rc.h" #include "rendervulkan.hpp" #include #include struct wl_listener; struct wlr_buffer; // TODO: Move to common code when we want it more. #define WAYLAND_LISTENER( member_listener, func ) \ wl_listener { .notify = []( wl_listener *pListener, void *pUserData ) { decltype( this ) pObject = wl_container_of( pListener, pObject, member_listener ); pObject->func( pUserData ); } } namespace gamescope { class CBufferMemoizer; class CBufferMemo; class CBufferMemo { public: CBufferMemo( CBufferMemoizer *pMemoizer, wlr_buffer *pBuffer, OwningRc pTexture ); ~CBufferMemo(); void Finalize(); CBufferMemoizer *GetMemoizer() const { return m_pMemoizer; } const OwningRc &GetVulkanTexture() const { return m_pVulkanTexture; } void OnBufferDestroyed( void *pUserData ); private: CBufferMemoizer *m_pMemoizer = nullptr; wlr_buffer *m_pBuffer = nullptr; wl_listener m_DeleteListener = WAYLAND_LISTENER( m_DeleteListener, OnBufferDestroyed ); // OwningRc to have a private reference: // So we can keep the CVulkanTexture, as public references // determine when we give the texture/buffer back to the app. OwningRc m_pVulkanTexture; }; class CBufferMemoizer { public: // Must return an OwningRc for the locking to make sense and not deadlock. OwningRc LookupVulkanTexture( wlr_buffer *pBuffer ) const; void MemoizeBuffer( wlr_buffer *pBuffer, OwningRc pTexture ); void UnmemoizeBuffer( wlr_buffer *pBuffer ); private: mutable std::mutex m_mutBufferMemos; std::unordered_map m_BufferMemos; }; } ValveSoftware-gamescope-eb620ab/src/GamescopeVersion.h.in000066400000000000000000000001531502457270500235520ustar00rootroot00000000000000#pragma once namespace gamescope { static constexpr const char k_szGamescopeVersion[] = "@VCS_TAG@"; }ValveSoftware-gamescope-eb620ab/src/InputEmulation.cpp000066400000000000000000000234251502457270500232130ustar00rootroot00000000000000#if HAVE_LIBEIS #include #include #include #include "backend.h" #include "InputEmulation.h" #include "wlserver.hpp" static LogScope gamescope_ei("gamescope_ei"); namespace gamescope { GamescopeInputServer::GamescopeInputServer() { } GamescopeInputServer::~GamescopeInputServer() { eis_unref( m_pEis ); m_pEis = nullptr; // We don't own the fd. m_nFd = -1; } bool GamescopeInputServer::Init( const char *pszSocketPath ) { if ( !pszSocketPath || !*pszSocketPath ) { gamescope_ei.errorf( "No Gamescope EIS socket path!" ); return false; } m_pEis = eis_new( nullptr ); if ( !m_pEis ) { gamescope_ei.errorf( "Failed to create eis." ); return false; } if ( eis_setup_backend_socket( m_pEis, pszSocketPath ) != 0 ) { gamescope_ei.errorf( "Failed to init eis: %s", strerror( errno ) ); return false; } m_nFd = eis_get_fd( m_pEis ); if ( m_nFd < 0 ) { gamescope_ei.errorf( "Failed to get eis fd" ); return false; } return true; } int GamescopeInputServer::GetFD() { return m_nFd; } void GamescopeInputServer::OnPollIn() { static uint32_t s_uSequence = 0; eis_dispatch( m_pEis ); while ( eis_event *pEisEvent = eis_get_event( m_pEis ) ) { switch ( eis_event_get_type( pEisEvent ) ) { case EIS_EVENT_CLIENT_CONNECT: { eis_client *pClient = eis_event_get_client( pEisEvent ); eis_client_connect( pClient ); eis_seat *pSeat = eis_client_new_seat( pClient, "chair" ); eis_seat_configure_capability( pSeat, EIS_DEVICE_CAP_POINTER ); eis_seat_configure_capability( pSeat, EIS_DEVICE_CAP_POINTER_ABSOLUTE ); eis_seat_configure_capability( pSeat, EIS_DEVICE_CAP_KEYBOARD ); //eis_seat_configure_capability( pSeat, EIS_DEVICE_CAP_TOUCH ); eis_seat_configure_capability( pSeat, EIS_DEVICE_CAP_BUTTON ); eis_seat_configure_capability( pSeat, EIS_DEVICE_CAP_SCROLL ); eis_seat_add( pSeat ); // Unref it now that we no longer need it, and gave it over to eis. eis_seat_unref( pSeat ); } break; case EIS_EVENT_CLIENT_DISCONNECT: { eis_client *pClient = eis_event_get_client( pEisEvent ); eis_client_disconnect( pClient ); } break; case EIS_EVENT_SEAT_BIND: { eis_client *pClient = eis_event_get_client( pEisEvent ); eis_seat *pSeat = eis_event_get_seat( pEisEvent ); bool bWantsDevice = eis_event_seat_has_capability( pEisEvent, EIS_DEVICE_CAP_POINTER ) || eis_event_seat_has_capability( pEisEvent, EIS_DEVICE_CAP_POINTER_ABSOLUTE ) || eis_event_seat_has_capability( pEisEvent, EIS_DEVICE_CAP_BUTTON ) || eis_event_seat_has_capability( pEisEvent, EIS_DEVICE_CAP_SCROLL ) || eis_event_seat_has_capability( pEisEvent, EIS_DEVICE_CAP_KEYBOARD ); bool bHasDevice = eis_client_get_user_data( pClient ) != nullptr; if ( bWantsDevice && !bHasDevice ) { eis_device *pVirtualInput = eis_seat_new_device( pSeat ); eis_device_configure_name( pVirtualInput, "Gamescope Virtual Input" ); eis_device_configure_capability( pVirtualInput, EIS_DEVICE_CAP_POINTER ); eis_device_configure_capability( pVirtualInput, EIS_DEVICE_CAP_POINTER_ABSOLUTE ); eis_device_configure_capability( pVirtualInput, EIS_DEVICE_CAP_BUTTON ); eis_device_configure_capability( pVirtualInput, EIS_DEVICE_CAP_SCROLL ); eis_device_configure_capability( pVirtualInput, EIS_DEVICE_CAP_KEYBOARD ); // Can add this someday if we want it. //eis_device_configure_capability( pVirtualInput, EIS_DEVICE_CAP_TOUCH ); eis_region *pVirtualInputRegion = eis_device_new_region( pVirtualInput ); eis_region_set_mapping_id( pVirtualInputRegion, "Mr. Worldwide" ); eis_region_set_size( pVirtualInputRegion, INT32_MAX, INT32_MAX ); eis_region_set_offset( pVirtualInputRegion, 0, 0 ); eis_region_add( pVirtualInputRegion ); // We don't want this anymore, but pVirtualInput can own it eis_region_unref( pVirtualInputRegion ); eis_device_add( pVirtualInput ); eis_device_resume( pVirtualInput ); if ( !eis_client_is_sender( pClient ) ) eis_device_start_emulating( pVirtualInput, ++s_uSequence ); // We have a ref on pVirtualInput, store that in pClient's userdata so we can remove device later. eis_client_set_user_data( pClient, (void *) pVirtualInput ); } else if ( !bWantsDevice && bHasDevice ) { eis_device *pDevice = (eis_device *) eis_client_get_user_data( pClient ); eis_device_remove( pDevice ); eis_device_unref( pDevice ); eis_client_set_user_data( pClient, nullptr ); } } break; case EIS_EVENT_DEVICE_CLOSED: { eis_client *pClient = eis_event_get_client( pEisEvent ); eis_device *pDevice = eis_event_get_device( pEisEvent ); // Remove the device from our tracking on the client. eis_device_remove( pDevice ); eis_device_unref( pDevice ); eis_client_set_user_data( pClient, nullptr ); } break; case EIS_EVENT_DEVICE_START_EMULATING: case EIS_EVENT_DEVICE_STOP_EMULATING: { // Don't care. } break; case EIS_EVENT_POINTER_MOTION: { GetBackend()->NotifyPhysicalInput( InputType::Mouse ); wlserver_lock(); wlserver_mousemotion( eis_event_pointer_get_dx( pEisEvent ), eis_event_pointer_get_dy( pEisEvent ), ++s_uSequence ); wlserver_unlock(); } break; case EIS_EVENT_POINTER_MOTION_ABSOLUTE: { GetBackend()->NotifyPhysicalInput( InputType::Mouse ); wlserver_lock(); wlserver_mousewarp( eis_event_pointer_get_absolute_x( pEisEvent ), eis_event_pointer_get_absolute_y( pEisEvent ), ++s_uSequence, true ); wlserver_unlock(); } break; case EIS_EVENT_BUTTON_BUTTON: { wlserver_lock(); wlserver_mousebutton( eis_event_button_get_button( pEisEvent ), eis_event_button_get_is_press( pEisEvent ), ++s_uSequence ); wlserver_unlock(); } break; case EIS_EVENT_SCROLL_DELTA: { wlserver_lock(); wlserver_mousewheel( eis_event_scroll_get_dx( pEisEvent ), eis_event_scroll_get_dy( pEisEvent ), ++s_uSequence ); wlserver_unlock(); } break; case EIS_EVENT_SCROLL_DISCRETE: { m_flScrollAccum[0] += eis_event_scroll_get_discrete_dx( pEisEvent ) / 120.0; m_flScrollAccum[1] += eis_event_scroll_get_discrete_dy( pEisEvent ) / 120.0; } break; case EIS_EVENT_KEYBOARD_KEY: { wlserver_lock(); wlserver_key( eis_event_keyboard_get_key( pEisEvent ), eis_event_keyboard_get_key_is_press( pEisEvent ), ++s_uSequence ); wlserver_unlock(); } break; case EIS_EVENT_TOUCH_DOWN: case EIS_EVENT_TOUCH_MOTION: case EIS_EVENT_TOUCH_UP: { // Touch not implemented yet. gamescope_ei.errorf( "No touch support yet! How did you get here?" ); } break; case EIS_EVENT_FRAME: { double flScrollX = m_flScrollAccum[0]; double flScrollY = m_flScrollAccum[1]; m_flScrollAccum[0] = 0.0; m_flScrollAccum[1] = 0.0; if ( flScrollX == 0.0 && flScrollY == 0.0 ) break; wlserver_lock(); wlserver_mousewheel( flScrollX, flScrollY, ++s_uSequence ); wlserver_unlock(); } break; default: { gamescope_ei.errorf( "Unhandled libei event!" ); } break; } eis_event_unref( pEisEvent ); } } } #endif ValveSoftware-gamescope-eb620ab/src/InputEmulation.h000066400000000000000000000007101502457270500226500ustar00rootroot00000000000000#pragma once #include "waitable.h" struct eis; namespace gamescope { class GamescopeInputServer final : public IWaitable { public: GamescopeInputServer(); ~GamescopeInputServer(); bool Init( const char *pszSocketPath ); virtual int GetFD() override; virtual void OnPollIn() override; private: eis *m_pEis = nullptr; int m_nFd = -1; double m_flScrollAccum[2]{}; }; } ValveSoftware-gamescope-eb620ab/src/LibInputHandler.cpp000066400000000000000000000144641502457270500232650ustar00rootroot00000000000000#include "LibInputHandler.h" #include #include #include #include #include "log.hpp" #include "backend.h" #include "wlserver.hpp" #include "Utils/Defer.h" // Handles libinput in contexts where we don't have a session // and can't use the wlroots libinput stuff. // // eg. in VR where we want global access to the m + kb // without doing any seat dance. // // That may change in the future... // but for now, this solves that problem. namespace gamescope { static LogScope log_input_stealer( "InputStealer" ); const libinput_interface CLibInputHandler::s_LibInputInterface = { .open_restricted = []( const char *pszPath, int nFlags, void *pUserData ) -> int { return open( pszPath, nFlags ); }, .close_restricted = []( int nFd, void *pUserData ) -> void { close( nFd ); }, }; CLibInputHandler::CLibInputHandler() { } CLibInputHandler::~CLibInputHandler() { if ( m_pLibInput ) { libinput_unref( m_pLibInput ); m_pLibInput = nullptr; } if ( m_pUdev ) { udev_unref( m_pUdev ); m_pUdev = nullptr; } } bool CLibInputHandler::Init() { m_pUdev = udev_new(); if ( !m_pUdev ) { log_input_stealer.errorf( "Failed to create udev interface" ); return false; } m_pLibInput = libinput_udev_create_context( &s_LibInputInterface, nullptr, m_pUdev ); if ( !m_pLibInput ) { log_input_stealer.errorf( "Failed to create libinput context" ); return false; } const char *pszSeatName = "seat0"; if ( libinput_udev_assign_seat( m_pLibInput, pszSeatName ) == -1 ) { log_input_stealer.errorf( "Could not assign seat \"%s\"", pszSeatName ); return false; } return true; } int CLibInputHandler::GetFD() { if ( !m_pLibInput ) return -1; return libinput_get_fd( m_pLibInput ); } void CLibInputHandler::OnPollIn() { static uint32_t s_uSequence = 0; libinput_dispatch( m_pLibInput ); while ( libinput_event *pEvent = libinput_get_event( m_pLibInput ) ) { defer( libinput_event_destroy( pEvent ) ); libinput_event_type eEventType = libinput_event_get_type( pEvent ); switch ( eEventType ) { case LIBINPUT_EVENT_POINTER_MOTION: { libinput_event_pointer *pPointerEvent = libinput_event_get_pointer_event( pEvent ); double flDx = libinput_event_pointer_get_dx( pPointerEvent ); double flDy = libinput_event_pointer_get_dy( pPointerEvent ); GetBackend()->NotifyPhysicalInput( InputType::Mouse ); wlserver_lock(); wlserver_mousemotion( flDx, flDy, ++s_uSequence ); wlserver_unlock(); } break; case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE: { libinput_event_pointer *pPointerEvent = libinput_event_get_pointer_event( pEvent ); double flX = libinput_event_pointer_get_absolute_x( pPointerEvent ); double flY = libinput_event_pointer_get_absolute_y( pPointerEvent ); GetBackend()->NotifyPhysicalInput( InputType::Mouse ); wlserver_lock(); wlserver_mousewarp( flX, flY, ++s_uSequence, true ); wlserver_unlock(); } break; case LIBINPUT_EVENT_POINTER_BUTTON: { libinput_event_pointer *pPointerEvent = libinput_event_get_pointer_event( pEvent ); uint32_t uButton = libinput_event_pointer_get_button( pPointerEvent ); libinput_button_state eButtonState = libinput_event_pointer_get_button_state( pPointerEvent ); wlserver_lock(); wlserver_mousebutton( uButton, eButtonState == LIBINPUT_BUTTON_STATE_PRESSED, ++s_uSequence ); wlserver_unlock(); } break; case LIBINPUT_EVENT_POINTER_SCROLL_WHEEL: { libinput_event_pointer *pPointerEvent = libinput_event_get_pointer_event( pEvent ); static constexpr libinput_pointer_axis eAxes[] = { LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL, LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL, }; for ( uint32_t i = 0; i < std::size( eAxes ); i++ ) { libinput_pointer_axis eAxis = eAxes[i]; if ( !libinput_event_pointer_has_axis( pPointerEvent, eAxis ) ) continue; double flScroll = libinput_event_pointer_get_scroll_value_v120( pPointerEvent, eAxis ); m_flScrollAccum[i] += flScroll / 120.0; } } break; case LIBINPUT_EVENT_KEYBOARD_KEY: { libinput_event_keyboard *pKeyboardEvent = libinput_event_get_keyboard_event( pEvent ); uint32_t uKey = libinput_event_keyboard_get_key( pKeyboardEvent ); libinput_key_state eState = libinput_event_keyboard_get_key_state( pKeyboardEvent ); wlserver_lock(); wlserver_key( uKey, eState == LIBINPUT_KEY_STATE_PRESSED, ++ s_uSequence ); wlserver_unlock(); } break; default: break; } } // Handle scrolling { double flScrollX = m_flScrollAccum[0]; double flScrollY = m_flScrollAccum[1]; m_flScrollAccum[0] = 0.0; m_flScrollAccum[1] = 0.0; if ( flScrollX != 0.0 || flScrollY != 0.0 ) { wlserver_lock(); wlserver_mousewheel( flScrollX, flScrollY, ++s_uSequence ); wlserver_unlock(); } } } } ValveSoftware-gamescope-eb620ab/src/LibInputHandler.h000066400000000000000000000010361502457270500227210ustar00rootroot00000000000000#pragma once #include "waitable.h" struct libinput_interface; struct udev; struct libinput; namespace gamescope { class CLibInputHandler final : public IWaitable { public: CLibInputHandler(); ~CLibInputHandler(); bool Init(); virtual int GetFD() override; virtual void OnPollIn() override; private: udev *m_pUdev = nullptr; libinput *m_pLibInput = nullptr; double m_flScrollAccum[2]{}; static const libinput_interface s_LibInputInterface; }; }ValveSoftware-gamescope-eb620ab/src/Ratio.h000066400000000000000000000036461502457270500207640ustar00rootroot00000000000000#pragma once #include #include #include #include #include namespace gamescope { template class Ratio { public: Ratio(T num, T denom) { Set( num, denom ); } Ratio( std::string_view view ) { Set(0, 0); size_t colon = view.find(":"); if ( colon == std::string_view::npos ) return; std::string_view numStr = view.substr( 0, colon ); std::string_view denomStr = view.substr( colon + 1 ); T num = 0, denom = 0; std::from_chars( numStr.data(), numStr.data() + numStr.size(), num ); std::from_chars( denomStr.data(), denomStr.data() + denomStr.size(), denom ); Set( num, denom ); } T Num() const { return m_num; } T Denom() const { return m_denom; } bool IsUndefined() const { return m_denom == 0; } void Set( T num, T denom ) { const T gcd = std::gcd( num, denom ); if ( gcd == 0 ) { m_num = 0; m_denom = 0; return; } m_num = num / gcd; m_denom = denom / gcd; } bool operator == ( const Ratio& other ) const { return Num() == other.Num() && Denom() == other.Denom(); } bool operator != ( const Ratio& other ) const { return !(*this == other); } bool operator >= ( const Ratio& other ) const { return Num() * other.Denom() >= other.Num() * Denom(); } bool operator > ( const Ratio& other ) const { return Num() * other.Denom() > other.Num() * Denom(); } bool operator < ( const Ratio& other ) const { return !( *this >= other ); } bool operator <= ( const Ratio& other ) const { return !( *this > other ); } private: T m_num, m_denom; }; }ValveSoftware-gamescope-eb620ab/src/Script/000077500000000000000000000000001502457270500207705ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/src/Script/Script.cpp000066400000000000000000000233341502457270500227450ustar00rootroot00000000000000#include "Script.h" #include "convar.h" #include "color_helpers.h" #include "../log.hpp" #include #include std::string_view GetHomeDir(); namespace gamescope { using namespace std::literals; static LogScope s_ScriptLog{ "script" }; static LogScope s_ScriptMgrLog{ "scriptmgr" }; static ConVar cv_script_use_local_scripts{ "script_use_local_scripts", false, "Whether or not to use the local scripts (../config) as opposed to the ones in /etc/gamescope.d" }; static ConVar cv_script_use_user_scripts{ "script_use_user_scripts", true, "Whether or not to use user config scripts ($XDG_CONFIG_DIR/gamescope) at all." }; static std::string_view GetConfigDir() { static std::string s_sConfigDir = []() -> std::string { const char *pszConfigHome = getenv( "XDG_CONFIG_HOME" ); if ( pszConfigHome && *pszConfigHome ) return pszConfigHome; return std::string{ GetHomeDir() } + "/.config"; }(); return s_sConfigDir; } static inline void PanicFunction( sol::optional oMsg ) { s_ScriptLog.errorf( "Lua is in a panic state and will now abort() the application" ); if ( oMsg ) { s_ScriptLog.errorf( "\tError Message: %s", oMsg->c_str() ); } abort(); } static inline int ExceptionFunction( lua_State* pState, sol::optional oException, sol::string_view psvDescription ) { // L is the lua state, which you can wrap in a state_view if necessary // maybe_exception will contain exception, if it exists // description will either be the what() of the exception or a description saying that we hit the general-case catch(...) s_ScriptLog.errorf( "An exception occurred:\n %.*s", (int)psvDescription.length(), psvDescription.data() ); // you must push 1 element onto the stack to be // transported through as the error object in Lua // note that Lua -- and 99.5% of all Lua users and libraries -- expects a string // so we push a single string (in our case, the description of the error) return sol::stack::push( pState, psvDescription ); } static inline void LuaErrorHandler( const std::string &msg ) { s_ScriptLog.errorf( "An error occurred:\n %.*s", (int)msg.length(), msg.data() ); } int32_t CScriptManager::s_nNextScriptId = 0; CScriptManager &CScriptManager::GlobalScriptScope() { static CScriptManager s_State; return s_State; } CScriptManager::CScriptManager() { m_State.open_libraries(); static bool s_bSetDefaultHandler = false; if ( !s_bSetDefaultHandler ) { m_State["_gamescope_error_handler"] = LuaErrorHandler; sol::protected_function::set_default_handler( m_State["_gamescope_error_handler"] ); s_bSetDefaultHandler = true; } m_State.set_panic( sol::c_call ); m_State.set_exception_handler( &ExceptionFunction ); m_Gamescope.Base = m_State.create_named_table( "gamescope" ); m_Gamescope.Base["hook"] = [this]( std::string_view svName, sol::function fnFunc ) { m_Hooks.emplace( std::make_pair( svName, Hook_t{ std::move( fnFunc ), m_nCurrentScriptId } ) ); }; m_Gamescope.Base.new_enum( "eotf", { { "gamma22", EOTF_Gamma22 }, { "pq", EOTF_PQ }, { "count", EOTF_Count }, } ); m_Gamescope.Base.new_enum( "log_priority", { { "silent", LOG_SILENT }, { "error", LOG_ERROR }, { "warning", LOG_WARNING }, { "info", LOG_INFO }, { "debug", LOG_DEBUG }, } ); m_Gamescope.Base["log"] = []( LogPriority ePriority, std::string_view svText ) { s_ScriptLog.log( ePriority, svText ); }; m_Gamescope.Convars.Base = m_State.create_table(); m_Gamescope.Base.set( "convars", m_Gamescope.Convars.Base ); m_Gamescope.Config.Base = m_State.create_table(); m_Gamescope.Base.set( "config", m_Gamescope.Config.Base ); m_Gamescope.Config.KnownDisplays = m_State.create_table(); m_Gamescope.Config.Base.set( "known_displays", m_Gamescope.Config.KnownDisplays ); } void CScriptManager::RunDefaultScripts() { const char *sScriptPathEnv = getenv("GAMESCOPE_SCRIPT_PATH"); if ( cv_script_use_local_scripts ) { RunFolder( "../scripts", true ); } else if ( sScriptPathEnv ) { std::vector sScriptPaths = gamescope::Split( sScriptPathEnv, ":" ); for ( const auto &sScriptPath : sScriptPaths ) { RunFolder( sScriptPath, true ); } } else { RunFolder( SCRIPT_DIR, true ); RunFolder( "/etc/gamescope/scripts", true ); } if ( cv_script_use_user_scripts ) { std::string sUserConfigs = std::string{ GetConfigDir() } + "/gamescope/scripts"; RunFolder( sUserConfigs, true ); } } void CScriptManager::RunScriptText( std::string_view svContents ) { uint32_t uScriptId = s_nNextScriptId++; { int32_t nPreviousScriptId = m_nCurrentScriptId; m_nCurrentScriptId = uScriptId; State().script( svContents ); m_nCurrentScriptId = nPreviousScriptId; } } void CScriptManager::RunFile( std::string_view svPath ) { uint32_t uScriptId = s_nNextScriptId++; s_ScriptMgrLog.infof( "Running script file '%.*s' (id: %u)", int( svPath.length() ), svPath.data(), uScriptId ); std::string sPath = std::string( svPath ); { int32_t nPreviousScriptId = m_nCurrentScriptId; m_nCurrentScriptId = uScriptId; State().script_file( std::move( sPath ) ); m_nCurrentScriptId = nPreviousScriptId; } } bool CScriptManager::RunFolder( std::string_view svDirectory, bool bRecursive ) { s_ScriptMgrLog.infof( "Loading scripts from: '%.*s'", int( svDirectory.size() ), svDirectory.data() ); std::filesystem::path dirConfig = std::filesystem::path{ svDirectory }; if ( !std::filesystem::is_directory( dirConfig ) ) { s_ScriptMgrLog.warnf( "Directory '%.*s' does not exist", int( svDirectory.size() ), svDirectory.data() ); return false; } if ( access( dirConfig.c_str(), R_OK | X_OK ) != 0 ) { s_ScriptMgrLog.warnf( "Cannot open directory '%.*s'", int( svDirectory.size() ), svDirectory.data() ); return false; } std::vector sFiles; std::vector sDirectories; for ( const auto &iter : std::filesystem::directory_iterator( dirConfig ) ) { const std::filesystem::path &path = iter.path(); // XXX: is_regular_file -> What about symlinks? if ( std::filesystem::is_regular_file( iter.status() ) && path.extension() == ".lua"sv ) { sFiles.push_back( path ); } if ( bRecursive && std::filesystem::is_directory( iter.status() ) ) { sDirectories.push_back( path ); } } std::sort( sFiles.begin(), sFiles.end() ); std::sort( sDirectories.begin(), sDirectories.end() ); for ( const auto &sPath : sFiles ) { RunFile( sPath ); } if ( bRecursive ) { for ( const auto &sPath : sDirectories ) { RunFolder( sPath, bRecursive ); } } return true; } void CScriptManager::InvalidateAllHooks() { m_Hooks.clear(); } void CScriptManager::InvalidateHooksForScript( int32_t nScriptId ) { if ( nScriptId < 0 ) return; std::erase_if( m_Hooks, [ nScriptId ]( const auto &iter ) -> bool { return iter.second.nScriptId == nScriptId; }); } // // GamescopeScript_t // std::optional> GamescopeScript_t::Config_t::LookupDisplay( CScriptScopedLock &script, std::string_view psvVendor, uint16_t uProduct, std::string_view psvModel, std::string_view psvDataString ) { int nMaxPrority = -1; std::optional> oOutDisplay; sol::table tDisplay = script->create_table(); tDisplay["vendor"] = psvVendor; tDisplay["product"] = uProduct; tDisplay["model"] = psvModel; tDisplay["data_string"] = psvDataString; for ( auto iter : KnownDisplays ) { sol::optional otTable = iter.second.as>(); if ( !otTable ) continue; sol::table tTable = *otTable; sol::optional ofnMatches = tTable["matches"]; if ( !ofnMatches ) continue; sol::function fnMatches = *ofnMatches; if ( !fnMatches ) continue; int nPriority = fnMatches( tDisplay); if ( nPriority <= nMaxPrority ) continue; std::string_view psvKey = iter.first.as(); nMaxPrority = nPriority; oOutDisplay = std::make_pair( psvKey, tTable ); } return oOutDisplay; } } ValveSoftware-gamescope-eb620ab/src/Script/Script.h000066400000000000000000000114461502457270500224130ustar00rootroot00000000000000#pragma once #include "../Utils/Dict.h" #include #if HAVE_SCRIPTING #include namespace gamescope { class CScriptScopedLock; struct GamescopeScript_t { // Stores table entries, and caches sol::table // handles for things we use frequently. sol::table Base; struct ConVars_t { sol::table Base; } Convars; struct Config_t { sol::table Base; sol::table KnownDisplays; std::optional> LookupDisplay( CScriptScopedLock &script, std::string_view psvVendor, uint16_t uProduct, std::string_view psvModel, std::string_view psvDataString ); } Config; }; class CScriptManager { public: CScriptManager(); template void CallHook( std::string_view svName, Args&&... args ) { auto range = m_Hooks.equal_range( svName ); for ( auto iter = range.first; iter != range.second; iter++ ) { int32_t nPreviousScriptId = m_nCurrentScriptId; Hook_t *pHook = &iter->second; m_nCurrentScriptId = pHook->nScriptId; iter->second.fnCallback( std::forward( args )... ); m_nCurrentScriptId = nPreviousScriptId; } } void RunDefaultScripts(); void RunScriptText( std::string_view svContents ); void RunFile( std::string_view svPath ); bool RunFolder( std::string_view svPath, bool bRecursive = false ); void InvalidateAllHooks(); void InvalidateHooksForScript( int32_t nScriptId ); sol::state *operator->() { return &m_State; } sol::state &State() { return m_State; } GamescopeScript_t &Gamescope() { return m_Gamescope; } std::mutex &Mutex() { return m_mutMutex; } protected: static CScriptManager &GlobalScriptScope(); friend CScriptScopedLock; private: mutable std::mutex m_mutMutex; sol::state m_State; GamescopeScript_t m_Gamescope; struct Hook_t { sol::function fnCallback; int32_t nScriptId = -1; }; MultiDict m_Hooks; int32_t m_nCurrentScriptId = -1; static int32_t s_nNextScriptId; }; class CScriptScopedLock { public: CScriptScopedLock() : CScriptScopedLock{ CScriptManager::GlobalScriptScope() } { } CScriptScopedLock( CScriptManager &manager ) : m_Lock{ manager.Mutex() } , m_ScriptManager{ manager } { } ~CScriptScopedLock() { } CScriptManager &Manager() { return m_ScriptManager; } sol::state *State() { return &m_ScriptManager.State(); } sol::state *operator ->() { return State(); } private: std::scoped_lock m_Lock; CScriptManager &m_ScriptManager; }; template T TableToVec( const sol::table &table ) { if ( !table ) return T{}; T out{}; for ( int i = 0; i < T::length(); i++ ) { std::array ppsvIndices { "x", "y", "z", "w" }; sol::optional ofValue = table[ppsvIndices[i]]; out[i] = ofValue ? *ofValue : 0; } return out; } template std::vector TableToVector( const sol::table &table ) { std::vector out; if ( table ) { for ( auto &iter : table ) { sol::optional oValue = iter.second.as>(); if ( oValue ) out.emplace_back( *oValue ); } } return out; } #define SCRIPTDESC_TEMPLATE( type ) template #define DECLARE_SCRIPTDESC( type ) \ static sol::usertype< type > s_ScriptType; \ class CEnsureScriptTypeInstantiation { public: CEnsureScriptTypeInstantiation() { (void) type :: s_ScriptType; } } m_EnsureTemplateInstantiation_ScriptDesc; #define START_SCRIPTDESC( type, lua_name ) \ inline sol::usertype type::s_ScriptType = CScriptScopedLock()->new_usertype( lua_name #define START_SCRIPTDESC_ANON( type ) \ inline sol::usertype type::s_ScriptType = CScriptScopedLock()->new_usertype( typeid( type ).name() #define SCRIPTDESC( x, y ) , x, y #define END_SCRIPTDESC() ); } #else #define SCRIPTDESC_TEMPLATE( type ) #define DECLARE_SCRIPTDESC( x ) #define START_SCRIPTDESC( type, lua_name ) #define START_SCRIPTDESC_ANON( type ) #define SCRIPTDESC( x, y ) #define END_SCRIPTDESC() #endifValveSoftware-gamescope-eb620ab/src/Timeline.cpp000066400000000000000000000121131502457270500217740ustar00rootroot00000000000000#include #include #include "Timeline.h" #include "wlserver.hpp" #include "rendervulkan.hpp" #include "wlr_begin.hpp" #include #include #include "wlr_end.hpp" namespace gamescope { static LogScope s_TimelineLog( "timeline" ); static uint32_t SyncobjFdToHandle( int32_t nFd ) { int32_t nRet; uint32_t uHandle = 0; if ( ( nRet = drmSyncobjFDToHandle( g_device.drmRenderFd(), nFd, &uHandle ) ) < 0 ) return 0; return uHandle; } /*static*/ std::shared_ptr CTimeline::Create( const TimelineCreateDesc_t &desc ) { std::shared_ptr pSemaphore = g_device.CreateTimelineSemaphore( desc.ulStartingPoint, true ); if ( !pSemaphore ) return nullptr; return std::make_shared( pSemaphore->GetFd(), std::move( pSemaphore ) ); } CTimeline::CTimeline( int32_t nSyncobjFd, std::shared_ptr pSemaphore ) : CTimeline( nSyncobjFd, SyncobjFdToHandle( nSyncobjFd ), std::move( pSemaphore ) ) { } CTimeline::CTimeline( int32_t nSyncobjFd, uint32_t uSyncobjHandle, std::shared_ptr pSemaphore ) : m_nSyncobjFd{ nSyncobjFd } , m_uSyncobjHandle{ uSyncobjHandle } , m_pVkSemaphore{ std::move( pSemaphore ) } { } CTimeline::~CTimeline() { m_pVkSemaphore = nullptr; if ( m_uSyncobjHandle ) drmSyncobjDestroy( GetDrmRenderFD(), m_uSyncobjHandle ); if ( m_nSyncobjFd >= 0 ) close( m_nSyncobjFd ); } int32_t CTimeline::GetDrmRenderFD() { return g_device.drmRenderFd(); } std::shared_ptr CTimeline::ToVkSemaphore() { if ( !m_pVkSemaphore ) m_pVkSemaphore = g_device.ImportTimelineSemaphore( this ); return m_pVkSemaphore; } // CTimelinePoint template CTimelinePoint::CTimelinePoint( std::shared_ptr pTimeline, uint64_t ulPoint ) : m_pTimeline{ std::move( pTimeline ) } , m_ulPoint{ ulPoint } { } template CTimelinePoint::~CTimelinePoint() { if ( ShouldSignalOnDestruction() ) { const uint32_t uHandle = m_pTimeline->GetSyncobjHandle(); drmSyncobjTimelineSignal( m_pTimeline->GetDrmRenderFD(), &uHandle, &m_ulPoint, 1 ); } } template bool CTimelinePoint::Wait( int64_t lTimeout ) { uint32_t uHandle = m_pTimeline->GetSyncobjHandle(); int nRet = drmSyncobjTimelineWait( m_pTimeline->GetDrmRenderFD(), &uHandle, &m_ulPoint, 1, lTimeout, DRM_SYNCOBJ_WAIT_FLAGS_WAIT_ALL, nullptr ); return nRet == 0; } // // Fence flags tl;dr // 0 -> Wait for signal on a materialized fence, -ENOENT if not materialized // DRM_SYNCOBJ_WAIT_FLAGS_WAIT_AVAILABLE -> Wait only for materialization // DRM_SYNCOBJ_WAIT_FLAGS_WAIT_FOR_SUBMIT -> Wait for materialization + signal // template std::pair CTimelinePoint::CreateEventFd() { assert( Type == TimelinePointType::Acquire ); uint32_t uHandle = m_pTimeline->GetSyncobjHandle(); uint64_t ulSignalledPoint = 0; int nRet = drmSyncobjQuery( m_pTimeline->GetDrmRenderFD(), &uHandle, &ulSignalledPoint, 1u ); if ( nRet != 0 ) { s_TimelineLog.errorf_errno( "drmSyncobjQuery failed (%d)", nRet ); return k_InvalidEvent; } if ( ulSignalledPoint >= m_ulPoint ) { return k_AlreadySignalledEvent; } else { const int32_t nExplicitSyncEventFd = eventfd( 0, EFD_CLOEXEC ); if ( nExplicitSyncEventFd < 0 ) { s_TimelineLog.errorf_errno( "Failed to create eventfd (%d)", nExplicitSyncEventFd ); return k_InvalidEvent; } drm_syncobj_eventfd syncobjEventFd = { .handle = m_pTimeline->GetSyncobjHandle(), // Only valid flags are: DRM_SYNCOBJ_WAIT_FLAGS_WAIT_AVAILABLE // -> Wait for fence materialization rather than signal. .flags = 0u, .point = m_ulPoint, .fd = nExplicitSyncEventFd, }; if ( drmIoctl( m_pTimeline->GetDrmRenderFD(), DRM_IOCTL_SYNCOBJ_EVENTFD, &syncobjEventFd ) != 0 ) { s_TimelineLog.errorf_errno( "DRM_IOCTL_SYNCOBJ_EVENTFD failed" ); close( nExplicitSyncEventFd ); return k_InvalidEvent; } return { nExplicitSyncEventFd, false }; } } template class CTimelinePoint; template class CTimelinePoint; } ValveSoftware-gamescope-eb620ab/src/Timeline.h000066400000000000000000000051421502457270500214450ustar00rootroot00000000000000#pragma once #include #include #include #include #include "Utils/NonCopyable.h" struct VulkanTimelineSemaphore_t; namespace gamescope { enum class TimelinePointType { Acquire, Release, }; struct TimelineCreateDesc_t { uint64_t ulStartingPoint = 0ul; }; class CTimeline : public NonCopyable { public: static std::shared_ptr Create( const TimelineCreateDesc_t &desc = {} ); // Inherits nSyncobjFd's ref. CTimeline( int32_t nSyncobjFd, std::shared_ptr pSemaphore = nullptr ); CTimeline( int32_t nSyncobjFd, uint32_t uSyncobjHandle, std::shared_ptr pSemaphore = nullptr ); CTimeline( CTimeline &&other ) : m_nSyncobjFd{ std::exchange( other.m_nSyncobjFd, -1 ) } , m_uSyncobjHandle{ std::exchange( other.m_uSyncobjHandle, 0 ) } { } ~CTimeline(); static int32_t GetDrmRenderFD(); bool IsValid() const { return m_uSyncobjHandle != 0; } int32_t GetSyncobjFd() const { return m_nSyncobjFd; } uint32_t GetSyncobjHandle() const { return m_uSyncobjHandle; } std::shared_ptr ToVkSemaphore(); private: int32_t m_nSyncobjFd = -1; uint32_t m_uSyncobjHandle = 0; std::shared_ptr m_pVkSemaphore; }; template class CTimelinePoint : public NonCopyable { public: static constexpr std::pair k_InvalidEvent = { -1, false }; static constexpr std::pair k_AlreadySignalledEvent = { -1, true }; CTimelinePoint( std::shared_ptr pTimeline, uint64_t ulPoint ); ~CTimelinePoint(); void SetPoint( uint64_t ulPoint ) { m_ulPoint = ulPoint; } uint64_t GetPoint() const { return m_ulPoint; } std::shared_ptr &GetTimeline() { return m_pTimeline; } const std::shared_ptr &GetTimeline() const { return m_pTimeline; } constexpr bool ShouldSignalOnDestruction() const { return Type == TimelinePointType::Release; } bool Wait( int64_t lTimeout = std::numeric_limits::max() ); std::pair CreateEventFd(); private: std::shared_ptr m_pTimeline; uint64_t m_ulPoint = 0; }; using CAcquireTimelinePoint = CTimelinePoint; using CReleaseTimelinePoint = CTimelinePoint; } ValveSoftware-gamescope-eb620ab/src/Utils/000077500000000000000000000000001502457270500206245ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/src/Utils/Algorithm.h000066400000000000000000000115451502457270500227310ustar00rootroot00000000000000#pragma once #include #include #include namespace gamescope::Algorithm { template constexpr TObj *Begin( std::span span ) { return span.data(); } template constexpr TObj *End( std::span span ) { return Begin( span ) + span.size(); } template constexpr const TObj *Begin( const std::vector &vec ) { return vec.data(); } template constexpr const TObj *End( const std::vector &vec ) { return Begin( vec ) + vec.size(); } template constexpr TIter FindSimple( TIter pFirst, TIter pEnd, const TObj &obj ) { while ( pFirst != pEnd && *pFirst != obj ) ++pFirst; return pFirst; } template constexpr TIter FindByFour( TIter pFirst, TIter pEnd, const TObj &obj ) { typename std::iterator_traits< TIter >::difference_type ulTripCount = ( pEnd - pFirst ) >> 2; while ( ulTripCount-- > 0 ) { if ( pFirst[0] == obj ) return &pFirst[0]; if ( pFirst[1] == obj ) return &pFirst[1]; if ( pFirst[2] == obj ) return &pFirst[2]; if ( pFirst[3] == obj ) return &pFirst[3]; pFirst += 4; } switch ( pEnd - pFirst ) { case 3: { if ( pFirst[0] == obj ) return &pFirst[0]; if ( pFirst[1] == obj ) return &pFirst[1]; if ( pFirst[2] == obj ) return &pFirst[2]; return pEnd; } case 2: { if ( pFirst[0] == obj ) return &pFirst[0]; if ( pFirst[1] == obj ) return &pFirst[1]; return pEnd; } case 1: { if ( pFirst[0] == obj ) return &pFirst[0]; return pEnd; } case 0: { return pEnd; } default: { __builtin_unreachable(); } } } template constexpr TIter Find( TIter pFirst, TIter pEnd, const TObj &obj ) { return FindSimple( pFirst, pEnd, obj ); } template constexpr TIter Find( std::span span, const TObj &obj ) { return Find( Begin( span ), End( span ), obj ); } template constexpr TIter Find( const std::vector &vec, const TObj &obj ) { return Find( Begin( vec ), End( vec ), obj ); } template constexpr bool ContainsShortcut( TIter pFirst, TIter pEnd, const TObj &obj ) { return Find( pFirst, pEnd, obj ) != pEnd; } template constexpr bool ContainsNoShortcut( TIter pFirst, TIter pEnd, const TObj &obj ) { bool bFound = false; typename std::iterator_traits< TIter >::difference_type ulTripCount = ( pEnd - pFirst ) >> 2; while ( ulTripCount-- > 0 ) { bFound |= pFirst[0] == obj || pFirst[1] == obj || pFirst[2] == obj || pFirst[3] == obj; pFirst += 4; } switch ( pEnd - pFirst ) { case 3: { bFound |= pFirst[0] == obj || pFirst[1] == obj || pFirst[2] == obj; break; } case 2: { bFound |= pFirst[0] == obj || pFirst[1] == obj; break; } case 1: { bFound |= pFirst[0] == obj; break; } case 0: { break; } default: { __builtin_unreachable(); } } return bFound; } template constexpr bool Contains( TIter pFirst, TIter pEnd, const TObj &obj ) { return ContainsNoShortcut( pFirst, pEnd, obj ); } template constexpr bool Contains( std::span span, const TObj &obj ) { return Contains( Begin( span ), End( span ), obj ); } template constexpr bool Contains( const std::vector &vec, const TObj &obj ) { return Contains( Begin( vec ), End( vec ), obj ); } } ValveSoftware-gamescope-eb620ab/src/Utils/Defer.h000066400000000000000000000010341502457270500220200ustar00rootroot00000000000000#pragma once #include namespace gamescope { template class DeferHelper { public: DeferHelper( Func fnFunc ) : m_fnFunc{ std::move( fnFunc ) } { } ~DeferHelper() { m_fnFunc(); } private: Func m_fnFunc; }; } #define DEFER_1(x, y) x##y #define DEFER_2(x, y) DEFER_1(x, y) #define DEFER_3(x) DEFER_2(x, __COUNTER__) #define defer(code) auto DEFER_3(_defer_) = ::gamescope::DeferHelper( [&](){ code; } ) ValveSoftware-gamescope-eb620ab/src/Utils/Dict.h000066400000000000000000000014401502457270500216570ustar00rootroot00000000000000#pragma once #include #include #include #include namespace gamescope { struct StringHash { using is_transparent = void; [[nodiscard]] size_t operator()( const char *string ) const { return std::hash{}( string ); } [[nodiscard]] size_t operator()( std::string_view string ) const { return std::hash{}( string ); } [[nodiscard]] size_t operator()( const std::string &string ) const { return std::hash{}( string ); } }; template using Dict = std::unordered_map>; template using MultiDict = std::unordered_multimap>; } ValveSoftware-gamescope-eb620ab/src/Utils/NonCopyable.h000066400000000000000000000003441502457270500232070ustar00rootroot00000000000000#pragma once namespace gamescope { class NonCopyable { public: NonCopyable() = default; NonCopyable(const NonCopyable &) = delete; void operator = (const NonCopyable &) = delete; }; } ValveSoftware-gamescope-eb620ab/src/Utils/Process.cpp000066400000000000000000000340611502457270500227520ustar00rootroot00000000000000#include "Process.h" #include "../Utils/Algorithm.h" #include "../convar.h" #include "../log.hpp" #include "../Utils/Defer.h" #include #include #include #include #include #include #include #if defined(__linux__) #include #include #elif defined(__DragonFly__) || defined(__FreeBSD__) #include #endif #include #include #include #include #include #include #include #include extern const char *__progname; static LogScope s_ProcessLog( "process" ); namespace gamescope::Process { static bool IsDigit( char chChar ) { return chChar >= '0' && chChar <= '9'; } void BecomeSubreaper() { #if defined(__linux__) prctl( PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0 ); #elif defined(__DragonFly__) || defined(__FreeBSD__) procctl(P_PID, getpid(), PROC_REAP_ACQUIRE, NULL); #else #warning "Changing reaper process for children is not supported on this platform" #endif } void SetDeathSignal( int nSignal ) { #if defined( __linux__ ) // Kill myself when my parent dies. prctl( PR_SET_PDEATHSIG, SIGTERM, 0, 0, 0 ); #else #warning "Setting death signal is not supported on this platform" #endif } std::vector GetChildPids( pid_t nPid ) { std::vector nPids; DIR *pProcDir = opendir( "/proc" ); if ( !pProcDir ) { s_ProcessLog.errorf( "Failed to open /proc" ); return {}; } defer( closedir( pProcDir ) ); struct dirent *pEntry; while ( ( pEntry = readdir( pProcDir ) ) ) { if ( pEntry->d_type != DT_DIR ) continue; if ( !IsDigit( pEntry->d_name[0] ) ) continue; char szPath[ PATH_MAX ]; snprintf( szPath, sizeof( szPath ), "/proc/%s/stat", pEntry->d_name ); FILE *pStatFile = fopen( szPath, "r" ); if ( !pStatFile ) continue; defer( fclose( pStatFile ) ); pid_t nParentPid = -1; fscanf( pStatFile, "%*d %*s %*c %d", &nParentPid ); if ( nParentPid > 0 && nParentPid == nPid ) nPids.push_back( *Parse( pEntry->d_name ) ); } return nPids; } void KillProcessTree( std::vector nPids, int nSignal ) { for ( pid_t nPid : nPids ) { auto nChildPids = GetChildPids( nPid ); KillProcess( nPid, nSignal ); KillProcessTree( nChildPids, nSignal ); } } void KillAllChildren( pid_t nParentPid, int nSignal ) { std::vector nChildPids = GetChildPids( nParentPid ); return KillProcessTree( nChildPids, nSignal ); } void KillProcess( pid_t nPid, int nSignal ) { if ( kill( nPid, nSignal ) == -1 ) { if ( errno == ESRCH ) { // Process already terminated. } else { s_ProcessLog.errorf_errno( "Failed to kill process %d", nPid ); } } } std::optional WaitForChild( pid_t nPid ) { for ( ;; ) { int nStatus = 0; if ( waitpid( nPid, &nStatus, 0 ) == -1 ) { if ( errno == EINTR ) continue; if ( errno != ECHILD ) { s_ProcessLog.errorf_errno( "Wait for primary child failed." ); } return std::nullopt; } else { return nStatus; } } } bool WaitForAllChildren( std::optional onStopPid ) { for ( ;; ) { int nStatus = 0; pid_t nDeadChild = waitpid( -1, &nStatus, 0 ); if ( onStopPid && nDeadChild == *onStopPid ) return true; if ( nDeadChild == -1 && errno == ECHILD ) return false; } } static std::optional g_oOriginalFDLimit{}; void RaiseFdLimit() { if ( g_oOriginalFDLimit ) { s_ProcessLog.errorf( "FD Limit already raised!" ); return; } rlimit originalLimit{}; if ( getrlimit( RLIMIT_NOFILE, &originalLimit ) != 0 ) { s_ProcessLog.errorf( "Could not query maximum number of open files. Leaving at default value." ); return; } if ( originalLimit.rlim_cur >= originalLimit.rlim_max ) { // Already at max. return; } rlimit newLimit = originalLimit; newLimit.rlim_cur = newLimit.rlim_max; if ( setrlimit( RLIMIT_NOFILE, &newLimit ) ) { s_ProcessLog.errorf( "Failed to raise the maximum number of open files. Leaving at default value." ); return; } g_oOriginalFDLimit = originalLimit; } void RestoreFdLimit() { if ( !g_oOriginalFDLimit ) return; if ( setrlimit( RLIMIT_NOFILE, &*g_oOriginalFDLimit ) ) { s_ProcessLog.errorf( "Failed to reset the maximum number of open files in child process." ); s_ProcessLog.errorf( "Use of select() may fail." ); return; } g_oOriginalFDLimit = std::nullopt; } void ResetSignals() { sigset_t set; sigemptyset( &set ); sigprocmask( SIG_SETMASK, &set, nullptr ); } static void ProcessPreSpawn() { ResetSignals(); RestoreFdLimit(); RestoreNice(); RestoreRealtime(); } bool CloseFd( int nFd ) { for ( ;; ) { if ( close( nFd ) == 0 ) { return true; } else { if ( errno == EINTR ) continue; s_ProcessLog.errorf_errno( "CloseFd failed to close FD %d", nFd ); return false; } } } void CloseAllFds( std::span nExcludedFds ) { DIR *pProcDir = opendir( "/proc/self/fd" ); if ( !pProcDir ) { s_ProcessLog.errorf( "Failed to open /proc" ); return; } defer( closedir( pProcDir ) ); struct dirent *pEntry; while ( ( pEntry = readdir( pProcDir ) ) ) { std::optional onFd = Parse( pEntry->d_name ); if ( !onFd ) continue; int nFd = *onFd; bool bExcluded = Algorithm::Contains( nExcludedFds, nFd ); if ( bExcluded ) continue; if ( !CloseFd( nFd ) ) { s_ProcessLog.errorf_errno( "CloseAllFds failed to close FD %d", nFd ); } } } pid_t SpawnProcess( char **argv, std::function fnPreambleInChild, bool bDoubleFork ) { // Create a pipe for the child to return the grandchild's // PID into. int nPidPipe[2] = { -1, -1 }; if ( bDoubleFork ) { if ( pipe2( nPidPipe, O_CLOEXEC | O_NONBLOCK ) != 0 ) { s_ProcessLog.errorf( "Failed to create PID pipe" ); return -1; } } pid_t nChild = fork(); if ( nChild < 0 ) { if ( bDoubleFork ) { CloseFd( nPidPipe[0] ); CloseFd( nPidPipe[1] ); } s_ProcessLog.errorf_errno( "Failed to fork() child" ); return -1; } else if ( nChild == 0 ) { std::array nExcludedFds = {{ STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO, nPidPipe[0], // -1 if !bDoubleFork, which is fine. nPidPipe[1], }}; CloseAllFds( nExcludedFds ); ProcessPreSpawn(); if ( bDoubleFork ) { // Don't need the read pipe anymore. CloseFd( nPidPipe[0] ); } if ( fnPreambleInChild ) fnPreambleInChild(); if ( bDoubleFork ) { pid_t nGrandChild = fork(); if ( nGrandChild == 0 ) { CloseFd( nPidPipe[1] ); if ( execvp( argv[0], argv ) == -1 ) { s_ProcessLog.errorf_errno( "Failed to start process \"%s\"", argv[0] ); } _exit( 0 ); } else if ( nGrandChild < 0 ) { s_ProcessLog.errorf_errno( "Failed to fork() grandchild." ); } ssize_t sszRet = write( nPidPipe[1], &nGrandChild, sizeof( nGrandChild ) ); (void) sszRet; // Cannot handle this error here, it is checked on the other side anyway. CloseFd( nPidPipe[1] ); _exit( 0 ); } else { if ( execvp( argv[0], argv ) == -1 ) { s_ProcessLog.errorf_errno( "Failed to start process \"%s\"", argv[0] ); } _exit( 0 ); } } // Parent Path // ... if ( bDoubleFork ) { // Wait for the immediate child to exit, as all it does // is fork to spawn a child to orphan. WaitForChild( nChild ); // Now that the child process is done it must have written fully to the pipe. // Read the PID back from the pipe and close it. pid_t nGrandChild = 0; ssize_t sszAmountRead = read( nPidPipe[0], &nGrandChild, sizeof( nGrandChild ) ); CloseFd( nPidPipe[0] ); CloseFd( nPidPipe[1] ); // Sanity check what we got from the pipe. if ( sszAmountRead != sizeof( nGrandChild ) ) return -1; return nGrandChild; } else { return nChild; } } pid_t SpawnProcessInWatchdog( char **argv, bool bRespawn, std::function fnPreambleInChild ) { std::vector args; args.push_back( (char *)"gamescopereaper" ); if ( bRespawn ) args.push_back( (char *)"--respawn" ); args.push_back( (char *)"--" ); while ( *argv ) { args.push_back( *argv ); argv++; } args.push_back( NULL ); return SpawnProcess( args.data(), fnPreambleInChild ); } bool HasCapSysNice() { #if defined(__linux__) && HAVE_LIBCAP static bool s_bHasCapSysNice = []() -> bool { cap_t pCaps = cap_get_proc(); if ( !pCaps ) return false; defer( cap_free( pCaps ) ); cap_flag_value_t eNiceCapValue = CAP_CLEAR; if ( cap_get_flag( pCaps, CAP_SYS_NICE, CAP_EFFECTIVE, &eNiceCapValue ) != 0 ) return false; return eNiceCapValue == CAP_SET; }(); return s_bHasCapSysNice; #else return false; #endif } std::optional g_oOldNice; std::optional g_oNewNice; void SetNice( int nNice ) { #if defined(__linux__) if ( !HasCapSysNice() ) return; errno = 0; int nOldNice = nice( 0 ); if ( nOldNice != -1 || errno == 0 ) { g_oOldNice = nOldNice; } errno = 0; int nNewNice = nice( -20 ); if ( nNewNice != -1 || errno == 0 ) { g_oNewNice = nNewNice; } #endif } void RestoreNice() { #if defined(__linux__) if ( !HasCapSysNice() ) return; if ( !g_oOldNice || !g_oNewNice ) return; if ( *g_oOldNice == *g_oNewNice ) return; errno = 0; int nNewNice = nice( *g_oOldNice - *g_oNewNice ); if ( g_oNewNice != -1 || errno == 0 ) { g_oNewNice = nNewNice; } if ( g_oOldNice == g_oNewNice ) { g_oOldNice = std::nullopt; g_oNewNice = std::nullopt; } else { s_ProcessLog.errorf( "RestoreNice: Old Nice != New Nice" ); } #endif } struct SchedulerInfo { int nPolicy; struct sched_param SchedParam; static std::optional Get() { SchedulerInfo info{}; if ( pthread_getschedparam( pthread_self(), &info.nPolicy, &info.SchedParam) ) { s_ProcessLog.errorf_errno( "Failed to get old scheduler info." ); return std::nullopt; } return info; } }; std::optional g_oOldSchedulerInfo; bool SetRealtime() { #if defined(__linux__) if ( !HasCapSysNice() ) return false; g_oOldSchedulerInfo = SchedulerInfo::Get(); if ( !g_oOldSchedulerInfo ) return false; struct sched_param newSched{}; sched_getparam( 0, &newSched ); newSched.sched_priority = sched_get_priority_min( SCHED_RR ); if ( pthread_setschedparam( pthread_self(), SCHED_RR, &newSched ) ) { s_ProcessLog.errorf_errno( "Failed to set realtime scheduling." ); return false; } return true; #endif } void RestoreRealtime() { #if defined(__linux__) if ( !HasCapSysNice() ) return; if ( !g_oOldSchedulerInfo ) return; if ( pthread_setschedparam( pthread_self(), g_oOldSchedulerInfo->nPolicy, &g_oOldSchedulerInfo->SchedParam ) ) { s_ProcessLog.errorf_errno( "Failed to restore from realtime scheduling." ); return; } g_oOldSchedulerInfo = std::nullopt; #endif } const char *GetProcessName() { return __progname; } } ValveSoftware-gamescope-eb620ab/src/Utils/Process.h000066400000000000000000000023531502457270500224160ustar00rootroot00000000000000#pragma once #include #include #include #include namespace gamescope::Process { void BecomeSubreaper(); void SetDeathSignal( int nSignal ); void KillAllChildren( pid_t nParentPid, int nSignal ); void KillProcess( pid_t nPid, int nSignal ); std::optional WaitForChild( pid_t nPid ); // Wait for all children to die, // but stop waiting if we hit a specific PID specified by onStopPid. // Returns true if we stopped because we hit the pid specified by onStopPid. // // Similar to what an `init` process would do. bool WaitForAllChildren( std::optional onStopPid = std::nullopt ); bool CloseFd( int nFd ); void RaiseFdLimit(); void RestoreFdLimit(); void ResetSignals(); void CloseAllFds( std::span nExcludedFds ); pid_t SpawnProcess( char **argv, std::function fnPreambleInChild = nullptr, bool bDoubleFork = false ); pid_t SpawnProcessInWatchdog( char **argv, bool bRespawn = false, std::function fnPreambleInChild = nullptr ); bool HasCapSysNice(); void SetNice( int nNice ); void RestoreNice(); bool SetRealtime(); void RestoreRealtime(); const char *GetProcessName(); }ValveSoftware-gamescope-eb620ab/src/Utils/TempFiles.cpp000066400000000000000000000037261502457270500232300ustar00rootroot00000000000000#include #include #include #include #include #include #include #include "TempFiles.h" namespace gamescope { class CDeferUnlinks { public: void Add( std::string sPath ) { m_DeferredUnlinks.emplace_front( sPath ); } private: class CDeferUnlink { public: CDeferUnlink( std::string sPath ) : m_sPath{ std::move( sPath ) } { } ~CDeferUnlink() { unlink( m_sPath.c_str() ); } private: const std::string m_sPath; }; std::list m_DeferredUnlinks; }; static CDeferUnlinks s_DeferredUnlinks; int MakeTempFile( char ( &pszOutPath )[ PATH_MAX ], const char *pszTemplate, bool bDeferUnlink ) { const char *pXDGPath = getenv( "XDG_RUNTIME_DIR" ); if ( !pXDGPath || !*pXDGPath ) return -1; snprintf( pszOutPath, PATH_MAX, "%s/%s", pXDGPath, pszTemplate ); // Overwrites pszOutPath with the file path. int nFd = mkostemp( pszOutPath, O_CLOEXEC ); if ( nFd < 0 ) return -1; // Unlink so it gets destroyed when Gamescope dies. if ( bDeferUnlink ) { s_DeferredUnlinks.Add( pszOutPath ); } else { unlink( pszOutPath ); } return nFd; } FILE *MakeTempFile( char ( &pszOutPath )[ PATH_MAX ], const char *pszTemplate, const char *pszMode, bool bDeferUnlink ) { int nFd = MakeTempFile( pszOutPath, pszTemplate, bDeferUnlink ); if ( nFd < 0 ) return nullptr; FILE *pFile = fdopen( nFd, pszMode ); if ( !pFile ) { close( nFd ); return nullptr; } // fclose will close the file **and** the underlying file descriptor. return pFile; } }ValveSoftware-gamescope-eb620ab/src/Utils/TempFiles.h000066400000000000000000000012771502457270500226740ustar00rootroot00000000000000#pragma once #include #include namespace gamescope { static constexpr const char k_szGamescopeTempFileTemplate[] = "gamescope-temp-XXXXXXXX"; static constexpr const char k_szGamescopeTempShmTemplate[] = "gamescope-shm-XXXXXXXX"; static constexpr const char k_szGamescopeTempMangoappTemplate[] = "gamescope-mangoapp-XXXXXXXX"; static constexpr const char k_szGamescopeTempLimiterTemplate[] = "gamescope-limiter-XXXXXXXX"; int MakeTempFile( char ( &pszOutPath )[ PATH_MAX ], const char *pszTemplate, bool bDeferUnlink = false ); FILE *MakeTempFile( char ( &pszOutPath )[ PATH_MAX ], const char *pszTemplate, const char *pszMode, bool bDeferUnlink = false ); }ValveSoftware-gamescope-eb620ab/src/Utils/Version.cpp000066400000000000000000000004041502457270500227530ustar00rootroot00000000000000#include "Version.h" #include "Process.h" #include "GamescopeVersion.h" #include "convar.h" namespace gamescope { void PrintVersion() { console_log.infof( "%s version %s", Process::GetProcessName(), gamescope::k_szGamescopeVersion ); } }ValveSoftware-gamescope-eb620ab/src/Utils/Version.h000066400000000000000000000000771502457270500224260ustar00rootroot00000000000000#pragma once namespace gamescope { void PrintVersion(); } ValveSoftware-gamescope-eb620ab/src/WaylandServer/000077500000000000000000000000001502457270500223125ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/src/WaylandServer/GamescopeActionBinding.h000066400000000000000000000156731502457270500270330ustar00rootroot00000000000000#pragma once #include "WaylandProtocol.h" #include "gamescope-action-binding-protocol.h" #include "../log.hpp" #include #include #include #include #include #include "convar.h" #include "Utils/Algorithm.h" #include "wlr_begin.hpp" #include #include #include "wlr_end.hpp" using namespace std::literals; uint64_t get_time_in_nanos(); inline LogScope log_binding( "binding" ); static inline std::string ComputeDebugName( const std::unordered_set &syms ) { std::string sTriggerDebugName; bool bFirst = true; for ( xkb_keysym_t uKeySym : syms ) { if ( !bFirst ) { sTriggerDebugName += " + "; } bFirst = false; char szName[256] = ""; if ( xkb_keysym_get_name( uKeySym, szName, sizeof( szName ) ) <= 0 ) { snprintf( szName, sizeof( szName ), "xkb_keysym_t( 0x%x )", uKeySym ); } sTriggerDebugName += szName; } return sTriggerDebugName; } static constexpr std::pair k_mapKeysymRemapping[] { // BUG: normalize to the same tab key. // Sometimes when the keyboard comes online we'll receive key events as // one of these two. We need to investigate why. For now lets just normalize // to one of the two. // TODO: A proper solution would probably be to have a set comparison that // takes into account aliased characters, or uses a fully normalized set of // chars. { XKB_KEY_ISO_Left_Tab, XKB_KEY_Tab }, { XKB_KEY_ISO_Enter, XKB_KEY_Return }, { XKB_KEY_Meta_L, XKB_KEY_Super_L }, { XKB_KEY_Meta_R, XKB_KEY_Super_R }, { XKB_KEY_ISO_Level3_Shift, XKB_KEY_Alt_R }, }; static inline xkb_keysym_t NormalizeKeysymForHotkey( xkb_keysym_t uKeySym ) { uKeySym = xkb_keysym_to_upper( uKeySym ); for ( auto [ uBadKey, uGoodKey ] : k_mapKeysymRemapping ) { if ( uKeySym == uBadKey ) uKeySym = uGoodKey; } return uKeySym; } namespace gamescope::WaylandServer { struct Keybind_t { std::unordered_set setKeySyms; std::string sDebugName; }; /////////////////////////// // CGamescopeActionBinding /////////////////////////// class CGamescopeActionBinding : public CWaylandResource { public: WL_PROTO_DEFINE( gamescope_action_binding, 1 ); CGamescopeActionBinding( WaylandResourceDesc_t desc ) : CWaylandResource( desc ) { s_Bindings.push_back( this ); } ~CGamescopeActionBinding() { std::erase_if( s_Bindings, [this]( CGamescopeActionBinding *pBinding ){ return pBinding == this; } ); } // gamescope_action_binding void SetDescription( const char *pszDescription ) { m_sDescription = pszDescription; } void AddKeyboardTrigger( wl_array *pKeysymsArray ) { size_t zKeysymCount = pKeysymsArray->size / sizeof( xkb_keysym_t ); std::span pKeysyms = std::span { reinterpret_cast( pKeysymsArray->data ), zKeysymCount }; std::unordered_set setKeySyms; for ( xkb_keysym_t uKeySym : pKeysyms ) { setKeySyms.emplace( NormalizeKeysymForHotkey( uKeySym ) ); } std::string sTriggerDebugName = ComputeDebugName( setKeySyms ); log_binding.infof( "(%s) -> Adding new trigger [%s].", m_sDescription.c_str(), sTriggerDebugName.c_str() ); m_KeyboardTriggers.emplace_back( std::move( setKeySyms ), std::move( sTriggerDebugName ) ); } void ClearTriggers() { log_binding.infof( "(%s) -> Cleared triggers.", m_sDescription.c_str() ); m_KeyboardTriggers.clear(); } void Arm( uint32_t uArmFlags ) { log_binding.debugf( "(%s) -> Arming: %x.", m_sDescription.c_str(), uArmFlags ); m_ouArmFlags = uArmFlags; } void Disarm() { log_binding.debugf( "(%s) -> Disarming.", m_sDescription.c_str() ); m_ouArmFlags = std::nullopt; } // bool IsArmed() { return m_ouArmFlags != std::nullopt; } std::span GetKeyboardTriggers() { return m_KeyboardTriggers; } bool Execute() { if ( !IsArmed() ) return false; uint32_t uArmFlags = *m_ouArmFlags; bool bBlockInput = !!( uArmFlags & GAMESCOPE_ACTION_BINDING_ARM_FLAG_NO_BLOCK ); uint32_t uTriggerFlags = GAMESCOPE_ACTION_BINDING_TRIGGER_FLAG_KEYBOARD; uint64_t ulNow = get_time_in_nanos(); static uint32_t s_uSequence = 0; uint32_t uTimeLo = static_cast( ulNow & 0xffffffff ); uint32_t uTimeHi = static_cast( ulNow >> 32 ); log_binding.debugf( "(%s) -> Triggered!", m_sDescription.c_str() ); gamescope_action_binding_send_triggered( GetResource(), s_uSequence++, uTriggerFlags, uTimeLo, uTimeHi ); if ( uArmFlags & GAMESCOPE_ACTION_BINDING_ARM_FLAG_ONE_SHOT ) { log_binding.debugf( "(%s) -> Disarming due to one-shot.", m_sDescription.c_str() ); Disarm(); } return bBlockInput; } static std::span GetBindings() { return s_Bindings; } private: std::string m_sDescription; std::vector m_KeyboardTriggers; std::optional m_ouArmFlags; static std::vector s_Bindings; }; const struct gamescope_action_binding_interface CGamescopeActionBinding::Implementation = { .destroy = WL_PROTO_DESTROY(), .set_description = WL_PROTO( CGamescopeActionBinding, SetDescription ), .add_keyboard_trigger = WL_PROTO( CGamescopeActionBinding, AddKeyboardTrigger ), .clear_triggers = WL_PROTO( CGamescopeActionBinding, ClearTriggers ), .arm = WL_PROTO( CGamescopeActionBinding, Arm ), .disarm = WL_PROTO( CGamescopeActionBinding, Disarm ), }; std::vector CGamescopeActionBinding::s_Bindings; ////////////////////////////////// // CGamescopeActionBindingManager ////////////////////////////////// class CGamescopeActionBindingManager : public CWaylandResource { public: WL_PROTO_DEFINE( gamescope_action_binding_manager, 1 ); WL_PROTO_DEFAULT_CONSTRUCTOR(); void CreateActionBinding( uint32_t uId ) { CWaylandResource::Create( m_pClient, m_uVersion, uId ); } }; const struct gamescope_action_binding_manager_interface CGamescopeActionBindingManager::Implementation = { .destroy = WL_PROTO_DESTROY(), .create_action_binding = WL_PROTO( CGamescopeActionBindingManager, CreateActionBinding ), }; } ValveSoftware-gamescope-eb620ab/src/WaylandServer/LinuxDrmSyncobj.h000066400000000000000000000114431502457270500255600ustar00rootroot00000000000000#include "WaylandProtocol.h" #include "WaylandServerLegacy.h" #include "../Timeline.h" #include "linux-drm-syncobj-v1-protocol.h" namespace gamescope::WaylandServer { class CLinuxDrmSyncobjTimeline : public CWaylandResource { public: WL_PROTO_DEFINE( wp_linux_drm_syncobj_timeline_v1, 1 ); CLinuxDrmSyncobjTimeline( WaylandResourceDesc_t desc, std::shared_ptr pTimeline ) : CWaylandResource( desc ) , m_pTimeline{ std::move( pTimeline ) } { } std::shared_ptr GetTimeline() const { return m_pTimeline; } private: std::shared_ptr m_pTimeline; }; const struct wp_linux_drm_syncobj_timeline_v1_interface CLinuxDrmSyncobjTimeline::Implementation = { .destroy = WL_PROTO_DESTROY(), }; class CLinuxDrmSyncobjSurface : public CWaylandResource { public: WL_PROTO_DEFINE( wp_linux_drm_syncobj_surface_v1, 1 ); CLinuxDrmSyncobjSurface( WaylandResourceDesc_t desc, wlserver_wl_surface_info *pWlSurfaceInfo ) : CWaylandResource( desc ) , m_pWlSurfaceInfo{ pWlSurfaceInfo } { } ~CLinuxDrmSyncobjSurface() { assert( m_pWlSurfaceInfo->pSyncobjSurface == this ); m_pWlSurfaceInfo->pSyncobjSurface = nullptr; } bool HasExplicitSync() const { return m_pAcquireTimeline || m_pReleaseTimeline; } std::shared_ptr ExtractAcquireTimelinePoint() const { if ( !m_pAcquireTimeline ) return nullptr; return std::make_shared( m_pAcquireTimeline, m_ulAcquirePoint ); } std::shared_ptr ExtractReleaseTimelinePoint() const { if ( !m_pReleaseTimeline ) return nullptr; return std::make_shared( m_pReleaseTimeline, m_ulReleasePoint ); } protected: void SetAcquirePoint( wl_resource *pWlTimeline, uint32_t uPointHi, uint32_t uPointLo ) { if ( !pWlTimeline ) { m_pAcquireTimeline = nullptr; return; } CLinuxDrmSyncobjTimeline *pTimeline = CWaylandResource::FromWlResource( pWlTimeline ); m_pAcquireTimeline = pTimeline->GetTimeline(); m_ulAcquirePoint = ToUint64( uPointHi, uPointLo ); } void SetReleasePoint( wl_resource *pWlTimeline, uint32_t uPointHi, uint32_t uPointLo ) { if ( !pWlTimeline ) { m_pReleaseTimeline = nullptr; return; } CLinuxDrmSyncobjTimeline *pTimeline = CWaylandResource::FromWlResource( pWlTimeline ); m_pReleaseTimeline = pTimeline->GetTimeline(); m_ulReleasePoint = ToUint64( uPointHi, uPointLo ); } private: wlserver_wl_surface_info *m_pWlSurfaceInfo = nullptr; std::shared_ptr m_pAcquireTimeline; uint64_t m_ulAcquirePoint = 0; std::shared_ptr m_pReleaseTimeline; uint64_t m_ulReleasePoint = 0; }; const struct wp_linux_drm_syncobj_surface_v1_interface CLinuxDrmSyncobjSurface::Implementation = { .destroy = WL_PROTO_DESTROY(), .set_acquire_point = WL_PROTO( CLinuxDrmSyncobjSurface, SetAcquirePoint ), .set_release_point = WL_PROTO( CLinuxDrmSyncobjSurface, SetReleasePoint ), }; class CLinuxDrmSyncobjManager : public CWaylandResource { public: WL_PROTO_DEFINE( wp_linux_drm_syncobj_manager_v1, 1 ); WL_PROTO_DEFAULT_CONSTRUCTOR(); void GetSurface( uint32_t uId, wl_resource *pSurfaceResource ) { struct wlr_surface *pWlrSurface = wlr_surface_from_resource( pSurfaceResource ); struct wlserver_wl_surface_info *pWlSurfaceInfo = get_wl_surface_info( pWlrSurface ); if ( !pWlSurfaceInfo ) { wl_client_post_implementation_error( m_pClient, "No wl_surface" ); return; } if ( pWlSurfaceInfo->pSyncobjSurface ) { wl_resource_post_error( m_pResource, WP_LINUX_DRM_SYNCOBJ_MANAGER_V1_ERROR_SURFACE_EXISTS, "wp_linux_drm_syncobj_surface_v1 already created for this surface" ); return; } pWlSurfaceInfo->pSyncobjSurface = CWaylandResource::Create( m_pClient, m_uVersion, uId, pWlSurfaceInfo ); } void ImportTimeline( uint32_t uId, int32_t nFd ) { // Transfer nFd to a new CTimeline. std::shared_ptr pTimeline = std::make_shared( nFd ); if ( !CheckAllocation( pTimeline ) ) return; if ( !pTimeline->IsValid() ) { wl_resource_post_error( m_pResource, WP_LINUX_DRM_SYNCOBJ_MANAGER_V1_ERROR_INVALID_TIMELINE, "Failed to import syncobj timeline" ); return; } CWaylandResource::Create( m_pClient, m_uVersion, uId, std::move( pTimeline ) ); } }; const struct wp_linux_drm_syncobj_manager_v1_interface CLinuxDrmSyncobjManager::Implementation = { .destroy = WL_PROTO_DESTROY(), .get_surface = WL_PROTO( CLinuxDrmSyncobjManager, GetSurface ), .import_timeline = WL_PROTO( CLinuxDrmSyncobjManager, ImportTimeline ), }; } ValveSoftware-gamescope-eb620ab/src/WaylandServer/Reshade.h000066400000000000000000000025561502457270500240460ustar00rootroot00000000000000#pragma once #include "WaylandProtocol.h" #include "gamescope-reshade-protocol.h" #include "reshade_effect_manager.hpp" #include namespace gamescope::WaylandServer { class CReshadeManager : public CWaylandResource { public: WL_PROTO_DEFINE( gamescope_reshade, 1 ); WL_PROTO_DEFAULT_CONSTRUCTOR(); void EffectReadyCallback( const char* path ) { gamescope_reshade_send_effect_ready(GetResource(), path); } void SetEffect( const char *path ) { reshade_effect_manager_set_effect( path, [this]( const char* callbackPath ) { EffectReadyCallback( callbackPath ); } ); } void EnableEffect() { reshade_effect_manager_enable_effect(); } void SetUniformVariable( const char *key, struct wl_array *value ) { uint8_t* data_copy = new uint8_t[value->size]; std::memcpy(data_copy, value->data, value->size); reshade_effect_manager_set_uniform_variable( key, data_copy ); } void DisableEffect() { reshade_effect_manager_disable_effect(); } }; const struct gamescope_reshade_interface CReshadeManager::Implementation = { .destroy = WL_PROTO_DESTROY(), .set_effect = WL_PROTO( CReshadeManager, SetEffect ), .enable_effect = WL_PROTO( CReshadeManager, EnableEffect ), .set_uniform_variable = WL_PROTO( CReshadeManager, SetUniformVariable ), .disable_effect = WL_PROTO( CReshadeManager, DisableEffect ) }; } ValveSoftware-gamescope-eb620ab/src/WaylandServer/WaylandDecls.h000066400000000000000000000010351502457270500250340ustar00rootroot00000000000000#pragma once namespace gamescope::WaylandServer { class CWaylandResource; template class CWaylandProtocol; class CLinuxDrmSyncobjManager; class CLinuxDrmSyncobjSurface; class CLinuxDrmSyncobjTimeline; using CLinuxDrmSyncobj = CWaylandProtocol; class CReshadeManager; using CReshade = CWaylandProtocol; class CGamescopeActionBindingManager; using CGamescopeActionBindingProtocol = CWaylandProtocol; } ValveSoftware-gamescope-eb620ab/src/WaylandServer/WaylandProtocol.h000066400000000000000000000020121502457270500255770ustar00rootroot00000000000000#pragma once #include "WaylandResource.h" #include namespace gamescope::WaylandServer { template class CWaylandProtocol : public NonCopyable { public: CWaylandProtocol( wl_display *pDisplay ) : m_pDisplay{ pDisplay } { ( CreateGlobal(), ... ); } private: template void CreateGlobal() { wl_global *pGlobal = wl_global_create( m_pDisplay, T::Interface, T::Version, this, []( struct wl_client *pClient, void *pData, uint32_t uVersion, uint32_t uId ) { CWaylandProtocol *pProtocol = reinterpret_cast( pData ); pProtocol->Bind( pClient, uVersion, uId ); } ); m_pGlobals.emplace_back( pGlobal ); } template void Bind( struct wl_client *pClient, uint32_t uVersion, uint32_t uId ) { CWaylandResource::Create( pClient, uVersion, uId ); } wl_display *m_pDisplay = nullptr; std::vector m_pGlobals; }; } ValveSoftware-gamescope-eb620ab/src/WaylandServer/WaylandResource.h000066400000000000000000000056251502457270500256020ustar00rootroot00000000000000#pragma once #include #include #include "../Utils/NonCopyable.h" namespace gamescope::WaylandServer { #define WL_PROTO_NULL() [] ( Args... args ) { } #define WL_PROTO_DESTROY() [] ( wl_client *pClient, wl_resource *pResource, Args... args ) { wl_resource_destroy( pResource ); } #define WL_PROTO( type, name ) \ [] ( wl_client *pClient, wl_resource *pResource, Args... args ) \ { \ type *pThing = reinterpret_cast( wl_resource_get_user_data( pResource ) ); \ pThing->name( std::forward(args)... ); \ } #define WL_PROTO_DEFINE( type, version ) \ static constexpr uint32_t Version = version; \ static constexpr const struct wl_interface *Interface = & type##_interface; \ static const struct type##_interface Implementation; #define WL_PROTO_DEFAULT_CONSTRUCTOR() \ using CWaylandResource::CWaylandResource; struct WaylandResourceDesc_t { wl_client *pClient; wl_resource *pResource; uint32_t uVersion; }; class CWaylandResource : public NonCopyable { public: CWaylandResource( WaylandResourceDesc_t desc ) : m_pClient{ desc.pClient } , m_pResource{ desc.pResource } , m_uVersion{ desc.uVersion } { } ~CWaylandResource() { } template static bool CheckAllocation( const T &object, wl_client *pClient ) { if ( !object ) { wl_client_post_no_memory( pClient ); return false; } return true; } template bool CheckAllocation( const T &object ) { return CheckAllocation( object, m_pClient ); } template static T *FromWlResource( wl_resource *pResource ) { T *pObject = reinterpret_cast( wl_resource_get_user_data( pResource ) ); return pObject; } template static T *Create( wl_client *pClient, uint32_t uVersion, uint32_t uId, Args... args ) { wl_resource *pResource = wl_resource_create( pClient, T::Interface, uVersion, uId ); if ( !CheckAllocation( pResource, pClient ) ) return nullptr; WaylandResourceDesc_t desc = { .pClient = pClient, .pResource = pResource, .uVersion = uVersion, }; T *pThing = new T{ desc, std::forward(args)... }; if ( !CheckAllocation( pThing, pClient ) ) return nullptr; wl_resource_set_implementation( pResource, &T::Implementation, pThing, []( wl_resource *pResource ) { T *pObject = CWaylandResource::FromWlResource( pResource ); delete pObject; }); return pThing; } static uint64_t ToUint64( uint32_t uHi, uint32_t uLo ) { return (uint64_t(uHi) << 32) | uLo; } wl_client *GetClient() const { return m_pClient; } wl_resource *GetResource() const { return m_pResource; } uint32_t GetVersion() const { return m_uVersion; } protected: wl_client *m_pClient = nullptr; wl_resource *m_pResource = nullptr; uint32_t m_uVersion = 0; }; } ValveSoftware-gamescope-eb620ab/src/WaylandServer/WaylandServerLegacy.h000066400000000000000000000027401502457270500264010ustar00rootroot00000000000000#pragma once #include #include "WaylandDecls.h" #include #include #include #include "vulkan_include.h" #include "wlr_begin.hpp" #include #include "wlr_end.hpp" namespace gamescope { class BackendBlob; } struct wlserver_x11_surface_info; struct wlserver_xdg_surface_info; struct wlserver_vk_swapchain_feedback { uint32_t image_count; VkFormat vk_format; VkColorSpaceKHR vk_colorspace; VkCompositeAlphaFlagBitsKHR vk_composite_alpha; VkSurfaceTransformFlagBitsKHR vk_pre_transform; VkBool32 vk_clipped; std::shared_ptr vk_engine_name; std::shared_ptr hdr_metadata_blob; }; struct wlserver_wl_surface_info { wlserver_x11_surface_info *x11_surface = nullptr; wlserver_xdg_surface_info *xdg_surface = nullptr; gamescope::WaylandServer::CLinuxDrmSyncobjSurface *pSyncobjSurface = nullptr; struct wlr_surface *wlr = nullptr; struct wl_listener commit; struct wl_listener destroy; std::shared_ptr swapchain_feedback = {}; std::optional oCurrentPresentMode; uint64_t sequence = 0; std::vector pending_presentation_feedbacks; std::vector gamescope_swapchains; std::optional present_id = std::nullopt; uint64_t desired_present_time = 0; uint64_t last_refresh_cycle = 0; }; wlserver_wl_surface_info *get_wl_surface_info(struct wlr_surface *wlr_surf); ValveSoftware-gamescope-eb620ab/src/backend.cpp000066400000000000000000000153131502457270500216220ustar00rootroot00000000000000#include "backend.h" #include "vblankmanager.hpp" #include "convar.h" #include "wlserver.hpp" #include "wlr_begin.hpp" #include #include "wlr_end.hpp" extern void sleep_until_nanos(uint64_t nanos); extern bool env_to_bool(const char *env); namespace gamescope { ConVar cv_backend_virtual_connector_strategy( "backend_virtual_connector_strategy", VirtualConnectorStrategies::SingleApplication ); ///////////// // IBackend ///////////// static IBackend *s_pBackend = nullptr; IBackend *IBackend::Get() { return s_pBackend; } bool IBackend::Set( IBackend *pBackend ) { if ( s_pBackend ) { delete s_pBackend; s_pBackend = nullptr; } if ( pBackend ) { s_pBackend = pBackend; if ( !s_pBackend->Init() ) { delete s_pBackend; s_pBackend = nullptr; return false; } } return true; } ///////////////// // CBaseBackendFb ///////////////// CBaseBackendFb::CBaseBackendFb() { } CBaseBackendFb::~CBaseBackendFb() { // I do not own the client buffer, but I released that in DecRef. //assert( !HasLiveReferences() ); } uint32_t CBaseBackendFb::IncRef() { uint32_t uRefCount = IBackendFb::IncRef(); if ( m_pClientBuffer && !uRefCount ) { wlserver_lock(); wlr_buffer_lock( m_pClientBuffer ); wlserver_unlock( false ); } return uRefCount; } uint32_t CBaseBackendFb::DecRef() { wlr_buffer *pClientBuffer = m_pClientBuffer; std::shared_ptr pReleasePoint = std::move( m_pReleasePoint ); m_pReleasePoint = nullptr; uint32_t uRefCount = IBackendFb::DecRef(); if ( uRefCount ) { if ( pReleasePoint ) m_pReleasePoint = std::move( pReleasePoint ); } else if ( pClientBuffer ) { wlserver_lock(); wlr_buffer_unlock( pClientBuffer ); wlserver_unlock(); } return uRefCount; } void CBaseBackendFb::SetBuffer( wlr_buffer *pClientBuffer ) { if ( m_pClientBuffer == pClientBuffer ) return; assert( m_pClientBuffer == nullptr ); m_pClientBuffer = pClientBuffer; if ( GetRefCount() ) { wlserver_lock(); wlr_buffer_lock( m_pClientBuffer ); wlserver_unlock( false ); } m_pReleasePoint = nullptr; } void CBaseBackendFb::SetReleasePoint( std::shared_ptr pReleasePoint ) { m_pReleasePoint = pReleasePoint; if ( m_pClientBuffer && GetRefCount() ) { wlserver_lock(); wlr_buffer_unlock( m_pClientBuffer ); wlserver_unlock(); m_pClientBuffer = nullptr; } } ///////////////////////// // CBaseBackendConnector ///////////////////////// VBlankScheduleTime CBaseBackendConnector::FrameSync() { VBlankScheduleTime schedule = GetVBlankTimer().CalcNextWakeupTime( false ); sleep_until_nanos( schedule.ulScheduledWakeupPoint ); return schedule; } ///////////////// // CBaseBackend ///////////////// bool CBaseBackend::NeedsFrameSync() const { const bool bForceTimerFd = env_to_bool( getenv( "GAMESCOPE_DISABLE_TIMERFD" ) ); return bForceTimerFd; } ConVar cv_touch_external_display_trackpad( "touch_external_display_trackpad", false, "If we are using an external display, should we treat the internal display's touch as a trackpad insteaad?" ); ConVar cv_touch_click_mode( "touch_click_mode", TouchClickModes::Left, "The default action to perform on touch." ); TouchClickMode CBaseBackend::GetTouchClickMode() { if ( cv_touch_external_display_trackpad && this->GetCurrentConnector() ) { gamescope::GamescopeScreenType screenType = this->GetCurrentConnector()->GetScreenType(); if ( screenType == gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL && cv_touch_click_mode == TouchClickMode::Passthrough ) return TouchClickMode::Trackpad; } return cv_touch_click_mode; } void CBaseBackend::DumpDebugInfo() { console_log.infof( "Uses Modifiers: %s", this->UsesModifiers() ? "true" : "false" ); console_log.infof( "Supports Plane Hardware Cursor: %s (not relevant for nested backends)", this->SupportsPlaneHardwareCursor() ? "true" : "false" ); console_log.infof( "Supports Tearing: %s", this->SupportsTearing() ? "true" : "false" ); console_log.infof( "Uses Vulkan Swapchain: %s", this->UsesVulkanSwapchain() ? "true" : "false" ); console_log.infof( "Is Session Based: %s", this->IsSessionBased() ? "true" : "false" ); console_log.infof( "Supports Explicit Sync: %s", this->SupportsExplicitSync() ? "true" : "false" ); console_log.infof( "Current Screen Type: %s", this->GetScreenType() == GAMESCOPE_SCREEN_TYPE_INTERNAL ? "Internal" : "External" ); console_log.infof( "Is Visible: %s", this->IsVisible() ? "true" : "false" ); console_log.infof( "Is Paused: %s", this->IsPaused() ? "true" : "false" ); console_log.infof( "Needs Frame Sync: %s", this->NeedsFrameSync() ? "true" : "false" ); console_log.infof( "VRR Active: %s", this->GetCurrentConnector()->IsVRRActive() ? "true" : "false" ); console_log.infof( "Total Presents Queued: %lu", this->GetCurrentConnector()->PresentationFeedback().TotalPresentsQueued() ); console_log.infof( "Total Presents Completed: %lu", this->GetCurrentConnector()->PresentationFeedback().TotalPresentsCompleted() ); console_log.infof( "Current Presents In Flight: %lu", this->GetCurrentConnector()->PresentationFeedback().CurrentPresentsInFlight() ); } bool CBaseBackend::UsesVirtualConnectors() { return false; } std::shared_ptr CBaseBackend::CreateVirtualConnector( uint64_t ulVirtualConnectorKey ) { assert( false ); return nullptr; } ConCommand cc_backend_info( "backend_info", "Dump debug info about the backend state", []( std::span svArgs ) { if ( !GetBackend() ) return; GetBackend()->DumpDebugInfo(); }); ConCommand cc_backend_set_dirty( "backend_set_dirty", "Dirty the backend state and re-poll", []( std::span svArgs ) { if ( !GetBackend() ) return; GetBackend()->DirtyState( true, true ); }); } ValveSoftware-gamescope-eb620ab/src/backend.h000066400000000000000000000350611502457270500212710ustar00rootroot00000000000000#pragma once #include "color_helpers.h" #include "gamescope_shared.h" #include "vulkan_include.h" #include "Timeline.h" #include "convar.h" #include "rc.h" #include "drm_include.h" #include "Utils/Algorithm.h" #include #include #include #include #include #include #include struct wlr_buffer; struct wlr_dmabuf_attributes; struct FrameInfo_t; extern bool steamMode; namespace gamescope { struct VBlankScheduleTime; class BackendBlob; class INestedHints; namespace VirtualConnectorStrategies { enum VirtualConnectorStrategy : uint32_t { SingleApplication, SteamControlled, PerAppId, PerWindow, Count, }; } using VirtualConnectorStrategy = VirtualConnectorStrategies::VirtualConnectorStrategy; using VirtualConnectorKey_t = uint64_t; extern ConVar cv_backend_virtual_connector_strategy; static constexpr bool VirtualConnectorStrategyIsSingleOutput( VirtualConnectorStrategy eStrategy ) { return eStrategy == VirtualConnectorStrategies::SingleApplication || eStrategy == VirtualConnectorStrategies::SteamControlled; } static inline bool VirtualConnectorIsSingleOutput() { return VirtualConnectorStrategyIsSingleOutput( cv_backend_virtual_connector_strategy ); } static inline bool VirtualConnectorInSteamPerAppState() { return steamMode && cv_backend_virtual_connector_strategy == gamescope::VirtualConnectorStrategies::PerAppId; } static inline bool VirtualConnectorKeyIsSteam( VirtualConnectorKey_t ulKey ) { return VirtualConnectorInSteamPerAppState() && ulKey == 769; } static inline std::string_view VirtualConnectorStrategyToString( VirtualConnectorStrategy eStrategy ) { switch ( eStrategy ) { case VirtualConnectorStrategies::SingleApplication: return "SingleApplication"; case VirtualConnectorStrategies::SteamControlled: return "SteamControlled"; case VirtualConnectorStrategies::PerAppId: return "PerAppId"; case VirtualConnectorStrategies::PerWindow: return "PerWindow"; default: return "Unknown"; } } enum class InputType { Mouse, }; namespace TouchClickModes { enum TouchClickMode : uint32_t { Hover, Left, Right, Middle, Passthrough, Disabled, Trackpad, }; } using TouchClickMode = TouchClickModes::TouchClickMode; struct BackendConnectorHDRInfo { // We still want to set up HDR info for Steam Deck LCD with some good // target/mapping values for the display brightness for undocking from a HDR display, // but don't want to expose HDR there as it is not good. bool bExposeHDRSupport = false; bool bAlwaysPatchEdid = false; // The output encoding to use for HDR output. // For typical HDR10 displays, this will be PQ. // For displays doing "traditional HDR" such as Steam Deck OLED, this is Gamma 2.2. EOTF eOutputEncodingEOTF = EOTF_Gamma22; uint16_t uMaxContentLightLevel = 500; // Nits uint16_t uMaxFrameAverageLuminance = 500; // Nits uint16_t uMinContentLightLevel = 0; // Nits / 10000 std::shared_ptr pDefaultMetadataBlob; bool IsHDRG22() const { return bExposeHDRSupport && eOutputEncodingEOTF == EOTF_Gamma22; } bool ShouldPatchEDID() const { return bAlwaysPatchEdid || IsHDRG22(); } bool IsHDR10() const { // PQ output encoding is always HDR10 (PQ + 2020) for us. // If that assumption changes, update me. return bExposeHDRSupport && eOutputEncodingEOTF == EOTF_PQ; } }; struct BackendMode { uint32_t uWidth; uint32_t uHeight; uint32_t uRefresh; // Hz }; struct BackendPresentFeedback { public: uint64_t CurrentPresentsInFlight() const { return TotalPresentsQueued() - TotalPresentsCompleted(); } // Across the lifetime of the backend. uint64_t TotalPresentsQueued() const { return m_uQueuedPresents.load(); } uint64_t TotalPresentsCompleted() const { return m_uCompletedPresents.load(); } std::atomic m_uQueuedPresents = { 0u }; std::atomic m_uCompletedPresents = { 0u }; }; class IBackendConnector { public: virtual ~IBackendConnector() {} virtual uint64_t GetConnectorID() const = 0; virtual GamescopeScreenType GetScreenType() const = 0; virtual GamescopePanelOrientation GetCurrentOrientation() const = 0; virtual bool SupportsHDR() const = 0; virtual bool IsHDRActive() const = 0; virtual const BackendConnectorHDRInfo &GetHDRInfo() const = 0; virtual bool IsVRRActive() const = 0; virtual std::span GetModes() const = 0; virtual bool SupportsVRR() const = 0; virtual std::span GetRawEDID() const = 0; virtual std::span GetValidDynamicRefreshRates() const = 0; virtual void GetNativeColorimetry( bool bHDR10, displaycolorimetry_t *displayColorimetry, EOTF *displayEOTF, displaycolorimetry_t *outputEncodingColorimetry, EOTF *outputEncodingEOTF ) const = 0; virtual const char *GetName() const = 0; virtual const char *GetMake() const = 0; virtual const char *GetModel() const = 0; virtual int Present( const FrameInfo_t *pFrameInfo, bool bAsync ) = 0; virtual VBlankScheduleTime FrameSync() = 0; virtual BackendPresentFeedback& PresentationFeedback() = 0; virtual uint64_t GetVirtualConnectorKey() const = 0; virtual INestedHints *GetNestedHints() = 0; }; class CBaseBackendConnector : public IBackendConnector { public: CBaseBackendConnector() { AssignConnectorId(); } CBaseBackendConnector( uint64_t ulVirtualConnectorKey ) : m_ulVirtualConnectorKey{ ulVirtualConnectorKey } { AssignConnectorId(); } virtual ~CBaseBackendConnector() { } virtual uint64_t GetConnectorID() const override { return m_ulConnectorId; } virtual VBlankScheduleTime FrameSync() override; virtual BackendPresentFeedback& PresentationFeedback() override { return m_PresentFeedback; } virtual uint64_t GetVirtualConnectorKey() const override { return m_ulVirtualConnectorKey; } virtual INestedHints *GetNestedHints() override { return nullptr; } protected: uint64_t m_ulConnectorId = 0; uint64_t m_ulVirtualConnectorKey = 0; BackendPresentFeedback m_PresentFeedback{}; private: void AssignConnectorId() { static uint64_t s_ulLastConnectorId = 0; m_ulConnectorId = ++s_ulLastConnectorId; } }; class INestedHints { public: virtual ~INestedHints() {} struct CursorInfo { std::vector pPixels; uint32_t uWidth; uint32_t uHeight; uint32_t uXHotspot; uint32_t uYHotspot; }; virtual void SetCursorImage( std::shared_ptr info ) = 0; virtual void SetRelativeMouseMode( bool bRelative ) = 0; virtual void SetVisible( bool bVisible ) = 0; virtual void SetTitle( std::shared_ptr szTitle ) = 0; virtual void SetIcon( std::shared_ptr> uIconPixels ) = 0; virtual void SetSelection( std::shared_ptr szContents, GamescopeSelection eSelection ) = 0; }; class IBackendFb : public IRcObject { public: virtual void SetBuffer( wlr_buffer *pClientBuffer ) = 0; virtual void SetReleasePoint( std::shared_ptr pReleasePoint ) = 0; }; class CBaseBackendFb : public IBackendFb { public: CBaseBackendFb(); virtual ~CBaseBackendFb(); uint32_t IncRef() override; uint32_t DecRef() override; void SetBuffer( wlr_buffer *pClientBuffer ) override; void SetReleasePoint( std::shared_ptr pReleasePoint ) override; private: wlr_buffer *m_pClientBuffer = nullptr; std::shared_ptr m_pReleasePoint; }; class IBackend { public: virtual ~IBackend() {} virtual bool Init() = 0; virtual bool PostInit() = 0; virtual std::span GetInstanceExtensions() const = 0; virtual std::span GetDeviceExtensions( VkPhysicalDevice pVkPhysicalDevice ) const = 0; virtual VkImageLayout GetPresentLayout() const = 0; virtual void GetPreferredOutputFormat( uint32_t *pPrimaryPlaneFormat, uint32_t *pOverlayPlaneFormat ) const = 0; virtual bool ValidPhysicalDevice( VkPhysicalDevice pVkPhysicalDevice ) const = 0; virtual void DirtyState( bool bForce = false, bool bForceModeset = false ) = 0; virtual bool PollState() = 0; virtual std::shared_ptr CreateBackendBlob( const std::type_info &type, std::span data ) = 0; template std::shared_ptr CreateBackendBlob( const T& thing ) { const uint8_t *pBegin = reinterpret_cast( &thing ); const uint8_t *pEnd = pBegin + sizeof( T ); return CreateBackendBlob( typeid( T ), std::span( pBegin, pEnd ) ); } // For DRM, this is // dmabuf -> fb_id. // // shared_ptr owns the structure. // Rc manages acquire/release of buffer to/from client while imported. virtual OwningRc ImportDmabufToBackend( wlr_buffer *pBuffer, wlr_dmabuf_attributes *pDmaBuf ) = 0; virtual bool UsesModifiers() const = 0; virtual std::span GetSupportedModifiers( uint32_t uDrmFormat ) const = 0; inline bool SupportsFormat( uint32_t uDrmFormat ) const { return Algorithm::Contains( this->GetSupportedModifiers( uDrmFormat ), DRM_FORMAT_MOD_INVALID ); } virtual IBackendConnector *GetCurrentConnector() = 0; virtual IBackendConnector *GetConnector( GamescopeScreenType eScreenType ) = 0; virtual bool SupportsPlaneHardwareCursor() const = 0; virtual bool SupportsTearing() const = 0; virtual bool UsesVulkanSwapchain() const = 0; virtual bool IsSessionBased() const = 0; virtual bool SupportsExplicitSync() const = 0; // Dumb helper we should remove to support multi display someday. gamescope::GamescopeScreenType GetScreenType() { if ( GetCurrentConnector() ) return GetCurrentConnector()->GetScreenType(); return gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL; } virtual bool IsPaused() const = 0; virtual bool IsVisible() const = 0; virtual glm::uvec2 CursorSurfaceSize( glm::uvec2 uvecSize ) const = 0; // This will move to the connector and be deprecated soon. virtual bool HackTemporarySetDynamicRefresh( int nRefresh ) = 0; virtual void HackUpdatePatchedEdid() = 0; virtual bool NeedsFrameSync() const = 0; virtual TouchClickMode GetTouchClickMode() = 0; virtual void DumpDebugInfo() = 0; virtual bool UsesVirtualConnectors() = 0; virtual std::shared_ptr CreateVirtualConnector( uint64_t ulVirtualConnectorKey ) = 0; virtual void NotifyPhysicalInput( InputType eInputType ) = 0; static IBackend *Get(); template static bool Set(); static bool Set( IBackend *pBackend ); protected: friend BackendBlob; virtual void OnBackendBlobDestroyed( BackendBlob *pBlob ) = 0; private: }; class CBaseBackend : public IBackend { public: virtual bool HackTemporarySetDynamicRefresh( int nRefresh ) override { return false; } virtual void HackUpdatePatchedEdid() override {} virtual bool NeedsFrameSync() const override; virtual TouchClickMode GetTouchClickMode() override; virtual void DumpDebugInfo() override; virtual bool UsesVirtualConnectors() override; virtual std::shared_ptr CreateVirtualConnector( uint64_t ulVirtualConnectorKey ) override; virtual void NotifyPhysicalInput( InputType eInputType ) override {} }; // This is a blob of data that may be associated with // a backend if it needs to be. // Currently on non-DRM backends this is basically a // no-op. class BackendBlob { public: BackendBlob() { } BackendBlob( std::span data ) : m_Data( data.begin(), data.end() ) { } BackendBlob( std::span data, uint32_t uBlob, bool bOwned ) : m_Data( data.begin(), data.end() ) , m_uBlob( uBlob ) , m_bOwned( bOwned ) { } ~BackendBlob() { if ( m_bOwned ) { IBackend *pBackend = IBackend::Get(); if ( pBackend ) pBackend->OnBackendBlobDestroyed( this ); } } // No copy constructor, because we can't duplicate the blob handle. BackendBlob( const BackendBlob& ) = delete; BackendBlob& operator=( const BackendBlob& ) = delete; // No move constructor, because we use shared_ptr anyway, but can be added if necessary. BackendBlob( BackendBlob&& ) = delete; BackendBlob& operator=( BackendBlob&& ) = delete; std::span GetData() const { return std::span( m_Data.begin(), m_Data.end() ); } template const T& View() const { assert( sizeof( T ) == m_Data.size() ); return *reinterpret_cast( m_Data.data() ); } uint32_t GetBlobValue() const { return m_uBlob; } private: std::vector m_Data; uint32_t m_uBlob = 0; bool m_bOwned = false; }; extern ConVar cv_touch_click_mode; } inline gamescope::IBackend *GetBackend() { return gamescope::IBackend::Get(); } ValveSoftware-gamescope-eb620ab/src/backends.h000066400000000000000000000005421502457270500214500ustar00rootroot00000000000000#pragma once namespace gamescope { // Backend enum. enum GamescopeBackend { Auto, DRM, SDL, OpenVR, Headless, Wayland, }; // Backend forward declarations. class CSDLBackend; class CDRMBackend; class COpenVRBackend; class CHeadlessBackend; class CWaylandBackend; } ValveSoftware-gamescope-eb620ab/src/color_bench.cpp000066400000000000000000000152361502457270500225140ustar00rootroot00000000000000#include #include #include #include "Utils/Algorithm.h" #include "color_helpers_impl.h" using color_bench::nLutEdgeSize3d; using color_bench::nLutSize1d; uint16_t lut1d[nLutSize1d*4]; uint16_t lut3d[nLutEdgeSize3d*nLutEdgeSize3d*nLutEdgeSize3d*4]; lut1d_t lut1d_float; lut3d_t lut3d_float; static void BenchmarkCalcColorTransform(EOTF inputEOTF, benchmark::State &state) { const primaries_t primaries = { { 0.602f, 0.355f }, { 0.340f, 0.574f }, { 0.164f, 0.121f } }; const glm::vec2 white = { 0.3070f, 0.3220f }; const glm::vec2 destVirtualWhite = { 0.f, 0.f }; displaycolorimetry_t inputColorimetry{}; inputColorimetry.primaries = primaries; inputColorimetry.white = white; displaycolorimetry_t outputEncodingColorimetry{}; outputEncodingColorimetry.primaries = primaries; outputEncodingColorimetry.white = white; colormapping_t colorMapping{}; tonemapping_t tonemapping{}; tonemapping.bUseShaper = true; nightmode_t nightmode{}; float flGain = 1.0f; for (auto _ : state) { calcColorTransform( &lut1d_float, nLutSize1d, &lut3d_float, inputColorimetry, inputEOTF, outputEncodingColorimetry, EOTF_Gamma22, destVirtualWhite, k_EChromaticAdapatationMethod_XYZ, colorMapping, nightmode, tonemapping, nullptr, flGain ); for ( size_t i=0, end = lut1d_float.dataR.size(); i static __attribute__((noinline)) std::array GetFindTestValues() { static std::array s_Values = []() { std::array values; for ( uint32_t i = 0; i < uSize; i++ ) values[i] = rand() % 255; return values; }(); return s_Values; } // Large static void Benchmark_Find_Large_Gamescope(benchmark::State &state) { std::array values = GetFindTestValues(); for (auto _ : state) { auto iter = gamescope::Algorithm::Find( values.begin(), values.end(), 765678478 ); benchmark::DoNotOptimize( iter ); } } BENCHMARK(Benchmark_Find_Large_Gamescope); static void Benchmark_Find_Large_Std(benchmark::State &state) { std::array values = GetFindTestValues(); for (auto _ : state) { auto iter = std::find( values.begin(), values.end(), 765678478 ); benchmark::DoNotOptimize( iter ); } } BENCHMARK(Benchmark_Find_Large_Std); static void Benchmark_Contains_Large_Gamescope(benchmark::State &state) { std::array values = GetFindTestValues(); for (auto _ : state) { bool bContains = gamescope::Algorithm::ContainsNoShortcut( values.begin(), values.end(), 765678478 ); benchmark::DoNotOptimize( bContains ); } } BENCHMARK(Benchmark_Contains_Large_Gamescope); // static void Benchmark_Find_Medium_Gamescope(benchmark::State &state) { std::array values = GetFindTestValues(); for (auto _ : state) { auto iter = gamescope::Algorithm::Find( values.begin(), values.end(), 765678478 ); benchmark::DoNotOptimize( iter ); } } BENCHMARK(Benchmark_Find_Medium_Gamescope); static void Benchmark_Find_Medium_Std(benchmark::State &state) { std::array values = GetFindTestValues(); for (auto _ : state) { auto iter = std::find( values.begin(), values.end(), 765678478 ); benchmark::DoNotOptimize( iter ); } } BENCHMARK(Benchmark_Find_Medium_Std); static void Benchmark_Contains_Medium_Gamescope(benchmark::State &state) { std::array values = GetFindTestValues(); for (auto _ : state) { bool bContains = gamescope::Algorithm::ContainsNoShortcut( values.begin(), values.end(), 765678478 ); benchmark::DoNotOptimize( bContains ); } } BENCHMARK(Benchmark_Contains_Medium_Gamescope); // static void Benchmark_Find_Small_Gamescope(benchmark::State &state) { std::array values = GetFindTestValues(); for (auto _ : state) { auto iter = gamescope::Algorithm::Find( values.begin(), values.end(), 765678478 ); benchmark::DoNotOptimize( iter ); } } BENCHMARK(Benchmark_Find_Small_Gamescope); static void Benchmark_Find_Small_Std(benchmark::State &state) { std::array values = GetFindTestValues(); for (auto _ : state) { auto iter = std::find( values.begin(), values.end(), 765678478 ); benchmark::DoNotOptimize( iter ); } } BENCHMARK(Benchmark_Find_Small_Std); static void Benchmark_Contains_Small_Gamescope(benchmark::State &state) { std::array values = GetFindTestValues(); for (auto _ : state) { bool bContains = gamescope::Algorithm::ContainsNoShortcut( values.begin(), values.end(), 765678478 ); benchmark::DoNotOptimize( bContains ); } } BENCHMARK(Benchmark_Contains_Small_Gamescope); BENCHMARK_MAIN(); ValveSoftware-gamescope-eb620ab/src/color_helpers.cpp000066400000000000000000001021741502457270500230750ustar00rootroot00000000000000#define COLOR_HELPERS_CPP #include "color_helpers_impl.h" #include #include #include #include #include #include #include #include glm::vec3 xyY_to_XYZ( const glm::vec2 & xy, float Y ) { if ( fabsf( xy.y ) < std::numeric_limits::min() ) { return glm::vec3( 0.f ); } else { return glm::vec3( Y * xy.x / xy.y, Y, ( 1.f - xy.x - xy.y ) * Y / xy.y ); } } glm::vec2 XYZ_to_xy( const glm::vec3 & XYZ ) { float sum = ( XYZ.x + XYZ.y + XYZ.z ); if ( fabsf( sum ) < std::numeric_limits::min() ) { return glm::vec2( 0.f ); } else { return glm::vec2( XYZ.x / sum, XYZ.y / sum ); } } glm::vec3 xy_to_xyz( const glm::vec2 & xy ) { return glm::vec3( xy.x, xy.y, 1.f - xy.x - xy.y ); } // Convert xy to CIE 1976 u'v' glm::vec2 xy_to_uv( const glm::vec2 & xy ) { float denom = -2.f * xy.x + 12.f * xy.y + 3.f; if ( fabsf( denom ) < std::numeric_limits::min() ) { return glm::vec2( 0.f ); } return glm::vec2( 4.f * xy.x / denom, 9.f * xy.y / denom ); } // Convert CIE 1976 u'v' to xy glm::vec2 uv_to_xy( const glm::vec2 & uv ) { float denom = 6.f * uv.x - 16.f * uv.y + 12.f; if ( fabsf( denom ) < std::numeric_limits::min() ) { return glm::vec2( 0.f ); } return glm::vec2( 9.f * uv.x / denom, 4.f * uv.y / denom ); } glm::mat3 normalised_primary_matrix( const primaries_t & rgbPrimaries, const glm::vec2 & whitePrimary, float whiteLuminance ) { glm::mat3 matPrimaries( xy_to_xyz( rgbPrimaries.r ), xy_to_xyz( rgbPrimaries.g ), xy_to_xyz( rgbPrimaries.b ) ); glm::vec3 whiteXYZ = xyY_to_XYZ( whitePrimary, whiteLuminance ); glm::mat3 whiteScale = glm::diagonal3x3( glm::inverse( matPrimaries ) * whiteXYZ ); return matPrimaries * whiteScale; } glm::mat3 chromatic_adaptation_matrix( const glm::vec3 & sourceWhiteXYZ, const glm::vec3 & destWhiteXYZ, EChromaticAdaptationMethod eMethod ) { static const glm::mat3 k_matBradford( 0.8951f,-0.7502f, 0.0389f, 0.2664f,1.7135f, -0.0685f, -0.1614f, 0.0367f, 1.0296f ); glm::mat3 matAdaptation = eMethod == k_EChromaticAdapatationMethod_XYZ ? glm::diagonal3x3( glm::vec3(1,1,1) ) : k_matBradford; glm::vec3 coneResponseDest = matAdaptation * destWhiteXYZ; glm::vec3 coneResponseSource = matAdaptation * sourceWhiteXYZ; glm::vec3 scale = glm::vec3( coneResponseDest.x / coneResponseSource.x, coneResponseDest.y / coneResponseSource.y, coneResponseDest.z / coneResponseSource.z ); return glm::inverse( matAdaptation ) * glm::diagonal3x3( scale ) * matAdaptation; } displaycolorimetry_t lerp( const displaycolorimetry_t & a, const displaycolorimetry_t & b, float t ) { displaycolorimetry_t result; primaries_t a_uv, b_uv; a_uv.r = xy_to_uv( a.primaries.r ); a_uv.g = xy_to_uv( a.primaries.g ); a_uv.b = xy_to_uv( a.primaries.b ); b_uv.r = xy_to_uv( b.primaries.r ); b_uv.g = xy_to_uv( b.primaries.g ); b_uv.b = xy_to_uv( b.primaries.b ); glm::vec2 a_white = xy_to_uv( a.white ); glm::vec2 b_white = xy_to_uv( b.white ); result.primaries.r.x = flerp( a_uv.r.x, b_uv.r.x, t ); result.primaries.r.y = flerp( a_uv.r.y, b_uv.r.y, t ); result.primaries.g.x = flerp( a_uv.g.x, b_uv.g.x, t ); result.primaries.g.y = flerp( a_uv.g.y, b_uv.g.y, t ); result.primaries.b.x = flerp( a_uv.b.x, b_uv.b.x, t ); result.primaries.b.y = flerp( a_uv.b.y, b_uv.b.y, t ); result.white.x = flerp( a_white.x, b_white.x, t ); result.white.y = flerp( a_white.y, b_white.y, t ); result.primaries.r = uv_to_xy( result.primaries.r ); result.primaries.g = uv_to_xy( result.primaries.g ); result.primaries.b = uv_to_xy( result.primaries.b ); result.white = uv_to_xy( result.white ); return result; } colormapping_t lerp( const colormapping_t & a, const colormapping_t & b, float t ) { colormapping_t result; result.blendEnableMinSat = flerp( a.blendEnableMinSat, b.blendEnableMinSat, t ); result.blendEnableMaxSat = flerp( a.blendEnableMaxSat, b.blendEnableMaxSat, t ); result.blendAmountMin = flerp( a.blendAmountMin, b.blendAmountMin, t ); result.blendAmountMax = flerp( a.blendAmountMax, b.blendAmountMax, t ); return result; } inline bool close_enough(float a, float b, float epsilon = 0.001f) { return fabsf(a - b) <= epsilon; } std::shared_ptr LoadCubeLut( FILE *pFile, bool &bRaisesBlackLevelFloor ) { // R changes fastest // ... // LUT_3D_SIZE %d(lutEdgeSize) // %f %f %f // ... bRaisesBlackLevelFloor = false; std::shared_ptr lut3d = std::make_shared(); glm::vec3 blackFloor = glm::vec3{ 0.0f, 0.0f, 0.0f }; char line[2048]; while ( fgets( line, sizeof( line ), pFile ) ) { if ( lut3d->lutEdgeSize ) { glm::vec3 val; if ( sscanf( line, "%f %f %f", &val.r, &val.g, &val.b ) == 3 ) { if ( lut3d->data.empty() ) { blackFloor = val; } lut3d->data.push_back( val ); } } else if ( sscanf( line, "LUT_3D_SIZE %d", &lut3d->lutEdgeSize ) == 1 ) { if ( lut3d->lutEdgeSize < 2 || lut3d->lutEdgeSize > 128 ) // sanity check { return nullptr; } lut3d->data.reserve( lut3d->lutEdgeSize * lut3d->lutEdgeSize * lut3d->lutEdgeSize ); } } int nExpectedElements = lut3d->lutEdgeSize * lut3d->lutEdgeSize * lut3d->lutEdgeSize; bool bValid = ( nExpectedElements > 0 && ( nExpectedElements == (int) lut3d->data.size() ) ); if ( !bValid ) { return nullptr; } bRaisesBlackLevelFloor = !close_enough(blackFloor.x, 0.0f) || !close_enough(blackFloor.y, 0.0f) || !close_enough(blackFloor.z, 0.0f); return lut3d; } std::shared_ptr LoadCubeLut( const char *pchFileName, bool &bRaisesBlackLevelFloor ) { bRaisesBlackLevelFloor = false; FILE *pFile = fopen( pchFileName, "r" ); if ( !pFile ) return nullptr; return LoadCubeLut( pFile, bRaisesBlackLevelFloor ); } int GetLut3DIndexRedFastRGB(int indexR, int indexG, int indexB, int dim) { return (indexR + (int)dim * (indexG + (int)dim * indexB)); } // Linear inline void lerp_rgb(float* out, const float* a, const float* b, const float* z) { out[0] = (b[0] - a[0]) * z[0] + a[0]; out[1] = (b[1] - a[1]) * z[1] + a[1]; out[2] = (b[2] - a[2]) * z[2] + a[2]; } // Bilinear inline void lerp_rgb(float* out, const float* a, const float* b, const float* c, const float* d, const float* y, const float* z) { float v1[3]; float v2[3]; lerp_rgb(v1, a, b, z); lerp_rgb(v2, c, d, z); lerp_rgb(out, v1, v2, y); } // Trilinear inline void lerp_rgb(float* out, const float* a, const float* b, const float* c, const float* d, const float* e, const float* f, const float* g, const float* h, const float* x, const float* y, const float* z) { float v1[3]; float v2[3]; lerp_rgb(v1, a,b,c,d,y,z); lerp_rgb(v2, e,f,g,h,y,z); lerp_rgb(out, v1, v2, x); } inline float ClampAndSanitize( float a, float min, float max ) { #ifndef __FAST_MATH__ return std::isfinite( a ) ? std::min(std::max(min, a), max) : min; #else return std::min(std::max(min, a), max); #endif } // Adapted from: // https://github.com/AcademySoftwareFoundation/OpenColorIO/ops/lut3d/Lut3DOpCPU.cpp // License available in their repo and in our LICENSE file. inline glm::vec3 ApplyLut3D_Trilinear( const lut3d_t & lut3d, const glm::vec3 & input ) { const float dimMinusOne = float(lut3d.lutEdgeSize) - 1.f; float idx[3]; idx[0] = input.r * dimMinusOne; idx[1] = input.g * dimMinusOne; idx[2] = input.b * dimMinusOne; // NaNs become 0. idx[0] = ClampAndSanitize(idx[0], 0.f, dimMinusOne); idx[1] = ClampAndSanitize(idx[1], 0.f, dimMinusOne); idx[2] = ClampAndSanitize(idx[2], 0.f, dimMinusOne); int indexLow[3]; indexLow[0] = static_cast(std::floor(idx[0])); indexLow[1] = static_cast(std::floor(idx[1])); indexLow[2] = static_cast(std::floor(idx[2])); int indexHigh[3]; // When the idx is exactly equal to an index (e.g. 0,1,2...) // then the computation of highIdx is wrong. However, // the delta is then equal to zero (e.g. idx-lowIdx), // so the highIdx has no impact. indexHigh[0] = static_cast(std::ceil(idx[0])); indexHigh[1] = static_cast(std::ceil(idx[1])); indexHigh[2] = static_cast(std::ceil(idx[2])); float delta[3]; delta[0] = idx[0] - static_cast(indexLow[0]); delta[1] = idx[1] - static_cast(indexLow[1]); delta[2] = idx[2] - static_cast(indexLow[2]); // Compute index into LUT for surrounding corners const int n000 = GetLut3DIndexRedFastRGB(indexLow[0], indexLow[1], indexLow[2], lut3d.lutEdgeSize); const int n100 = GetLut3DIndexRedFastRGB(indexHigh[0], indexLow[1], indexLow[2], lut3d.lutEdgeSize); const int n010 = GetLut3DIndexRedFastRGB(indexLow[0], indexHigh[1], indexLow[2], lut3d.lutEdgeSize); const int n001 = GetLut3DIndexRedFastRGB(indexLow[0], indexLow[1], indexHigh[2], lut3d.lutEdgeSize); const int n110 = GetLut3DIndexRedFastRGB(indexHigh[0], indexHigh[1], indexLow[2], lut3d.lutEdgeSize); const int n101 = GetLut3DIndexRedFastRGB(indexHigh[0], indexLow[1], indexHigh[2], lut3d.lutEdgeSize); const int n011 = GetLut3DIndexRedFastRGB(indexLow[0], indexHigh[1], indexHigh[2], lut3d.lutEdgeSize); const int n111 = GetLut3DIndexRedFastRGB(indexHigh[0], indexHigh[1], indexHigh[2], lut3d.lutEdgeSize); float x[3], y[3], z[3]; x[0] = delta[0]; x[1] = delta[0]; x[2] = delta[0]; y[0] = delta[1]; y[1] = delta[1]; y[2] = delta[1]; z[0] = delta[2]; z[1] = delta[2]; z[2] = delta[2]; glm::vec3 out; lerp_rgb((float *) &out, (float *) &lut3d.data[n000].r, (float *) &lut3d.data[n001].r, (float *) &lut3d.data[n010].r, (float *) &lut3d.data[n011].r, (float *) &lut3d.data[n100].r, (float *) &lut3d.data[n101].r, (float *) &lut3d.data[n110].r, (float *) &lut3d.data[n111].r, x, y, z); return out; } inline glm::vec3 ApplyLut3D_Tetrahedral( const lut3d_t & lut3d, const glm::vec3 & input ) { const float dimMinusOne = float(lut3d.lutEdgeSize) - 1.f; float idx[3]; idx[0] = input.r * dimMinusOne; idx[1] = input.g * dimMinusOne; idx[2] = input.b * dimMinusOne; // NaNs become 0. idx[0] = ClampAndSanitize(idx[0], 0.f, dimMinusOne); idx[1] = ClampAndSanitize(idx[1], 0.f, dimMinusOne); idx[2] = ClampAndSanitize(idx[2], 0.f, dimMinusOne); int indexLow[3]; indexLow[0] = static_cast(std::floor(idx[0])); indexLow[1] = static_cast(std::floor(idx[1])); indexLow[2] = static_cast(std::floor(idx[2])); int indexHigh[3]; // When the idx is exactly equal to an index (e.g. 0,1,2...) // then the computation of highIdx is wrong. However, // the delta is then equal to zero (e.g. idx-lowIdx), // so the highIdx has no impact. indexHigh[0] = static_cast(std::ceil(idx[0])); indexHigh[1] = static_cast(std::ceil(idx[1])); indexHigh[2] = static_cast(std::ceil(idx[2])); float fx = idx[0] - static_cast(indexLow[0]); float fy = idx[1] - static_cast(indexLow[1]); float fz = idx[2] - static_cast(indexLow[2]); // Compute index into LUT for surrounding corners const int n000 = GetLut3DIndexRedFastRGB(indexLow[0], indexLow[1], indexLow[2], lut3d.lutEdgeSize); const int n100 = GetLut3DIndexRedFastRGB(indexHigh[0], indexLow[1], indexLow[2], lut3d.lutEdgeSize); const int n010 = GetLut3DIndexRedFastRGB(indexLow[0], indexHigh[1], indexLow[2], lut3d.lutEdgeSize); const int n001 = GetLut3DIndexRedFastRGB(indexLow[0], indexLow[1], indexHigh[2], lut3d.lutEdgeSize); const int n110 = GetLut3DIndexRedFastRGB(indexHigh[0], indexHigh[1], indexLow[2], lut3d.lutEdgeSize); const int n101 = GetLut3DIndexRedFastRGB(indexHigh[0], indexLow[1], indexHigh[2], lut3d.lutEdgeSize); const int n011 = GetLut3DIndexRedFastRGB(indexLow[0], indexHigh[1], indexHigh[2], lut3d.lutEdgeSize); const int n111 = GetLut3DIndexRedFastRGB(indexHigh[0], indexHigh[1], indexHigh[2], lut3d.lutEdgeSize); glm::vec3 out; if (fx > fy) { if (fy > fz) { out = (1 - fx) * lut3d.data[n000] + (fx - fy) * lut3d.data[n100] + (fy - fz) * lut3d.data[n110] + (fz) * lut3d.data[n111]; } else if (fx > fz) { out = (1 - fx) * lut3d.data[n000] + (fx - fz) * lut3d.data[n100] + (fz - fy) * lut3d.data[n101] + (fy) * lut3d.data[n111]; } else { out = (1 - fz) * lut3d.data[n000] + (fz - fx) * lut3d.data[n001] + (fx - fy) * lut3d.data[n101] + (fy) * lut3d.data[n111]; } } else { if (fz > fy) { out = (1 - fz) * lut3d.data[n000] + (fz - fy) * lut3d.data[n001] + (fy - fx) * lut3d.data[n011] + (fx) * lut3d.data[n111]; } else if (fz > fx) { out = (1 - fy) * lut3d.data[n000] + (fy - fz) * lut3d.data[n010] + (fz - fx) * lut3d.data[n011] + (fx) * lut3d.data[n111]; } else { out = (1 - fy) * lut3d.data[n000] + (fy - fx) * lut3d.data[n010] + (fx - fz) * lut3d.data[n110] + (fz) * lut3d.data[n111]; } } return out; } inline glm::vec3 ApplyLut1D_Linear( const lut1d_t & lut, const glm::vec3 & input ) { const float dimMinusOne = float(lut.lutSize) - 1.f; float idx[3]; idx[0] = input.r * dimMinusOne; idx[1] = input.g * dimMinusOne; idx[2] = input.b * dimMinusOne; // NaNs become 0. idx[0] = ClampAndSanitize(idx[0], 0.f, dimMinusOne); idx[1] = ClampAndSanitize(idx[1], 0.f, dimMinusOne); idx[2] = ClampAndSanitize(idx[2], 0.f, dimMinusOne); int indexLow[3]; indexLow[0] = static_cast(std::floor(idx[0])); indexLow[1] = static_cast(std::floor(idx[1])); indexLow[2] = static_cast(std::floor(idx[2])); int indexHigh[3]; // When the idx is exactly equal to an index (e.g. 0,1,2...) // then the computation of highIdx is wrong. However, // the delta is then equal to zero (e.g. idx-lowIdx), // so the highIdx has no impact. indexHigh[0] = static_cast(std::ceil(idx[0])); indexHigh[1] = static_cast(std::ceil(idx[1])); indexHigh[2] = static_cast(std::ceil(idx[2])); float delta[3]; delta[0] = idx[0] - static_cast(indexLow[0]); delta[1] = idx[1] - static_cast(indexLow[1]); delta[2] = idx[2] - static_cast(indexLow[2]); float vLow[3] = { lut.dataR[indexLow[0]], lut.dataG[indexLow[1]], lut.dataB[indexLow[2]] }; float vHigh[3] = { lut.dataR[indexHigh[0]], lut.dataG[indexHigh[1]], lut.dataB[indexHigh[2]] }; glm::vec3 out; lerp_rgb( (float *) &out, vLow, vHigh, delta ); return out; } // Calculate the inverse of a value resulting from linear interpolation // in a 1d LUT. // start: Pointer to the first effective LUT entry (end of flat spot). // startOffset: Distance between first LUT entry and start. // end: Pointer to the last effective LUT entry (start of flat spot). // scale: From LUT index units to outDepth units. // val: The value to invert. // Return the result that would produce val if used // in a forward linear interpolation in the LUT. inline float FindLutInv(const float * start, const float startOffset, const float * end, const float scale, const float val) { // Note that the LUT data pointed to by start/end must be in increasing order, // regardless of whether the original LUT was increasing or decreasing because // this function uses std::lower_bound(). // Clamp the value to the range of the LUT. const float cv = std::min( std::max( val, *start ), *end ); // std::lower_bound() // "Returns an iterator pointing to the first element in the range [first,last) // which does not compare less than val (but could be equal)." // (NB: This is correct using either end or end+1 since lower_bound will return a // value one greater than the second argument if no values in the array are >= cv.) // http://www.sgi.com/tech/stl/lower_bound.html const float* lowbound = std::lower_bound(start, end, cv); // lower_bound() returns first entry >= val so decrement it unless val == *start. if (lowbound > start) { --lowbound; } const float* highbound = lowbound; if (highbound < end) { ++highbound; } // Delta is the fractional distance of val between the adjacent LUT entries. float delta = 0.f; if (*highbound > *lowbound) { // (handle flat spots by leaving delta = 0) delta = (cv - *lowbound) / (*highbound - *lowbound); } // Inds is the index difference from the effective start to lowbound. const float inds = (float)( lowbound - start ); // Correct for the fact that start is not the beginning of the LUT if it // starts with a flat spot. // (NB: It may seem like lower_bound would automatically find the end of the // flat spot, so start could always simply be the start of the LUT, however // this fails when val equals the flat spot value.) const float totalInds = inds + startOffset; // Scale converts from units of [0,dim] to [0,outDepth]. return (totalInds + delta) * scale; } int FindNonFlatStartIndex( const std::vector & data ) { if ( !data.empty() ) { for ( size_t nIndex = 1; nIndex < data.size(); ++nIndex ) { if ( data[nIndex] != data[0] ) { return nIndex - 1; } } } return 0; } void lut1d_t::finalize() { startIndexR = FindNonFlatStartIndex( dataR ); startIndexG = FindNonFlatStartIndex( dataG ); startIndexB = FindNonFlatStartIndex( dataB ); } inline glm::vec3 ApplyLut1D_Inverse_Linear( const lut1d_t & lut, const glm::vec3 & input ) { // Disallow inverse if not finalized if ( lut.startIndexR < 0 ) { return glm::vec3( -1.f ); } return glm::vec3( FindLutInv( lut.dataR.data() + lut.startIndexR, lut.startIndexR, lut.dataR.data() + lut.dataR.size() - 1, 1.f / ( lut.dataR.size() - 1.f ), input.r ), FindLutInv( lut.dataG.data() + lut.startIndexG, lut.startIndexG, lut.dataG.data() + lut.dataG.size() - 1, 1.f / ( lut.dataG.size() - 1.f ), input.g ), FindLutInv( lut.dataB.data() + lut.startIndexB, lut.startIndexB, lut.dataB.data() + lut.dataB.size() - 1, 1.f / ( lut.dataB.size() - 1.f ), input.b ) ); } inline glm::vec3 hsv_to_rgb( const glm::vec3 & hsv ) { if ( fabsf( hsv.y ) < std::numeric_limits::min() ) { return glm::vec3( hsv.z ) ; } float flHue = positive_mod( hsv.x, 1.f ); flHue *= 6.f; int i = flHue; // integer part float f = flHue - i; // fractional part float p = hsv.z * ( 1.f - hsv.y ); float q = hsv.z * ( 1.f - hsv.y * f ); float t = hsv.z * ( 1.f - hsv.y * ( 1.f - f ) ); switch(i) { case 0: return glm::vec3( hsv.z, t, p ); break; case 1: return glm::vec3( q, hsv.z, p ); break; case 2: return glm::vec3( p, hsv.z, t ); break; case 3: return glm::vec3( p, q, hsv.z ); break; case 4: return glm::vec3( t, p, hsv.z ); break; case 5: return glm::vec3( hsv.z, p, q ); break; } return glm::vec3( 0 ); } inline glm::vec3 rgb_to_hsv( const glm::vec3 & rgb ) { float flMax = std::max( std::max( rgb.x, rgb.y ), rgb.z ); float flMin = std::min( std::min( rgb.x, rgb.y ), rgb.z ); float flDelta = flMax - flMin; glm::vec3 hsv; hsv.y = ( fabsf( flMax ) < std::numeric_limits::min() ) ? 0.f : flDelta / flMax; hsv.z = flMax; if (hsv.y == 0.f) { hsv.x = -1.0f; } else { if ( rgb.x == flMax ) { hsv.x = (rgb.y - rgb.z) / flDelta; } else if ( rgb.y == flMax ) { hsv.x = 2.f + ( rgb.z - rgb.x ) / flDelta; } else { hsv.x = 4.f + ( rgb.x - rgb.y ) / flDelta; } hsv.x /= 6.f; if ( hsv.x < 0.f ) { hsv.x += 1.f; } } return hsv; } bool BOutOfGamut( const glm::vec3 & color ) { return ( color.x<0.f || color.x > 1.f || color.y<0.f || color.y > 1.f || color.z<0.f || color.z > 1.f ); } template inline T calcEOTFToLinear( const T & input, EOTF eotf, const tonemapping_t & tonemapping ) { if ( eotf == EOTF_Gamma22 ) { return glm::pow( input, T( 2.2f ) ) * tonemapping.g22_luminance; } else if ( eotf == EOTF_PQ ) { return pq_to_nits( input ); } return T(0); } template inline T calcLinearToEOTF( const T & input, EOTF eotf, const tonemapping_t & tonemapping ) { if ( eotf == EOTF_Gamma22 ) { T val = input; if ( tonemapping.g22_luminance > 0.f ) { val = glm::clamp( input / tonemapping.g22_luminance, T( 0.f ), T( 1.f ) ); } return glm::pow( val, T( 1.f/2.2f ) ); } else if ( eotf == EOTF_PQ ) { return T( nits_to_pq(input) ); } return T(0); } // input is from 0->1 // TODO: use tone-mapping for white, black, contrast ratio bool g_bUseSourceEOTFForShaper = false; template inline T applyShaper( const T & input, EOTF source, EOTF dest, const tonemapping_t & tonemapping, float flGain ) { if ( ( source == dest && flGain == 1.f ) || !tonemapping.bUseShaper ) { return input; } T flLinear = flGain * calcEOTFToLinear( input, source, tonemapping ); flLinear = tonemapping.apply( flLinear ); return calcLinearToEOTF( flLinear, g_bUseSourceEOTFForShaper ? source : dest, tonemapping ); } bool g_bHuePreservationWhenClipping = false; template void calcColorTransform( lut1d_t * pShaper, int nLutSize1d, lut3d_t * pLut3d, const displaycolorimetry_t & source, EOTF sourceEOTF, const displaycolorimetry_t & dest, EOTF destEOTF, const glm::vec2 & destVirtualWhite, EChromaticAdaptationMethod eMethod, const colormapping_t & mapping, const nightmode_t & nightmode, const tonemapping_t & tonemapping, const lut3d_t * pLook, float flGain ) { // Generate shaper lut // Note: while this is typically a 1D approximation of our end to end transform, // it need not be! Conceptually this is just to determine the interpolation properties... // The 3d lut should be considered a 'matched' pair where the transform is only complete // when applying both. I.e., you can put ANY transform in here, and it should work. static constexpr int32_t nLutEdgeSize3d = static_cast(lutEdgeSize3d); if ( pShaper ) { float flScale = 1.f / ( (float) nLutSize1d - 1.f ); pShaper->resize( nLutSize1d ); for ( int nVal=0; nValdataR[nVal] = shapedSourceColor.r; pShaper->dataG[nVal] = shapedSourceColor.g; pShaper->dataB[nVal] = shapedSourceColor.b; } pShaper->finalize(); } if ( pLut3d ) { glm::mat3 xyz_from_dest = normalised_primary_matrix( dest.primaries, dest.white, 1.f ); glm::mat3 dest_from_xyz = glm::inverse( xyz_from_dest ); glm::mat3 xyz_from_source = normalised_primary_matrix( source.primaries, source.white, 1.f ); glm::mat3 dest_from_source = dest_from_xyz * xyz_from_source; // XYZ scaling for white point adjustment // Precalc night mode scalars & digital gain // amount and saturation are overdetermined but we separate the two as they conceptually represent // different quantities, and this preserves forwards algorithmic compatibility glm::vec3 nightModeMultHSV( nightmode.hue, clamp01( nightmode.saturation * nightmode.amount ), 1.f ); glm::vec3 vMultLinear = glm::pow( hsv_to_rgb( nightModeMultHSV ), glm::vec3( 2.2f ) ); vMultLinear = vMultLinear * flGain; // Calculate the virtual white point adaptation glm::mat3x3 whitePointDestAdaptation = glm::mat3x3( 1.f ); // identity if ( destVirtualWhite.x > 0.01f && destVirtualWhite.y > 0.01f ) { // if source white is within tiny tolerance of sourceWhitePointOverride // don't do the override? (aka two quantizations of d65) glm::mat3x3 virtualWhiteXYZFromPhysicalWhiteXYZ = chromatic_adaptation_matrix( xy_to_xyz( dest.white ), xy_to_xyz( destVirtualWhite ), eMethod ); whitePointDestAdaptation = dest_from_xyz * virtualWhiteXYZFromPhysicalWhiteXYZ * xyz_from_dest; // Consider lerp-ing the gain limiting between 0-1? That would allow partial clipping // so that contrast ratios wouldnt be sacrified too bad with alternate white points static const bool k_bLimitGain = true; if ( k_bLimitGain ) { glm::vec3 white = whitePointDestAdaptation * glm::vec3(1.f, 1.f, 1.f ); float whiteMax = std::max( white.r, std::max( white.g, white.b ) ); float normScale = 1.f / whiteMax; whitePointDestAdaptation = whitePointDestAdaptation * glm::diagonal3x3( glm::vec3( normScale ) ); } } // Precalculate source color EOTF encoded per-edge. glm::vec3 vSourceColorEOTFEncodedEdge[nLutEdgeSize3d]; float flEdgeScale = 1.f / ( (float) nLutEdgeSize3d - 1.f ); for ( int nIndex = 0; nIndex < nLutEdgeSize3d; ++nIndex ) { vSourceColorEOTFEncodedEdge[nIndex] = glm::vec3( nIndex * flEdgeScale ); if ( pShaper ) { vSourceColorEOTFEncodedEdge[nIndex] = ApplyLut1D_Inverse_Linear( *pShaper, vSourceColorEOTFEncodedEdge[nIndex] ); } } pLut3d->resize( nLutEdgeSize3d ); for ( int nBlue=0; nBluedata.empty() ) { sourceColorEOTFEncoded = ApplyLut3D_Tetrahedral( *pLook, sourceColorEOTFEncoded ); } // Convert to linearized display referred for source colorimetry glm::vec3 sourceColorLinear = calcEOTFToLinear( sourceColorEOTFEncoded, sourceEOTF, tonemapping ); // Convert to dest colorimetry (linearized display referred) glm::vec3 destColorLinear = dest_from_source * sourceColorLinear; // Do a naive blending with native gamut based on saturation // ( A very simplified form of gamut mapping ) // float colorSaturation = rgb_to_hsv( sourceColor ).y; float colorSaturation = rgb_to_hsv( sourceColorLinear ).y; float amount = cfit( colorSaturation, mapping.blendEnableMinSat, mapping.blendEnableMaxSat, mapping.blendAmountMin, mapping.blendAmountMax ); destColorLinear = glm::mix( destColorLinear, sourceColorLinear, amount ); // Apply linear Mult destColorLinear = vMultLinear * destColorLinear; // Apply destination virtual white point mapping destColorLinear = whitePointDestAdaptation * destColorLinear; // Apply tonemapping destColorLinear = tonemapping.apply( destColorLinear ); // Hue preservation if ( g_bHuePreservationWhenClipping ) { float flMax = std::max( std::max( destColorLinear.r, destColorLinear.g ), destColorLinear.b ); // TODO: Don't use g22_luminance here or in tonemapping, use whatever maxContentLightLevel is for the connector. if ( flMax > tonemapping.g22_luminance + 1.0f ) { destColorLinear /= flMax; destColorLinear *= tonemapping.g22_luminance; } } // Apply dest EOTF glm::vec3 destColorEOTFEncoded = calcLinearToEOTF( destColorLinear, destEOTF, tonemapping ); // Write LUT pLut3d->data[GetLut3DIndexRedFastRGB( nRed, nGreen, nBlue, nLutEdgeSize3d )] = destColorEOTFEncoded; } } } } } bool BIsWideGamut( const displaycolorimetry_t & nativeDisplayOutput ) { // Use red as a sentinal for a wide-gamut display return ( nativeDisplayOutput.primaries.r.x > 0.650f && nativeDisplayOutput.primaries.r.y < 0.320f ); } void buildSDRColorimetry( displaycolorimetry_t * pColorimetry, colormapping_t *pMapping, float flSDRGamutWideness, const displaycolorimetry_t & nativeDisplayOutput ) { if ( BIsWideGamut( nativeDisplayOutput) ) { // If not set, make it native. if (flSDRGamutWideness < 0 ) flSDRGamutWideness = 1.0f; displaycolorimetry_t r709NativeWhite = displaycolorimetry_709; r709NativeWhite.white = nativeDisplayOutput.white; // 0.0: 709 // 1.0: Native colormapping_t noRemap; noRemap.blendEnableMinSat = 0.7f; noRemap.blendEnableMaxSat = 1.0f; noRemap.blendAmountMin = 0.0f; noRemap.blendAmountMax = 0.0f; *pMapping = noRemap; *pColorimetry = lerp( r709NativeWhite, nativeDisplayOutput, flSDRGamutWideness ); } else { // If not set, make it native. if (flSDRGamutWideness < 0 ) flSDRGamutWideness = 0.0f; // 0.0: Native // 0.5: Generic wide gamut display w/smooth mapping // 1.0: Generic wide gamut display w/harsh mapping // This is a full blending to the unit cube, starting at 70% max sat // Creates a smooth transition from in-gamut to out of gamut colormapping_t smoothRemap; smoothRemap.blendEnableMinSat = 0.7f; smoothRemap.blendEnableMaxSat = 1.0f; smoothRemap.blendAmountMin = 0.0f; smoothRemap.blendAmountMax = 1.f; // Assume linear saturation computation // This is a partial (25%) blending to the unit cube // Allows some (but not full) clipping colormapping_t partialRemap; partialRemap.blendEnableMinSat = 0.7f; partialRemap.blendEnableMaxSat = 1.0f; partialRemap.blendAmountMin = 0.0f; partialRemap.blendAmountMax = 0.25; displaycolorimetry_t wideGamutNativeWhite = displaycolorimetry_widegamutgeneric; wideGamutNativeWhite.white = nativeDisplayOutput.white; if ( flSDRGamutWideness < 0.5f ) { float t = cfit( flSDRGamutWideness, 0.f, 0.5f, 0.0f, 1.0f ); *pColorimetry = lerp( nativeDisplayOutput, wideGamutNativeWhite, t ); *pMapping = smoothRemap; } else { float t = cfit( flSDRGamutWideness, 0.5f, 1.0f, 0.0f, 1.0f ); *pColorimetry = wideGamutNativeWhite; *pMapping = lerp( smoothRemap, partialRemap, t ); } } } void buildPQColorimetry( displaycolorimetry_t * pColorimetry, colormapping_t *pMapping, const displaycolorimetry_t & nativeDisplayOutput ) { *pColorimetry = displaycolorimetry_2020; colormapping_t noRemap; noRemap.blendEnableMinSat = 0.0f; noRemap.blendEnableMaxSat = 1.0f; noRemap.blendAmountMin = 0.0f; noRemap.blendAmountMax = 0.0f; *pMapping = noRemap; } bool approxEqual( const glm::vec3 & a, const glm::vec3 & b, float flTolerance = 1e-5f ) { glm::vec3 v = glm::abs(a - b); return ( v.x < flTolerance && v.y < flTolerance && v.z < flTolerance ); } const glm::mat3 k_xyz_from_709 = normalised_primary_matrix( displaycolorimetry_709.primaries, displaycolorimetry_709.white, 1.f ); const glm::mat3 k_709_from_xyz = glm::inverse( k_xyz_from_709 ); const glm::mat3 k_xyz_from_2020 = normalised_primary_matrix( displaycolorimetry_2020.primaries, displaycolorimetry_2020.white, 1.f ); const glm::mat3 k_2020_from_xyz = glm::inverse( k_xyz_from_2020 ); const glm::mat3 k_2020_from_709 = k_2020_from_xyz * k_xyz_from_709; ValveSoftware-gamescope-eb620ab/src/color_helpers.h000066400000000000000000000347171502457270500225510ustar00rootroot00000000000000#pragma once #define GLM_ENABLE_EXPERIMENTAL 1 #include #include #include #include #include #include // glm::vec2 #include // glm::vec3 #include // glm::mat3 #include // Color utils inline int quantize( float fVal, float fMaxVal ) { return std::max( 0.f, std::min( fMaxVal, rintf( fVal * fMaxVal ) ) ); } inline uint16_t quantize_lut_value_16bit( float flValue ) { return (uint16_t)quantize( flValue, (float)UINT16_MAX ); } inline float clamp01( float val ) { return std::max( 0.f, std::min( 1.f, val ) ); } inline float clamp( float val, float lo, float hi ) { return std::max( lo, std::min( hi, val ) ); } inline float cfit( float x, float i1, float i2, float f1, float f2 ) { return f1+(f2-f1)*clamp01( (x-i1)/(i2-i1) ); } inline float srgb_to_linear( float fVal ) { return ( fVal < 0.04045f ) ? fVal / 12.92f : std::pow( ( fVal + 0.055f) / 1.055f, 2.4f ); } inline float linear_to_srgb( float fVal ) { return ( fVal < 0.0031308f ) ? fVal * 12.92f : std::pow( fVal, 1.0f / 2.4f ) * 1.055f - 0.055f; } template inline T pq_to_nits( const T& pq ) { const float c1 = 0.8359375f; const float c2 = 18.8515625f; const float c3 = 18.6875f; const float oo_m1 = 1.0f / 0.1593017578125f; const float oo_m2 = 1.0f / 78.84375f; T num = glm::max(glm::pow(pq, T(oo_m2)) - c1, T(0.0f)); T den = c2 - c3 * glm::pow(pq, T(oo_m2)); return glm::pow(num / den, T(oo_m1)) * 10000.0f; } template inline T nits_to_pq( const T& nits ) { T y = glm::clamp(nits / 10000.0f, T(0.0f), T(1.0f)); const float c1 = 0.8359375f; const float c2 = 18.8515625f; const float c3 = 18.6875f; const float m1 = 0.1593017578125f; const float m2 = 78.84375f; T num = c1 + c2 * glm::pow(y, T(m1)); T den = T(1.0) + c3 * glm::pow(y, T(m1)); T n = glm::pow(num / den, T(m2)); return n; } struct tonemap_info_t { bool operator == (const tonemap_info_t&) const = default; bool operator != (const tonemap_info_t&) const = default; float flBlackPointNits = 0.f; float flWhitePointNits = 0.f; bool BIsValid() const { return ( flWhitePointNits > flBlackPointNits ); } void reset() { flBlackPointNits = 0.f; flWhitePointNits = 0.f; } }; // Apply an HDR tonemapping according to eetf 2390 (R-REP-BT.2390-8-2020-PDF-E.pdf) // sourceXXX == "Mastering Display" == Lw, Lb (in the paper) // targetXXX == "Target Display" == Lmin, Lmax (in the paper) // Be warned... PQ in, PQ out, for ALL params [0,1] // This does not imply this function has anything to do with PQ // (it's quite sensible to apply it to linear values created in other ways... you just have to // PQ all params first, and undo the output) // Values outside of 0-1 are not defined struct eetf_2390_t { void init( const tonemap_info_t & source, const tonemap_info_t & target ) { init_pq( nits_to_pq( source.flBlackPointNits ), nits_to_pq( source.flWhitePointNits ), nits_to_pq( target.flBlackPointNits ), nits_to_pq( target.flWhitePointNits ) ); } void init_pq( float sourceBlackPQ, float sourceWhitePQ, float targetBlackPQ, float targetWhitePQ ) { m_sourceBlackPQ = sourceBlackPQ; m_sourcePQScale = sourceWhitePQ - sourceBlackPQ; m_invSourcePQScale = m_sourcePQScale > 0.f ? 1.f / m_sourcePQScale : 0.f; m_minLumPQ = ( targetBlackPQ - sourceBlackPQ ) * m_invSourcePQScale; m_maxLumPQ = ( targetWhitePQ - sourceBlackPQ ) * m_invSourcePQScale; m_ks = 1.5 * m_maxLumPQ - 0.5; // TODO : return false if ks == 1.f? } inline float apply( float inputNits ) const { return pq_to_nits( apply_pq( nits_to_pq( inputNits ) ) ); } // Raw PQ transfer function inline float apply_pq( float valuePQ ) const { // normalize PQ based on the mastering (source) display (E1) float e1 = ( valuePQ - m_sourceBlackPQ ) * m_invSourcePQScale; // Apply high end rolloff float e2 = e1 < m_ks ? e1 : _eetf_2390_spline( e1, m_ks, m_maxLumPQ ); // Apply low end pedestal float one_min_e2 = 1.f - e2; float one_min_e2_sq = one_min_e2 * one_min_e2; float e3 = e2 + m_minLumPQ * one_min_e2_sq * one_min_e2_sq; // Re-apply mastering (source) transform return e3 * m_sourcePQScale + m_sourceBlackPQ; } // "Max RGB" approach, as defined in "Color Volume and Hue-Preservation in HDR Tone Mapping" // Digital Object Identifier 10.5594/JMI.2020.2984046 // Date of publication: 4 May 2020 inline glm::vec3 apply_max_rgb( const glm::vec3 & inputNits ) const { float input_scalar_nits = std::max( inputNits.r, std::max( inputNits.g, inputNits.b ) ); float output_scalar_nits = pq_to_nits( apply_pq( nits_to_pq( input_scalar_nits ) ) ); float gain = input_scalar_nits > 0.f ? output_scalar_nits / input_scalar_nits : 0.f; return inputNits * gain; } inline glm::vec3 apply_luma_rgb( const glm::vec3 & inputNits ) const { float input_scalar_nits = 0.2627f * inputNits.r + 0.6780f * inputNits.g + 0.0593f * inputNits.b; float output_scalar_nits = pq_to_nits( apply_pq( nits_to_pq( input_scalar_nits ) ) ); float gain = input_scalar_nits > 0.f ? output_scalar_nits / input_scalar_nits : 0.f; return inputNits * gain; } inline glm::vec3 apply_independent_rgb( const glm::vec3 & inputNits ) const { glm::vec3 inputPQ = nits_to_pq( inputNits ); glm::vec3 outputPQ = { apply_pq( inputPQ.r ), apply_pq( inputPQ.g ), apply_pq( inputPQ.b ) }; return pq_to_nits( outputPQ ); } private: float m_sourceBlackPQ = 0.f; float m_sourcePQScale = 0.f; float m_invSourcePQScale = 0.f; float m_minLumPQ = 0.f; float m_maxLumPQ = 0.f; float m_ks = 0.f; inline float _eetf_2390_spline( float value, float ks, float maxLum ) const { float t = ( value - ks ) / ( 1.f - ks ); // TODO : guard against ks == 1.f? float t_sq = t*t; float t_cub = t_sq*t; float v1 = ( 2.f * t_cub - 3.f * t_sq + 1.f ) * ks; float v2 = ( t_cub - 2.f * t_sq + t ) * ( 1.f - ks ); float v3 = (-2.f * t_cub + 3.f * t_sq ) * maxLum; return v1 + v2 + v3; } }; inline float flerp( float a, float b, float t ) { return a + t * (b - a); } inline float safe_pow(float x, float y) { // Avoids pow(x, 1.0f) != x. if (y == 1.0f) return x; return std::pow(std::max(x, 0.0f), y); } inline float positive_mod( float flX, float flPeriod ) { float flVal = fmodf( flX, flPeriod ); return ( flVal < 0 ) ? flVal + flPeriod : fabsf( flVal ); // fabs fixes -0 } // Colorimetry functions related to color space conversions struct primaries_t { bool operator == (const primaries_t&) const = default; bool operator != (const primaries_t&) const = default; glm::vec2 r; glm::vec2 g; glm::vec2 b; }; enum EOTF { EOTF_Gamma22 = 0, EOTF_PQ = 1, EOTF_Count = 2, }; struct displaycolorimetry_t { bool operator == (const displaycolorimetry_t&) const = default; bool operator != (const displaycolorimetry_t&) const = default; primaries_t primaries; glm::vec2 white; }; struct nightmode_t { bool operator == (const nightmode_t&) const = default; bool operator != (const nightmode_t&) const = default; float amount; // [0 = disabled, 1.f = on] float hue; // [0,1] float saturation; // [0,1] }; struct colormapping_t { bool operator == (const colormapping_t&) const = default; bool operator != (const colormapping_t&) const = default; float blendEnableMinSat; float blendEnableMaxSat; float blendAmountMin; float blendAmountMax; }; displaycolorimetry_t lerp( const displaycolorimetry_t & a, const displaycolorimetry_t & b, float t ); colormapping_t lerp( const colormapping_t & a, const colormapping_t & b, float t ); // These values are directly exposed to steam // Values must be stable over time enum ETonemapOperator { ETonemapOperator_None = 0, ETonemapOperator_EETF2390_Luma = 1, ETonemapOperator_EETF2390_Independent = 2, ETonemapOperator_EETF2390_MaxChan = 3, }; struct tonemapping_t { bool bUseShaper = true; float g22_luminance = 1.f; // what luminance should be applied for g22 EOTF conversions? ETonemapOperator eOperator = ETonemapOperator_None; eetf_2390_t eetf2390; inline glm::vec3 apply( const glm::vec3 & inputNits ) const { switch ( eOperator ) { case ETonemapOperator_EETF2390_Luma: return eetf2390.apply_luma_rgb( inputNits ); case ETonemapOperator_EETF2390_Independent: return eetf2390.apply_independent_rgb( inputNits ); case ETonemapOperator_EETF2390_MaxChan: return eetf2390.apply_max_rgb( inputNits ); default: return inputNits; } } }; // Exposed in external atoms. Dont change the values enum EChromaticAdaptationMethod { k_EChromaticAdapatationMethod_XYZ = 0, k_EChromaticAdapatationMethod_Bradford = 1, }; glm::mat3 chromatic_adaptation_matrix( const glm::vec3 & sourceWhiteXYZ, const glm::vec3 & destWhiteXYZ, EChromaticAdaptationMethod eMethod ); struct lut1d_t { int lutSize = 0; std::vector dataR; std::vector dataG; std::vector dataB; // Some LUTs start with a flat section... // Where does the non-flat part start? // (impacts the inverse computation) int startIndexR = -1; int startIndexG = -1; int startIndexB = -1; void finalize(); // calculates start indicies void resize( int lutSize_ ) { lutSize = lutSize_; dataR.resize( lutSize_ ); dataG.resize( lutSize_ ); dataB.resize( lutSize_ ); startIndexR = -1; startIndexG = -1; startIndexB = -1; } }; struct lut3d_t { int lutEdgeSize = 0; std::vector data; // R changes fastest void resize( int lutEdgeSize_ ) { lutEdgeSize = lutEdgeSize_; data.resize( lutEdgeSize_ * lutEdgeSize_ * lutEdgeSize_ ); } }; std::shared_ptr LoadCubeLut( FILE *pFile, bool &bRaisesBlackLevelFloor ); std::shared_ptr LoadCubeLut( const char *pchFileName, bool &bRaisesBlackLevelFloor ); // Generate a color transform from the source colorspace, to the dest colorspace, // nLutSize1d is the number of color entries in the shaper lut // I.e., for a shaper lut with 256 input colors nLutSize1d = 256, countof(pRgbxData1d) = 1024 // nLutEdgeSize3d is the number of color entries, per edge, in the 3d lut // I.e., for a 17x17x17 lut nLutEdgeSize3d = 17, countof(pRgbxData3d) = 19652 // // If the white points differ, this performs an absolute colorimetric match // Look luts are optional, but if specified applied in the sourceEOTF space template void calcColorTransform( lut1d_t * pShaper, int nLutSize1d, lut3d_t * pLut3d, const displaycolorimetry_t & source, EOTF sourceEOTF, const displaycolorimetry_t & dest, EOTF destEOTF, const glm::vec2 & destVirtualWhite, EChromaticAdaptationMethod eMethod, const colormapping_t & mapping, const nightmode_t & nightmode, const tonemapping_t & tonemapping, const lut3d_t * pLook, float flGain ); #define REGISTER_LUT_EDGE_SIZE(size) template void calcColorTransform<(size)>( lut1d_t * pShaper, int nLutSize1d, \ lut3d_t * pLut3d, \ const displaycolorimetry_t & source, EOTF sourceEOTF, \ const displaycolorimetry_t & dest, EOTF destEOTF, \ const glm::vec2 & destVirtualWhite, EChromaticAdaptationMethod eMethod, \ const colormapping_t & mapping, const nightmode_t & nightmode, const tonemapping_t & tonemapping, \ const lut3d_t * pLook, float flGain ) // Build colorimetry and a gamut mapping for the given SDR configuration // Note: the output colorimetry will use the native display's white point // Only the color gamut will change void buildSDRColorimetry( displaycolorimetry_t * pColorimetry, colormapping_t *pMapping, float flSDRGamutWideness, const displaycolorimetry_t & nativeDisplayOutput ); // Build colorimetry and a gamut mapping for the given PQ configuration void buildPQColorimetry( displaycolorimetry_t * pColorimetry, colormapping_t *pMapping, const displaycolorimetry_t & nativeDisplayOutput ); // Colormetry helper functions for DRM, kindly taken from Weston: // https://gitlab.freedesktop.org/wayland/weston/-/blob/main/libweston/backend-drm/kms-color.c // Licensed under MIT. // Josh: I changed the asserts to clamps here (going to 0, rather than 1) to deal better with // bad EDIDs (that have 0'ed out metadata) and naughty clients. static inline uint16_t color_xy_to_u16(float v) { //assert(v >= 0.0f); //assert(v <= 1.0f); v = std::clamp(v, 0.0f, 1.0f); // CTA-861-G // 6.9.1 Static Metadata Type 1 // chromaticity coordinate encoding return (uint16_t)round(v * 50000.0f); } static inline float color_xy_from_u16(uint16_t v) { return v / 50000.0f; } static inline uint16_t nits_to_u16(float nits) { //assert(nits >= 1.0f); //assert(nits <= 65535.0f); nits = std::clamp(nits, 0.0f, 65535.0f); // CTA-861-G // 6.9.1 Static Metadata Type 1 // max display mastering luminance, max content light level, // max frame-average light level return (uint16_t)round(nits); } static inline float nits_from_u16(uint16_t v) { return float(v); } static inline uint16_t nits_to_u16_dark(float nits) { //assert(nits >= 0.0001f); //assert(nits <= 6.5535f); nits = std::clamp(nits, 0.0f, 6.5535f); // CTA-861-G // 6.9.1 Static Metadata Type 1 // min display mastering luminance return (uint16_t)round(nits * 10000.0f); } static inline float nits_from_u16_dark(uint16_t v) { return v / 10000.0f; } static constexpr displaycolorimetry_t displaycolorimetry_steamdeck_spec { .primaries = { { 0.602f, 0.355f }, { 0.340f, 0.574f }, { 0.164f, 0.121f } }, .white = { 0.3070f, 0.3220f }, // not D65 }; static constexpr displaycolorimetry_t displaycolorimetry_steamdeck_measured { .primaries = { { 0.603f, 0.349f }, { 0.335f, 0.571f }, { 0.163f, 0.115f } }, .white = { 0.296f, 0.307f }, // not D65 }; static constexpr displaycolorimetry_t displaycolorimetry_709 { .primaries = { { 0.64f, 0.33f }, { 0.30f, 0.60f }, { 0.15f, 0.06f } }, .white = { 0.3127f, 0.3290f }, // D65 }; // Our "saturated SDR target", per jeremys static constexpr displaycolorimetry_t displaycolorimetry_widegamutgeneric { .primaries = { { 0.6825f, 0.3165f }, { 0.241f, 0.719f }, { 0.138f, 0.050f } }, .white = { 0.3127f, 0.3290f }, // D65 }; static constexpr displaycolorimetry_t displaycolorimetry_2020 { .primaries = { { 0.708f, 0.292f }, { 0.170f, 0.797f }, { 0.131f, 0.046f } }, .white = { 0.3127f, 0.3290f }, // D65 }; extern const glm::mat3 k_xyz_from_709; extern const glm::mat3 k_709_from_xyz; extern const glm::mat3 k_xyz_from_2020; extern const glm::mat3 k_2020_from_xyz; extern const glm::mat3 k_2020_from_709; ValveSoftware-gamescope-eb620ab/src/color_helpers_impl.h000066400000000000000000000007321502457270500235600ustar00rootroot00000000000000#pragma once #include "color_helpers.h" namespace rendervulkan { static constexpr uint32_t s_nLutEdgeSize3d = 17; static constexpr uint32_t s_nLutSize1d = 4096; } namespace color_bench { static constexpr uint32_t nLutEdgeSize3d = 17; static constexpr uint32_t nLutSize1d = 4096; } namespace ns_color_tests { [[maybe_unused]] static constexpr uint32_t nLutEdgeSize3d = 17; } #ifdef COLOR_HELPERS_CPP REGISTER_LUT_EDGE_SIZE(rendervulkan::s_nLutEdgeSize3d); #endifValveSoftware-gamescope-eb620ab/src/color_tests.cpp000066400000000000000000000203041502457270500225670ustar00rootroot00000000000000#include "color_helpers.h" #include //#include #include /* using ns_color_tests::nLutEdgeSize3d; const uint32_t nLutSize1d = 4096; uint16_t lut1d[nLutSize1d*4]; uint16_t lut3d[nLutEdgeSize3d*nLutEdgeSize3d*nLutEdgeSize3d*4]; lut1d_t lut1d_float; lut3d_t lut3d_float; static void BenchmarkCalcColorTransform(EOTF inputEOTF, benchmark::State &state) { const primaries_t primaries = { { 0.602f, 0.355f }, { 0.340f, 0.574f }, { 0.164f, 0.121f } }; const glm::vec2 white = { 0.3070f, 0.3220f }; displaycolorimetry_t inputColorimetry{}; inputColorimetry.primaries = primaries; inputColorimetry.white = white; displaycolorimetry_t outputEncodingColorimetry{}; outputEncodingColorimetry.primaries = primaries; outputEncodingColorimetry.white = white; colormapping_t colorMapping{}; tonemapping_t tonemapping{}; tonemapping.bUseShaper = true; nightmode_t nightmode{}; float flGain = 1.0f; for (auto _ : state) { calcColorTransform( &lut1d_float, nLutSize1d, &lut3d_float, inputColorimetry, inputEOTF, outputEncodingColorimetry, EOTF_Gamma22, colorMapping, nightmode, tonemapping, nullptr, flGain ); for ( size_t i=0, end = lut1d_float.dataR.size(); i 0.1 - 1000 float sourceBlackNits = 0.01f; float sourceWhiteNits = 5000.0f; float sourceBlackPQ = nits_to_pq( sourceBlackNits ); float sourceWhitePQ = nits_to_pq( sourceWhiteNits ); printf("source\t%0.02f - %0.01f\t\tPQ10: %0.1f %0.1f \n", sourceBlackNits, sourceWhiteNits, sourceBlackPQ * 1023.f, sourceWhitePQ * 1023.f ); float destBlackNits = 0.1f; float destWhiteNits = 1000.0f; float destBlackPQ = nits_to_pq( destBlackNits ); float destWhitePQ = nits_to_pq( destWhiteNits ); printf("dest\t%0.02f - %0.01f\t\tPQ10: %0.1f %0.1f\n", destBlackNits, destWhiteNits, destBlackPQ * 1023.f, destWhitePQ * 1023.f ); printf("\n"); eetf_2390_t eetf; eetf.init_pq( sourceBlackPQ, sourceWhitePQ, destBlackPQ, destWhitePQ ); for ( size_t nLevel=0; nLevel < 12; ++nLevel ) { float flInputNits = vLumaLevels[nLevel]; float inputPQ = nits_to_pq( flInputNits ); float tonemappedOutputPQ = eetf.apply_pq( inputPQ ); float tonemappedOutputNits = pq_to_nits( tonemappedOutputPQ ); printf("value\t%0.03f -> %0.03f\tPQ10: %0.1f -> %0.1f\n", flInputNits, tonemappedOutputNits, inputPQ * 1023.f, tonemappedOutputPQ * 1023.f ); } } int main(int argc, char* argv[]) { printf("color_tests\n"); // test_eetf2390_mono(); color_tests(); return 0; }ValveSoftware-gamescope-eb620ab/src/commit.cpp000066400000000000000000000077131502457270500215300ustar00rootroot00000000000000#include "wlserver.hpp" #include "rendervulkan.hpp" #include "steamcompmgr.hpp" #include "commit.h" #include "gpuvis_trace_utils.h" extern gamescope::CAsyncWaiter> g_ImageWaiter; commit_t::commit_t() { static uint64_t maxCommmitID = 0; commitID = ++maxCommmitID; } commit_t::~commit_t() { { std::unique_lock lock( m_WaitableCommitStateMutex ); CloseFenceInternal(); } if ( vulkanTex != nullptr ) vulkanTex = nullptr; wlserver_lock(); if (!presentation_feedbacks.empty()) { wlserver_presentation_feedback_discard(surf, presentation_feedbacks); // presentation_feedbacks cleared by wlserver_presentation_feedback_discard } wlr_buffer_unlock( buf ); wlserver_unlock(); } GamescopeAppTextureColorspace commit_t::colorspace() const { VkColorSpaceKHR colorspace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; if (feedback && vulkanTex) colorspace = feedback->vk_colorspace; if (!vulkanTex) return GAMESCOPE_APP_TEXTURE_COLORSPACE_LINEAR; return VkColorSpaceToGamescopeAppTextureColorSpace(vulkanTex->format(), colorspace); } int commit_t::GetFD() { return m_nCommitFence; } void commit_t::OnPollIn() { gpuvis_trace_end_ctx_printf( commitID, "wait fence" ); { std::unique_lock lock( m_WaitableCommitStateMutex ); if ( !CloseFenceInternal() ) return; } Signal(); nudge_steamcompmgr(); } void commit_t::Signal() { uint64_t frametime; if ( m_bMangoNudge ) { uint64_t now = get_time_in_nanos(); static uint64_t lastFrameTime = now; frametime = now - lastFrameTime; lastFrameTime = now; } // TODO: Move this so it's called in the main loop. // Instead of looping over all the windows like before. // When we get the new IWaitable stuff in there. { std::unique_lock< std::mutex > lock( m_pDoneCommits->listCommitsDoneLock ); m_pDoneCommits->listCommitsDone.push_back( CommitDoneEntry_t{ .winSeq = win_seq, .commitID = commitID, .desiredPresentTime = desired_present_time, .fifo = fifo, } ); } if ( m_bMangoNudge ) mangoapp_update( IsPerfOverlayFIFO() ? uint64_t(~0ull) : frametime, frametime, uint64_t(~0ull) ); } void commit_t::OnPollHangUp() { std::unique_lock lock( m_WaitableCommitStateMutex ); CloseFenceInternal(); } bool commit_t::IsPerfOverlayFIFO() { return fifo || is_steam; } // Returns true if we had a fence that was closed. bool commit_t::CloseFenceInternal() { if ( m_nCommitFence < 0 ) return false; // Will automatically remove from epoll! g_ImageWaiter.RemoveWaitable( this ); close( m_nCommitFence ); m_nCommitFence = -1; return true; } void commit_t::SetFence( int nFence, bool bMangoNudge, CommitDoneList_t *pDoneCommits ) { std::unique_lock lock( m_WaitableCommitStateMutex ); CloseFenceInternal(); m_nCommitFence = nFence; m_bMangoNudge = bMangoNudge; m_pDoneCommits = pDoneCommits; } void calc_scale_factor(float &out_scale_x, float &out_scale_y, float sourceWidth, float sourceHeight); bool commit_t::ShouldPreemptivelyUpscale() { // Don't pre-emptively upscale if we are not a FIFO commit. // Don't want to FSR upscale 1000fps content. if ( !fifo ) return false; // If we support the upscaling filter in hardware, don't // pre-emptively do it via shaders. if ( DoesHardwareSupportUpscaleFilter( g_upscaleFilter ) ) return false; if ( !vulkanTex ) return false; float flScaleX = 1.0f; float flScaleY = 1.0f; // I wish this function was more programatic with its inputs, but it does do exactly what we want right now... // It should also return a std::pair or a glm uvec calc_scale_factor( flScaleX, flScaleY, vulkanTex->width(), vulkanTex->height() ); return !close_enough( flScaleX, 1.0f ) || !close_enough( flScaleY, 1.0f ); } ValveSoftware-gamescope-eb620ab/src/commit.h000066400000000000000000000044031502457270500211660ustar00rootroot00000000000000#include "steamcompmgr_shared.hpp" #include "Utils/NonCopyable.h" #include #include "main.hpp" #include "rendervulkan.hpp" class CVulkanTexture; struct UpscaledTexture_t { GamescopeUpscaleFilter eFilter{}; GamescopeUpscaleScaler eScaler{}; uint32_t uOutputWidth = 0; uint32_t uOutputHeight = 0; gamescope::Rc pTexture{}; VkColorSpaceKHR colorspace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; }; struct commit_t final : public gamescope::RcObject, public gamescope::IWaitable, public gamescope::NonCopyable { commit_t(); ~commit_t(); GamescopeAppTextureColorspace colorspace() const; // For waitable: int GetFD() final; void OnPollIn() final; void Signal(); void OnPollHangUp() final; bool IsPerfOverlayFIFO(); // Returns true if we had a fence that was closed. bool CloseFenceInternal(); void SetFence( int nFence, bool bMangoNudge, CommitDoneList_t *pDoneCommits ); bool ShouldPreemptivelyUpscale(); struct wlr_buffer *buf = nullptr; gamescope::Rc vulkanTex; std::optional upscaledTexture; gamescope::Rc GetTexture( GamescopeUpscaleFilter eFilter, GamescopeUpscaleScaler eScaler, GamescopeAppTextureColorspace &colorspace ) { if ( upscaledTexture && upscaledTexture->eFilter == eFilter && upscaledTexture->eScaler == eScaler && upscaledTexture->uOutputWidth == g_nOutputWidth && upscaledTexture->uOutputHeight == g_nOutputHeight ) { colorspace = VkColorSpaceToGamescopeAppTextureColorSpace( upscaledTexture->pTexture->format(), upscaledTexture->colorspace ); return upscaledTexture->pTexture; } colorspace = this->colorspace(); return vulkanTex; } uint64_t commitID = 0; bool done = false; bool async = false; bool fifo = false; bool is_steam = false; std::optional feedback = std::nullopt; uint64_t win_seq = 0; struct wlr_surface *surf = nullptr; std::vector presentation_feedbacks; std::optional present_id = std::nullopt; uint64_t desired_present_time = 0; uint64_t earliest_present_time = 0; uint64_t present_margin = 0; std::mutex m_WaitableCommitStateMutex; int m_nCommitFence = -1; bool m_bMangoNudge = false; CommitDoneList_t *m_pDoneCommits = nullptr; // I hate this };ValveSoftware-gamescope-eb620ab/src/convar.cpp000066400000000000000000000046631502457270500215310ustar00rootroot00000000000000#include "convar.h" #include "Utils/Version.h" #include LogScope console_log("console"); extern void PrintGamescopeVersion(); namespace gamescope { ConCommand::ConCommand( std::string_view pszName, std::string_view pszDescription, ConCommandFunc func, bool bRegisterScript ) : m_pszName{ pszName } , m_pszDescription{ pszDescription } , m_Func{ func } { assert( !GetCommands().contains( pszName ) ); GetCommands()[ std::string( pszName ) ] = this; #if HAVE_SCRIPTING if ( bRegisterScript ) CScriptScopedLock().Manager().Gamescope().Convars.Base[pszName] = this; #endif } ConCommand::~ConCommand() { GetCommands().erase( GetCommands().find( m_pszName ) ); } bool ConCommand::Exec( std::span args ) { if ( args.size() < 1 ) { console_log.warnf( "No command specified." ); return false; } std::string_view commandName = args[0]; auto iter = GetCommands().find( commandName ); if ( iter == GetCommands().end() ) { console_log.warnf( "Command not found." ); return false; } iter->second->Invoke( args ); return true; } Dict& ConCommand::GetCommands() { static Dict s_Commands; return s_Commands; } static ConCommand cc_help("help", "List all Gamescope convars and commands", []( std::span args ) { auto &commands = ConCommand::GetCommands(); struct CommandHelp { std::string_view pszName; std::string_view pszDesc; }; std::vector commandHelps; for ( auto &command : commands ) commandHelps.emplace_back( command.second->GetName(), command.second->GetDescription() ); std::sort( commandHelps.begin(), commandHelps.end(), []( const CommandHelp &a, const CommandHelp &b ) { return a.pszName < b.pszName; }); for ( auto &help : commandHelps ) { console_log.infof( "%.*s: %.*s", (int)help.pszName.size(), help.pszName.data(), (int)help.pszDesc.size(), help.pszDesc.data() ); } }); static ConCommand cc_version("version", "Print current Gamescope version", []( std::span args ) { PrintVersion(); }); }ValveSoftware-gamescope-eb620ab/src/convar.h000066400000000000000000000170151502457270500211710ustar00rootroot00000000000000#pragma once #include #include #include #include #include #include #include #include #include #include #include #include "Script/Script.h" #include "Utils/Dict.h" #include "log.hpp" extern LogScope console_log; namespace gamescope { class ConCommand; template inline std::string ToString( const T &thing ) { return std::to_string( thing ); } template <> inline std::string ToString( const std::string &sThing ) { return sThing; } template <> inline std::string ToString( const std::string_view &svThing ) { return std::string( svThing ); } template inline std::optional Parse( std::string_view chars ) { T obj; auto result = std::from_chars( chars.begin(), chars.end(), obj ); if ( result.ec == std::errc{} ) return obj; else return std::nullopt; } template <> inline std::optional Parse( std::string_view chars ) { std::optional oNumber = Parse( chars ); if ( oNumber ) return !!*oNumber; if ( chars == "true" ) return true; else return false; } inline void Split( std::vector &tokens, std::string_view string, std::string_view delims = " " ) { size_t end = 0; for ( size_t start = 0; start < string.size() && end != std::string_view::npos; start = end + 1 ) { end = string.find_first_of( delims, start ); if ( start != end ) tokens.emplace_back( string.substr( start, end-start ) ); } } inline std::vector Split( std::string_view string, std::string_view delims = " " ) { std::vector tokens; Split( tokens, string, delims ); return tokens; } class ConCommand { using ConCommandFunc = std::function )>; public: DECLARE_SCRIPTDESC( ConCommand ); ConCommand( std::string_view pszName, std::string_view pszDescription, ConCommandFunc func, bool bRegisterScript = true ); ~ConCommand(); void Invoke( std::span args ) { if ( m_Func ) m_Func( args ); } // Calls it with space separated args. void CallWithArgString( std::string_view args ) { std::vector sArgs; sArgs.push_back( m_pszName ); Split( sArgs, args, " " ); Invoke( sArgs ); } static bool Exec( std::span args ); std::string_view GetName() const { return m_pszName; } std::string_view GetDescription() const { return m_pszDescription; } static Dict& GetCommands(); protected: std::string_view m_pszName; std::string_view m_pszDescription; ConCommandFunc m_Func; }; START_SCRIPTDESC( ConCommand, "concommand" ) SCRIPTDESC( "name", &ConCommand::m_pszName ) SCRIPTDESC( "description", &ConCommand::m_pszDescription ) SCRIPTDESC( "call", &ConCommand::CallWithArgString ) END_SCRIPTDESC() template class ConVar : public ConCommand { using ConVarCallbackFunc = std::function &)>; public: DECLARE_SCRIPTDESC( ConVar ); ConVar( std::string_view pszName, T defaultValue = T{}, std::string_view pszDescription = "", ConVarCallbackFunc func = nullptr, bool bRunCallbackAtStartup = false, bool bRegisterScript = true ) : ConCommand( pszName, pszDescription, [this]( std::span pArgs ){ this->InvokeFunc( pArgs ); }, false ) , m_Value{ defaultValue } , m_Callback{ func } { if ( bRunCallbackAtStartup ) { RunCallback(); } #if HAVE_SCRIPTING if ( bRegisterScript ) { CScriptScopedLock().Manager().Gamescope().Convars.Base[pszName] = this; } #endif } const T& Get() const { return m_Value; } template void SetValue( const J &newValue ) { m_Value = T{ newValue }; RunCallback(); } void RunCallback() { if ( !m_bInCallback && m_Callback ) { m_bInCallback = true; m_Callback( *this ); m_bInCallback = false; } } template ConVar& operator =( const J &newValue ) { SetValue( newValue ); return *this; } operator T() const { return m_Value; } // SFINAE for std::string... operator std::string_view() const { return m_Value; } template bool operator == ( const J &other ) const { return m_Value == other; } template bool operator != ( const J &other ) const { return m_Value != other; } template auto operator <=>( const J &other ) const { return m_Value <=> other; } template bool operator == ( const ConVar &other ) const { return *this == other.Get(); } template bool operator != ( const ConVar &other ) const { return *this != other.Get(); } template auto operator <=>( const ConVar &other ) const { return *this <=> other.Get(); } T operator | (T other) { return m_Value | other; } T &operator |=(T other) { return m_Value |= other; } T operator & (T other) { return m_Value & other; } T &operator &=(T other) { return m_Value &= other; } void InvokeFunc( std::span pArgs ) { if ( pArgs.size() == 1 ) { // We should move to std format for logging and stuff. // This is kinda gross and grody! std::string sValue = ToString( m_Value ); console_log.infof( "%.*s: %.*s\n%.*s", (int)m_pszName.length(), m_pszName.data(), (int)sValue.length(), sValue.data(), (int)m_pszDescription.length(), m_pszDescription.data() ); return; } if ( pArgs.size() != 2 ) return; if constexpr ( std::is_enum::value ) { using Underlying = std::underlying_type::type; std::optional oResult = Parse( pArgs[1] ); SetValue( oResult ? static_cast( *oResult ) : T{} ); } else if constexpr ( std::is_integral::value || std::is_floating_point::value ) { std::optional oResult = Parse( pArgs[1] ); SetValue( oResult ? *oResult : T{} ); } else { SetValue( pArgs[1] ); } } private: T m_Value{}; ConVarCallbackFunc m_Callback; bool m_bInCallback; }; SCRIPTDESC_TEMPLATE( T ) START_SCRIPTDESC_ANON( ConVar ) SCRIPTDESC( "name", &ConVar::m_pszName ) SCRIPTDESC( "description", &ConVar::m_pszDescription ) SCRIPTDESC( "call", &ConVar::CallWithArgString ) SCRIPTDESC( "value", &ConVar::m_Value ) END_SCRIPTDESC() } ValveSoftware-gamescope-eb620ab/src/docs/000077500000000000000000000000001502457270500204545ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/src/docs/Steam Deck Display Pipeline.png000066400000000000000000005641661502457270500262200ustar00rootroot00000000000000PNG  IHDREXertEXtmxfile%3Cmxfile%20host%3D%22app.diagrams.net%22%20modified%3D%222023-04-20T17%3A03%3A45.610Z%22%20agent%3D%22Mozilla%2F5.0%20(X11%3B%20Linux%20x86_64%3B%20rv%3A109.0)%20Gecko%2F20100101%20Firefox%2F114.0%22%20etag%3D%22ecydnw_5KtoFxzYO0dzC%22%20version%3D%2221.2.1%22%20type%3D%22google%22%3E%0A%20%20%3Cdiagram%20name%3D%22Page-1%22%20id%3D%22H19bTcVdlAs_GQVTQ4QJ%22%3E%0A%20%20%20%20%3CmxGraphModel%20dx%3D%222374%22%20dy%3D%221316%22%20grid%3D%221%22%20gridSize%3D%2210%22%20guides%3D%221%22%20tooltips%3D%221%22%20connect%3D%221%22%20arrows%3D%221%22%20fold%3D%221%22%20page%3D%221%22%20pageScale%3D%221%22%20pageWidth%3D%221100%22%20pageHeight%3D%22850%22%20math%3D%220%22%20shadow%3D%220%22%3E%0A%20%20%20%20%20%20%3Croot%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%220%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%221%22%20parent%3D%220%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-9%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3BentryX%3D0%3BentryY%3D0.5%3BentryDx%3D0%3BentryDy%3D0%3B%22%20parent%3D%221%22%20source%3D%22e5xB5EPQwGM9hv1eYVk4-2%22%20target%3D%22g-a2YEmZG7LvmfDlxI8T-2%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CmxPoint%20x%3D%22650%22%20y%3D%22110%22%20as%3D%22sourcePoint%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%3C%2FmxGeometry%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22e5xB5EPQwGM9hv1eYVk4-9%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3B%22%20parent%3D%221%22%20source%3D%22g-a2YEmZG7LvmfDlxI8T-1%22%20target%3D%22e5xB5EPQwGM9hv1eYVk4-6%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-1%22%20value%3D%22%26lt%3Bdiv%26gt%3BHDR10%20PQ%20Plane%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BPQ%20%2B%20Rec.2020%26lt%3B%2Fi%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22100%22%20y%3D%2280%22%20width%3D%22120%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-23%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3BentryX%3D0%3BentryY%3D0.5%3BentryDx%3D0%3BentryDy%3D0%3B%22%20parent%3D%221%22%20source%3D%22g-a2YEmZG7LvmfDlxI8T-2%22%20target%3D%22g-a2YEmZG7LvmfDlxI8T-3%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-2%22%20value%3D%22%26lt%3Bdiv%26gt%3BShaper%20LUT%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BPQ%20-%26amp%3Bgt%3B%20Display%20Native%26lt%3B%2Fi%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22890%22%20y%3D%2280%22%20width%3D%22160%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-18%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3B%22%20parent%3D%221%22%20source%3D%22g-a2YEmZG7LvmfDlxI8T-3%22%20target%3D%22g-a2YEmZG7LvmfDlxI8T-4%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-3%22%20value%3D%22%26lt%3Bdiv%26gt%3B3D%20LUT%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BGamut%20Mapping%20%2B%20Tone%20Mapping%20%2B%20Night%20Mode%26lt%3B%2Fi%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%221090%22%20y%3D%2280%22%20width%3D%22280%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-20%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3BentryX%3D0%3BentryY%3D0.5%3BentryDx%3D0%3BentryDy%3D0%3B%22%20parent%3D%221%22%20source%3D%22g-a2YEmZG7LvmfDlxI8T-4%22%20target%3D%22g-a2YEmZG7LvmfDlxI8T-19%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-4%22%20value%3D%22%26lt%3Bdiv%26gt%3BBlend%20LUT%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bb%26gt%3Boptional%20if%20single%20plane%26lt%3B%2Fb%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BDisplay%20Native%20-%26amp%3Bgt%3B%20%26lt%3B%2Fi%26gt%3B%26lt%3Bi%26gt%3BLinearized%20Display%20Referred%26lt%3B%2Fi%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%221410%22%20y%3D%2280%22%20width%3D%22290%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22e5xB5EPQwGM9hv1eYVk4-10%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3BentryX%3D0%3BentryY%3D0.5%3BentryDx%3D0%3BentryDy%3D0%3B%22%20parent%3D%221%22%20source%3D%22g-a2YEmZG7LvmfDlxI8T-5%22%20target%3D%22e5xB5EPQwGM9hv1eYVk4-7%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-5%22%20value%3D%22%26lt%3Bdiv%26gt%3BHDR%20scRGB%20Plane%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BscRGB%20%2B%20Rec.709%26lt%3B%2Fi%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22100%22%20y%3D%22240%22%20width%3D%22120%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-24%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3BentryX%3D0%3BentryY%3D0.5%3BentryDx%3D0%3BentryDy%3D0%3B%22%20parent%3D%221%22%20source%3D%22g-a2YEmZG7LvmfDlxI8T-6%22%20target%3D%22g-a2YEmZG7LvmfDlxI8T-13%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-6%22%20value%3D%22%26lt%3Bdiv%26gt%3BShaper%20LUT%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BPQ%20-%26amp%3Bgt%3B%20Display%20Native%26lt%3B%2Fi%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22890%22%20y%3D%22240%22%20width%3D%22160%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-26%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3BentryX%3D0%3BentryY%3D0.5%3BentryDx%3D0%3BentryDy%3D0%3B%22%20parent%3D%221%22%20source%3D%22g-a2YEmZG7LvmfDlxI8T-8%22%20target%3D%22g-a2YEmZG7LvmfDlxI8T-6%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-8%22%20value%3D%22Pre-Shaper%20TF%26lt%3Bbr%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BscRGB%20(TF)%20-%26amp%3Bgt%3B%20PQ%26lt%3B%2Fi%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22690%22%20y%3D%22240%22%20width%3D%22160%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22e5xB5EPQwGM9hv1eYVk4-11%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3B%22%20parent%3D%221%22%20source%3D%22g-a2YEmZG7LvmfDlxI8T-10%22%20target%3D%22e5xB5EPQwGM9hv1eYVk4-8%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-10%22%20value%3D%22SDR%20sRGB%20Plane%26lt%3Bbr%26gt%3B%26lt%3Bi%26gt%3BsRGB%20%2B%20Rec.709%26lt%3B%2Fi%26gt%3B%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22100%22%20y%3D%22400%22%20width%3D%22120%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-25%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3BentryX%3D0%3BentryY%3D0.5%3BentryDx%3D0%3BentryDy%3D0%3B%22%20parent%3D%221%22%20source%3D%22g-a2YEmZG7LvmfDlxI8T-11%22%20target%3D%22g-a2YEmZG7LvmfDlxI8T-12%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-11%22%20value%3D%22%26lt%3Bdiv%26gt%3BShaper%20LUT%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BsRGB%20-%26amp%3Bgt%3B%20Display%20Native%26lt%3B%2Fi%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22890%22%20y%3D%22400%22%20width%3D%22160%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-16%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3B%22%20parent%3D%221%22%20source%3D%22g-a2YEmZG7LvmfDlxI8T-12%22%20target%3D%22g-a2YEmZG7LvmfDlxI8T-15%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-12%22%20value%3D%22%26lt%3Bdiv%26gt%3B3D%20LUT%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BGamut%20Mapping%20%2B%20%26lt%3B%2Fi%26gt%3B%26lt%3Bi%26gt%3BHDR%20Scale%20%2B%20%26lt%3B%2Fi%26gt%3B%26lt%3Bi%26gt%3BNight%20Mode%26lt%3B%2Fi%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%221090%22%20y%3D%22400%22%20width%3D%22280%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-17%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3B%22%20parent%3D%221%22%20source%3D%22g-a2YEmZG7LvmfDlxI8T-13%22%20target%3D%22g-a2YEmZG7LvmfDlxI8T-14%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-13%22%20value%3D%22%26lt%3Bdiv%26gt%3B3D%20LUT%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BGamut%20Mapping%20%2B%20Tone%20Mapping%20%2B%20Night%20Mode%26lt%3B%2Fi%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%221090%22%20y%3D%22240%22%20width%3D%22280%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-21%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3BentryX%3D0%3BentryY%3D0.5%3BentryDx%3D0%3BentryDy%3D0%3B%22%20parent%3D%221%22%20source%3D%22g-a2YEmZG7LvmfDlxI8T-14%22%20target%3D%22g-a2YEmZG7LvmfDlxI8T-19%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-14%22%20value%3D%22%26lt%3Bdiv%26gt%3BBlend%20LUT%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bb%26gt%3Boptional%20if%20single%20plane%26lt%3B%2Fb%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BDisplay%20Native%20-%26amp%3Bgt%3B%20Linearized%20Display%20Referred%26lt%3Bbr%26gt%3B%26lt%3B%2Fi%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%221410%22%20y%3D%22240%22%20width%3D%22290%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-22%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3BentryX%3D0%3BentryY%3D0.5%3BentryDx%3D0%3BentryDy%3D0%3B%22%20parent%3D%221%22%20source%3D%22g-a2YEmZG7LvmfDlxI8T-15%22%20target%3D%22g-a2YEmZG7LvmfDlxI8T-19%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-15%22%20value%3D%22%26lt%3Bdiv%26gt%3BBlend%20LUT%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bb%26gt%3Boptional%20if%20single%20plane%26lt%3B%2Fb%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BDisplay%20Native%20-%26amp%3Bgt%3B%20%26lt%3B%2Fi%26gt%3B%26lt%3Bi%26gt%3BLinearized%20Display%20Referred%26lt%3B%2Fi%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%221410%22%20y%3D%22400%22%20width%3D%22290%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-30%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3B%22%20parent%3D%221%22%20source%3D%22g-a2YEmZG7LvmfDlxI8T-19%22%20target%3D%22g-a2YEmZG7LvmfDlxI8T-29%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-19%22%20value%3D%22Blend%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%221810%22%20y%3D%22240%22%20width%3D%22120%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-32%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3B%22%20parent%3D%221%22%20source%3D%22g-a2YEmZG7LvmfDlxI8T-29%22%20target%3D%22g-a2YEmZG7LvmfDlxI8T-31%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-29%22%20value%3D%22%26lt%3Bdiv%26gt%3BGamma%20LUT%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bb%26gt%3Boptional%20if%20single%20plane%26lt%3B%2Fb%26gt%3B%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BLinearized%20Display%20Referred%26lt%3B%2Fi%26gt%3B%26lt%3Bi%26gt%3B%20-%26amp%3Bgt%3B%20Display%20Native%26lt%3B%2Fi%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%221960%22%20y%3D%22240%22%20width%3D%22260%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-31%22%20value%3D%22%26lt%3Bdiv%26gt%3BOutput%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%222250%22%20y%3D%22240%22%20width%3D%22120%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-33%22%20value%3D%22%26lt%3Bb%26gt%3BGamescope%2FSteam%20Deck%20Display%20Pipeline%26lt%3B%2Fb%26gt%3B%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dcenter%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22580%22%20y%3D%2220%22%20width%3D%22750%22%20height%3D%2230%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-39%22%20value%3D%22%26lt%3Bdiv%20align%3D%26quot%3Bleft%26quot%3B%26gt%3BNotes%3A%26lt%3B%2Fdiv%26gt%3B%26lt%3Bul%26gt%3B%26lt%3Bli%20align%3D%26quot%3Bleft%26quot%3B%26gt%3BDisplay%20Native%20in%20this%20context%20means%20the%20current%20EOTF%20of%20the%20display.%26lt%3Bbr%26gt%3BEg.%20Gamma%202.2%20for%20Steam%20Deck%20or%20PQ%20for%20a%20typical%20modern%20HDR%20display.%26lt%3Bbr%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fli%26gt%3B%26lt%3Bli%20align%3D%26quot%3Bleft%26quot%3B%26gt%3BAny%20steps%20that%20are%20in%201D%20LUTs%20do%20not%20perform%20any%20gammut%2Fcolor%20primary%20mapping%20and%20purely%20affect%20the%20current%20TF.%26lt%3Bbr%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fli%26gt%3B%26lt%3Bli%20align%3D%26quot%3Bleft%26quot%3B%26gt%3BscRGB%20is%20just%20%26lt%3Bi%26gt%3Blinear%20nits%20%2F%2080%26lt%3B%2Fi%26gt%3B.%26lt%3Bbr%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fli%26gt%3B%26lt%3Bli%20align%3D%26quot%3Bleft%26quot%3B%26gt%3B%5B1%5D%20This%20is%20lossy%20in%20the%20HDR%20on%20SDR%20case%20for%20undocking%2C%20as%20we%20have%20already%20gone%20from%20PQ%20-%26amp%3Bgt%3B%20Display%20Native%2C%20so%20in%20the%20case%20we%20are%20not%20going%20from%20PQ%20-%26amp%3Bgt%3B%20PQ%2C%20eg.%20PQ%20-%26amp%3Bgt%3B%20Gamma%202.2%2C%20this%20would%20essentially%20be%20a%20Clamp(nits%2C%200%2C%20displayNativeBrightness).%26lt%3Bbr%26gt%3BThis%20is%20actually%20%26lt%3Bb%26gt%3Badvantageous%26lt%3B%2Fb%26gt%3B%20as%20it%20avoids%20overlays%20such%20MangoApp%20or%20Steam%20potentially%20being%20destroyed%20by%20a%2010%26%2339%3B000%20nit%20highlight.%26lt%3Bbr%26gt%3B%26lt%3B%2Fli%26gt%3B%26lt%3B%2Ful%26gt%3B%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dleft%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%221760%22%20y%3D%22290%22%20width%3D%22520%22%20height%3D%22300%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22g-a2YEmZG7LvmfDlxI8T-42%22%20value%3D%22%26lt%3Bdiv%26gt%3BSee%20note%20%5B1%5D%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dcenter%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%221450%22%20y%3D%22150%22%20width%3D%22210%22%20height%3D%2210%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22e5xB5EPQwGM9hv1eYVk4-13%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3BentryX%3D0%3BentryY%3D0.5%3BentryDx%3D0%3BentryDy%3D0%3B%22%20parent%3D%221%22%20source%3D%22e5xB5EPQwGM9hv1eYVk4-1%22%20target%3D%22g-a2YEmZG7LvmfDlxI8T-8%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22e5xB5EPQwGM9hv1eYVk4-1%22%20value%3D%22%26lt%3Bdiv%26gt%3BCTM%20Matrix%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3BRec.709%20-%26amp%3Bgt%3B%20Rec.2020%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22490%22%20y%3D%22240%22%20width%3D%22160%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22e5xB5EPQwGM9hv1eYVk4-2%22%20value%3D%22Pre-Shaper%20TF%26lt%3Bbr%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BscRGB%20(TF)%20-%26amp%3Bgt%3B%20PQ%26lt%3B%2Fi%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22690%22%20y%3D%2280%22%20width%3D%22160%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22e5xB5EPQwGM9hv1eYVk4-5%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3B%22%20parent%3D%221%22%20source%3D%22e5xB5EPQwGM9hv1eYVk4-4%22%20target%3D%22g-a2YEmZG7LvmfDlxI8T-11%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22e5xB5EPQwGM9hv1eYVk4-4%22%20value%3D%22Pre-Shaper%20TF%26lt%3Bbr%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BLinear%20-%26amp%3Bgt%3B%20sRGB%26lt%3B%2Fi%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22690%22%20y%3D%22400%22%20width%3D%22160%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22e5xB5EPQwGM9hv1eYVk4-14%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3BentryX%3D0%3BentryY%3D0.5%3BentryDx%3D0%3BentryDy%3D0%3B%22%20parent%3D%221%22%20source%3D%22e5xB5EPQwGM9hv1eYVk4-6%22%20target%3D%22e5xB5EPQwGM9hv1eYVk4-2%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22e5xB5EPQwGM9hv1eYVk4-6%22%20value%3D%22%26lt%3Bdiv%26gt%3BSample%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BTap%3A%20PQ%20-%26amp%3Bgt%3B%20scRGB%20(TF)%26lt%3Bbr%26gt%3B%26lt%3B%2Fi%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22260%22%20y%3D%2280%22%20width%3D%22190%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22e5xB5EPQwGM9hv1eYVk4-12%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3B%22%20parent%3D%221%22%20source%3D%22e5xB5EPQwGM9hv1eYVk4-7%22%20target%3D%22e5xB5EPQwGM9hv1eYVk4-1%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22e5xB5EPQwGM9hv1eYVk4-7%22%20value%3D%22%26lt%3Bdiv%26gt%3BSample%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BTap%3A%20No%20Degamma%26lt%3Bbr%26gt%3B%26lt%3B%2Fi%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22260%22%20y%3D%22240%22%20width%3D%22190%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22e5xB5EPQwGM9hv1eYVk4-15%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3B%22%20parent%3D%221%22%20source%3D%22e5xB5EPQwGM9hv1eYVk4-8%22%20edge%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CmxPoint%20x%3D%22680%22%20y%3D%22430%22%20as%3D%22targetPoint%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%3C%2FmxGeometry%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22e5xB5EPQwGM9hv1eYVk4-8%22%20value%3D%22%26lt%3Bdiv%26gt%3BSample%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3B%26lt%3Bi%26gt%3BTap%3A%20sRGB%20-%26amp%3Bgt%3B%20Linear%26lt%3Bbr%26gt%3B%26lt%3B%2Fi%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22260%22%20y%3D%22400%22%20width%3D%22190%22%20height%3D%2260%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22e5xB5EPQwGM9hv1eYVk4-17%22%20value%3D%22%26lt%3Bdiv%26gt%3BWhen%20scRGB%20(TF)%20is%20mentioned%20here%2C%20this%20purely%20means%20linear%20%26lt%3Bi%26gt%3Bnits%20%2F%2080.%26lt%3B%2Fi%26gt%3B%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3BIt%20is%20completely%20unrelated%20to%20primaries.%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dcenter%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22250%22%20y%3D%2210%22%20width%3D%22210%22%20height%3D%2270%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22e5xB5EPQwGM9hv1eYVk4-20%22%20value%3D%22%26lt%3Bdiv%26gt%3BLinear%20here%20just%20means%20the%20inverse%20of%20the%20sRGB%20TF%20has%20been%20applied%20per-tap.%26lt%3Bbr%26gt%3B%26lt%3B%2Fdiv%26gt%3B%26lt%3Bdiv%26gt%3BLinearized%20sRGB.%26lt%3B%2Fdiv%26gt%3B%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dcenter%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3B%22%20parent%3D%221%22%20vertex%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22242.5%22%20y%3D%22467.5%22%20width%3D%22225%22%20height%3D%2245%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%20%20%3CmxCell%20id%3D%22H5g6ek9Ced4drCcJTTBn-2%22%20value%3D%22Consider%20this%20scRGB%20(TF)%20more%20as%20%26lt%3Bi%26gt%3BLinearized%20PQ%20%2F%2080%26lt%3B%2Fi%26gt%3B%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dcenter%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CmxGeometry%20x%3D%22200%22%20y%3D%22140%22%20width%3D%22340%22%20height%3D%2230%22%20as%3D%22geometry%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FmxCell%3E%0A%20%20%20%20%20%20%3C%2Froot%3E%0A%20%20%20%20%3C%2FmxGraphModel%3E%0A%20%20%3C%2Fdiagram%3E%0A%3C%2Fmxfile%3E%0A} IDATx^E]H` Ye .'d%@!hgq`AB{~gNΙ9}u]tWWU]]NR2I@$  H@$  H@$  H@$  H@-&03 H@$  H@$  H@$  H@$  7v H@$  H@$  H@$  H@$  V4 H@$  H@$  H@$  H@$  (H@$  H@$  H@$  H@$  HߴI@$  H@$  H@$  H@$  H@}@$  H@$  H@$  H@$  H@$4Oӟ":!'}i6K>h~kr |Xb6O34_NG}t:Kb<餓W_}N9jcoO>Q)e}G:jz+R뛹$  H@$  H@$  H@$  .7v5W_!@@;6lYgt뭷N?tM7'x"0 ,i ~RKs=7!y믟N8ԫWz驧J ,@oHvZe]6e]iaÆk6;]uUiWn1|Խ{qAt^xQ_~%?S~(ː!CB*UJoIM8$ H@$  H@$  H@$  H@I@M}k.k;<̓p|߿r)C 2; QG{Fpk_|;/M7ts 0JVXalֈg}vaN=Դ馛cǦC=4]qiI' ̞{p)ߐo8CB߾ѵp8o8q!8!L±ng}6O_8נyk~HkFkR׮]Gy$ z"|>p|Ӊ'm>4xK_ӝw{pl8 ʂ36 Rnݚl?8뮑0  AeiРA%or("?6Ui6]r%o߾饗^ qSrfP$  H@$  H@$  H@$loۃ\su]7/I7xcvmC@ FϞ=W_{챴k15*B!XqA|p!D9bW^y!AKr@d$  H@$  H@$  H@$ &ۯJA %W^ye}a㤓N Q *77<̍[n ab N68oSr7~.-?lowq)8Ppe!> QGa{'bbgpRNhSKo8'7ʮ0 Cgo` iUWsp!6o#7/YBChS!ȳ ~ y!P6'u%Ya"l H~i8vW_h\!y뭷ʕ61I@@&\xwĝ'cg!Scp`l8f̘p5DC}G1v ,2,i嗏 [pc|Ԝ3vZeUc&ƉiX4M3m=L6Xd$  H@$  H@$P)o*E|KB N8?s5ndb,N"T$R, )'Aԃp-"zFCb1S*ǂF $g87H&CKa")` + S&a1 L4SG Ss7 qI_GYdh[634Sd=> 4E]Ǔ\, *emN(Bf7u_q+XN%7؆ 7MՏ;5(wr A?$il_n¥i)pl am߾}c 4nzN8a1 .z'-vL.`mMjp !BT) .^ztmEa=\(eoZkey$  H@$  H@JP|cpcb"qB檂؅&X"OSK-Tf"l!+1 ;qb7 g+x \mJoU+QY,ZC)>JoXP"<Tp}Nũ5nݺL|V[Hg3Q.X_veonH|,( 8׌O|3jԨÅ84zhOpmw=nM!|m] 6,YB ?/S+eA4A9M$tMBz%4$'`{p Wx/"уq% nEЀh*0qa{ WTV3>B<[y_n 2!?10qa@JI XaAHmV!إ8'+Q;ᙃ ZB2b#EKwuר;uB!tDL* +q%DۜxuСʝ9P,*0q'H3~6O'A*M9o /&M#d%  H@$  H@$ P|Rbf0o6# a0N*'a #ZAE߰e`~״SYwȃXH /a7,bQ6X4ҥK5XdaAay-1BRZ*AH"y°S,e^8\z饱`W+aq0XŹ瞛&d&7߂#ik7SL1E X!_7n;Y daGQi&xXHb8CS⛦W(Z5Ydq$ H@cgJ@,ޅN7\{㽆1-"V7Aޯ!,Fhc#`LBވb A:.T*umұXbe8 ]1AXqp@lB@44D^Gt͹$ƌ/З|o A3ΝwB\ C؃$ w o06` ~F(C[i7 E=q7Фc|GS({ Gz#!16& h^3bHӞA! T,OPω&(xeF|{$B*k{Rr=> ueE =;$  H@$  H@$ P|3>B^U$b({WJBY3>|H@@ '8!(E!K>`U @<Q*BL|y98a ."Y8 /spsp);DAOBpܙr)C(ù82g}B믇1GAKx1= B*D A/ XxʄL2i(/N88 (c\EXB]pBKNM)K&:AĂ\=~1|D<6F OPȂ %刖%22ȈiGJX/"BM)eG(V2Mq}SJo>u!ꫣjoG5jT )<Jo2g|(kz"T",i|C;#^I wYxXJ|Sx}4s ț@BYЯ2#q!!2 }\_ _F=#vSſ/|C8.D2A=A_,ބ0m^r%  H@$  H@$ z"ZӺH@$  ,!F 'BQ=oNw^ 7Yةopc! >_|. RZ eyS޽8;q>"Yg5yBx"D?< GJ׮]#&%(ă( ,! ,!4hP\cF!GV[-3f̘.BqqE)+u]+mqmBXn .`p)WOH֋rGx%ʃ@Mn1 :)ߔ>LB"bn>0M3q &|`O_|q:S7+)uuA C]Sk7zZkph¥r퍸rI& H@$  H@$  H@#f|$  H@*D`ر! Ad2,B! bPxdذa[o A7]vY'HN;mgn"i\x=p aAGq.*Q2ugONni\oȐ!!@Zjq tPW8P6'$.a"TG`qAπa*I|Q ++WOlHbhcڵ{ 7L4'h”o~ :rH ]Rv ƈPhg7ohr}q喋:!BCR-P&D6(іeDYr{PK7"5uT7[ H@$  H@$  H(ƴ*$  H@6L|WS~ ঄0Q kKBk]wM^xa[sq) m @$  H@$  H@@ ( \$  H@@g#vZ7=ZZwu AM$  H@$  H@$Ёt l/r^{m a)U ۿwG~q 1K_|E{?&tG}4mcvHrJ0I@:;^z)ޏX*|vv.ykKnFȧ/]Dzry$  H@$  H@$ "H{V`I#8I͋/fmԭ[V]'3&Ro V[m4DYR=3iEM?c_Fq B_~Σ, AqbQqJv[qc_WZ}[t]$  H@$  H@$  H@$ '۰nke=".]aÆ~!!@lw}p9c-ǭkI]vmiO[ne #_DrH ;G>gH'N;-vnOg.,B:Cѣөڨ<30C\}iJgyf⋣j?&p~KݻwO'pBZve#/Ρ,_uPu -"peE6d(7*tlyni9LJgy\s͸F23>}DqniN2gq8l;6A ~O9ʉop9S ?>k`h f/|\u]J M!RP믿>0 /p",uyӹyPG\2r Z*Nw}w8O B+X".vi2˄ yM6dq a:,$  H@$  H@$  H@$  ?7ۑ@'|2o.. D716tqDqUp> b_~9B5%!.>/!d 2qPPwIw8퀰B1C) 1 "wFh 6HtP^A ye6,mq6l!YsXp>Id"OLO?)ocƌ N@ FڎB9{ 178⊉GMp)N'u=\tve#D,˺[gw6TvեaJsXT|ӷo(?~i8!B,) uG,;:BY-Fۆ}ݗ/=Xw!ж-K0;nIʼnR3t_]z !. 裏+\`k$  H@$  H@$  H@$ A@Mh皭#<25!.p"sa@ A\ !sJW\ÇGgy&}G8 A|PgѩW^WB đ (#aaމWVz68P7B2PRF5UK78 B {kW_B~ a,b骫 S "v8 ^Æ Kݺu aN5Y(_5<\"}~rc&B4p;=g%  H@$  H@$  H@$  HeߴG *#/8͘$  H@$  H@$  H@$  H@o-I thp!.*& H@$  H@$  H@$  H@ꇀiKkw}w(r)[pVR߾}GL$  H@$  H@$  H@$  7՞F$  H@$  H@$  H@$  H (@^J$  H@$  H@$  H@$  H(umntG^x!+H't8e^?ϴ>/Rq.M޽{[aͫ`q>HvZ}ՒDꪫҍ7账CK/w,쫯JrJ{d(aÆEy$  H@$  H@$  H@$  䟀QݔP|OC=4 2difKݺumݛ+Y{zٺ47o '0Jt%-*V9MSE(q1c~45F7x㍴r˵kf& H@$  H@$  H@$  H@A@MmS]2쳄x{{M3nc 铎9pqӕW^c뮻{.<C|\r2DQF??LM7]CtAἲ~b⋧.,QM6$uY .Էo߸&+}iVKw^\P|n.O Aa~ Ro͜}3|E9V\q;h$i7Y "LqBT]>1GaliJmYo,C; A-pm&QL|T^'#boq:ea`X '"B*6zpM =!ȳP|3a,BM!B|k qX|CߡO!"!ڈqח?$  H@$  H@$  H@$P3LS~A[*!-6JZwuCȁX%K$[lpU)L?cqdd k> 3",fp{A,&  h`\ Ne7K;or"FAozpAB(#>X|O0$ ;f뭷Sp­'߼[Q~\apAToDyFxD 7MN4ԑ0 773YBDM? ?C|CYBXt95+&ooE@dD}w&!|٫yoQ,!!.sk[o Noiر!&wuWoYg+š^8}ikF AP|vF8 $  H@$  H@$  H@$  H&(fBob :tm'd駟~ GԫW" "i¡d]v ג;/f#E]4{,q-B"!3\\O?MjBp{"CDnC^ԋEC_>ꄈG D;-p]DRK-<Í7ݺu g=zD%=FPK ǖdN+B8V_RQ8JVjf饗6L3Mw}ӕW^ T9 uF486`8+sZH@$  H@$  H@$  H@<tzM 7~m80eNR ,N.Evu `pJEE #8"D: =1 SN9%R F&AT A 4FH?xpv-+80C9 D&6p82ʓ AZ"!Oa(+B%y u1\@pU­6wygq7a*.WP>#Z`:E|^{핸,%JӧA|Cy;bs=7e‘.N=j$  H@$  H@$  H@$  HߴgtÇ\b:2 "SycpAc$  H@$  H@$  H@$  Tj'P n)S<7 H@$  H@$  H@$  H@7yhːKaxQZSL1Evi;5y?0BfNd$  H@$  H@$  H@$  Tj$  H@$  H@$  H@$  H@5O@M7$  H@$  H@$  H@$  H@7"u%  H@$  H@$  H@$  H@j⛚oB+  H@$  H@$  H@$  H@$P-oEJ@$  H@$  H@$  H@$  <75߄V@$  H@$  H@$  H@$  HZTו$  H@$  H@$  H@$  H@yoj $  H@$  H@$  H@$  H@@(y+ H@$  H@$  H@$  H@$P|Z H@$  H@$  H@$  H@$ jP|S-^W$  H@$  H@$  H@$  H (&$  H@$  H@$  H@$  H@"Z佮$  H@$  H@$  H@$  H@@P|SMh$  H@$  H@$  H@$  H@Ef̘13L/B2dHZh#/ T@ޟ/y/_ZƜ% 9$  H 4iLO׼Nk>!N-$?~j,c{߷7Q@8^i\yy/} |;@ޟ/y/_}k' @@$  H4o9zcmsL/g& `T@ ^$nU^NM ϗ{ s~  H@$QB|C~MǛ4hPڵks'ϭc*E~_)+@$  t|1"RNSg=׎F~thE ]޿vPu(O /oI@K ϗv[ޒK@%s}5;oyy/_2T@_W1g H|{*>/ Tϡ귁%$  H@kC ,t\s5S׏N%]޿vPu(O /oI@K ϗv[z%ҟ4DƄN^xtYgUW]5KiwLllo4bĈFi)L#GLL3M߭iK;C}ݴB _5u%~cǦ'8?7x=m9"s(GaQ$  H@@ΫoE IDATyk^IJ5o Toe{^>W}{{wX; 7?_^]&D/Y&7?0=T|oZ3 H@$7o"\\|f' `#Hv xnC^z֡<Ͼ% .?_^mꕼX|CI{エ $hpɜo~>n-Qf!|iiI';ө4{4hP:dTu}u$m% H@:77uy{{X*W @;mfjyy/_{bM{{j-$P@ޟ/y/ݪ7cƌIG}t{9 yҵ^V[mt뭷_>7kFJ={LFJCMlMz衇s̑6l+iySy#sȮ  H@$Qt*]'y/_9}7իk޿uݼ\_˗m" 'K@U%KWƫыg⛉&>b^zu]VZin햦jpA|sWuY'~73_=BXM?7'Ll/y(ϡ@a_7?L7_LKP5$  H@y#jͪ;vۆ:i%HGQv@L34iWNg}v%]r%i]wMؔf/„u)ij^;@kҾ;W_}5-QZdEgVYe/;.nĎ vW7S[lnصA |#O!"C;HLB0Y®?J2w<ŭo=sI{gj'O.4묳9Fot=]v1j&Ɗ>h~ xq{0+,>s{{Ӷc4S_>_祏i)L܏|c^;mv ;ݻ7|3u%;vl7ҼΛx 1yy/_nF ']v%=iKvX,%2ׇ\0RC=4իxwe]>{ta_[5y|Ӵҥ7O=T>Ck׮ s̭O Kڴ꫗ fm#<s6,}嗍JԘpVrHۨG/H;s_:묆|ߴ3>j,$  H@5bĈ/<ۉ 77tS{=s5W୶*0&!ჍSL1E{bgm$ +~_=tקÇؙA'tRv@8D^\p8Gy"a@,SE\##y/_0+J={>Hm9LvZE}ۗI# twT/fqX+LyԧO8}w_Lb3w57cƌIN:i*=%qnZ{{|3<hѠAҭ!^-⛌A=.4|?}ƈC&p{~qo!IJũw]mX|{gTf18 !.3p@l6&~1w"`~XTc͔x`پ*rdD VA8d5Scʵ|svΒK@$ Z#a dWc29LN?8>2 ;;3#kv}Ge7qL}gH'pBzv~rW_}8\nk&Xc=>0df+ y,k!ޡfqXmc֤\|a^s~1Y^{-??\?=LfLhRESM5UsBRIbvsa |ꩧN n-0`@c+Uz~_o-j}:Z7W\ 9Y` b2;#yR" Y:c7)ݱrg7+]Sc"<{Lb*ܘA7-v"ƙTR|ӱwg-MA9 JH,&xy>^nFWMfIMV{{:iQW7yH{P(j/noTFy|nOPH@$ B7&@HK8v ;PoXGĎT\j]7LfeEq" /:F*߰XqLe;pa 7 _|1v1ac:.:֤\|a^s2gCCEK$ٹRKEx*2,"zc&,9{ eې!C"" YD*ĸ`j}8yHb Ba & l&qDk -0`g*p6Cc YB}"&F@CDxp>,7&ûk7!AL{ܘw=TpIMEpb-v3v/av*7\/O ˭t+|7}zw^SUJ|feF+ik?y|% H 6$  H /$!@ Zѣ)0TVa2wuW,A@3P=X(Lv;h*L$y&Bd.4$tMӒK.Я,]2G~,O˭tK|͹?-.ҭئ po.ʳ7J%t$P}>@$  t.A0S@G3Sˌ3;,&M:bD=i|_# IXD#6`+ayV+ɄAgw<3 bnH.Y=ý$siiERޕ< B1B},s=-߰3)"D1 0hfεqW yQ#Z#!!"7x#\d Dø&_3KM0Al 7M4z5Þ}p)7&bFXDk*)ػntتz3AYqG\j9ߔ_!`+eiq "daDķ9WY~> ^zըnkvpTm7?p{7 S9{f -OxoA"K`5;@ޟ/y/_ǴWIP5{m H@$йtP=QB߀#~ waom2[_~qOa*,ݻuN:餰GD81W\q8=5,HL "Gs=^J|8jL&$sIy{ZüR e]"laUW] azn_8 oozJz6evbO΋VڸL#`o'@  ݻsD,0 AX,/1>Mw >&45&B|~mZs5)rc"7y;R, V8?ٵk׉gD/xqtMR+r}r! Ř/ ԫP{iO8S뮻n÷Yfر!]˱ rD<3p ݋/Φl&*(gnoZ?G()X _BTvG`S}8иW\o`b;h N2f|=_GS?!{ ޏ$gu2vc=Y*1wW1'/4dȐt衇 X=NaR|S?}95%kZ=1;c\/##g_TN'.ߔ>汮W攇y`AomKR|zwϡk3K, H@j@7Zᖔ0VN;mӟԒrul?.^\5f 9B3L%`,_s@% x?]op)t7gUGV`RWZB8T!%wk$Rbm?^MVʎt|rf=j.?c D>裏xwBp"<92a!,ٛc snOSfzdR.mva7\4G|[eRqҁFx*ʍk_WΔ3< rȆ2"POqN):묓}p d!w S9SgG"=L(^?{y (LM'PdK$  H@F@MX [ \>SBu}ѣcUWK-|o^%ꫯMM=U˙ ֒< H lvZ}%DBD5lV) B#, ;USO5/ B;6a^x4lذ; n2Ec^{E(cs7x`en'_C$tSL_|'pAxD8 tݻw_VP .ă+4p)LMS&Y`7~:;$ߔ*w}a;NF!*5V}%]/GH@$  7ҒeQ lg|PcZ(©v^N s0K#{ng~xwX6$Y Gx^ iW4Bp{AABѧOtq7|Woy#g=X!yÙ_2"=7ovᇹD\_=r,HKB4s]w^z)B=!z!,#<\sq7,t"% Uaj5mo-t饗h?P|S̸ *N^U9Y꙽$  H@@(QcT(y s6 еkI&.J0O @˼#[yl(29s~1H͓O>dMozzஃN{o|ԥK-Po>pA<;38#\do;tGuH_|EtI)s)ޙ&N;sag|#Fv)\V]u믿o& S 9{$  H@@-P|SKՊf1[qj2DMe',ff&}3,G"{+Waa??y$VL@Gw  -P0BX ,wygh7=z͉'58<B \nʷ)_wqQz! V{Oq7fv瞋c9p)Lk7~a ;.8"LNA8vK/h~p2rzyyN,T똧$  H@@(i=*|4#!7$Q|S˂@V$)\&N(Ghf+) !GDXf-D8JH5\yiA!+"ᚓ D4oS:n1GX,~n1J{5 eY&Ae-H7tS9w}7z~pAr?>Q IDATg 70zQ1yp)vecL;qBW!r-C8C+Jy=22$lP5sRUUJ@$ Jڝxn8ؓJsԨQ'mI\%9L3%fmɟs$ҕW^9]~iglAȑ#ӠA6l?C p S1:蠰grJhT847퐉nR˗uY0iw1z+Uhk{Ϗp-uEXj/dz7G}҉ESe]x֓/L,`1oLS7>4Sge3fL\{Y(e6>Y{/,zN _x0ܑk<Y|el&ˋ~ýs%SJ> ZtEcgy+:ʌ:󪼧MG9'oygm&!b.9=˺v{X$Wu$7$  H@"P b`JbN vdI&$W ; B vn[29䐘Ģq ;0c#f9D8p,XPc"+R"/&V_}5ίCXU(*W򕂼{#JjmOs/Sc?*oLs7;GN rs$r0雥Rĵ58qv )LB[cלg7 yXe!c)D/qUWBI;7qMq*#_,{rgت=Uk]\G?J|Aw=ƜW]uUokF=SXk69| xiu׍o&8TK>ݖM.dɂ .mǷu&$g6 =Ʀ,gIp,XlbE;]s™KךSn 9GTƸ=yү +^b%b (oRc /0Ɠ,X,=q$EgX8FO[p@1) ⛅#KJu 1LwT3!>$6<k-*%j/lplv*Lֈ˵#(< d7aL1X](Ġ8(UH|ӿF>>,Bh6 d $ܫM#l݈؎oFjp%/7mM5x;t#=иLSCG]( ڨ2b6\v> H)Aʕq$|W@D!LZ[ Nھ0sqyʵ#ߝ,bKks}#]x%Bwͷ3*+C1= ,&b7&&pe~I|#j+w܋K5'pqpó\hDp< eDVoԗ{> ypm6w*Cl>DT|vNxyxNl~@8BٹkJԇ<ٱQ\>VQK6Ȇ,aԚ˚w}"ԅ0"2M]YuĢ{Sn^*3_+}[/-^K{ZZj@=?n,$  H@Anvc8; WLp0D7]WjGF^b&T\(&{H}-Y^& YLa"'f2&0Db6a$B8,a}5NL2Ť:L\35pMDBLT1I%&E4oJВqv~hb'$R$$>d$iLZsO[㼃xWjq;.^`bk]LFgmɽԞ߷gKNo{?yg3"C6uPK\FD&KEpG)"Q1|LW60^/FSDD3QbXʸ%"+8:ن"=)ZtE]_Ëyү(6}gAYsd:uD#cW+.gM 5vݙl2'M6/ 'vyYZ( H@'0&{X a[b,w_'̎O&P%Q-0pLR!aAM9&iD`®w0 d;J4a(:$1a΄Y9* rv"a8Lx"*"y'X;uh1̈́sʂrcHN4e/iI=L4JS]?AHLCb󈄘fj'eqk?ƿ[8BQ ;LLRwIXnvF|âO d²sxX~#;Ey~ 4@wnsGA?Sw(,$[^m79%z!?,6W7UuK{" ,^D9 | :l_ߨ|L)B4 >bUadSDOC}( 6/D ¸[e؄njXf\E?u DC\ЍxixWTx 8Px.G2>"zXF@C?p%b cͦzz"G3U={oz0,3Adؙ_fSm'aH);e/"/g,ߡl٨΄o!`c^!'N9~؊:ԫ0Dccq1ދ)3閘C@$ĸ_QW`P=.zM[E>ðaS ?Kr\R<yDC80F“k1,n#a$ٔ1-Dj!" "0F$+&0Y_!r$v&>^BG-B~=Mu#,F[>vR$ 脿c+>8LZ")јXA8/UTfW"uIL3nS"[1aK] ˮR`1r*CDbn ,vw}7{q֙DZS\8>u$P0nfn}/D w"z!q< X'L!Aua e,C_#D1^ c2*A±, ,n3~w8$crͨo<6U} PNtD8W9?ILm(/D0xh,\7Dsb 1C`26gAt匟ه y) wԫN#G` &2("0 K7$nB?!)QR;.*cCt&cGIR`]S7DQ|G1/A}G@|3nSH+1e(m ˜ yƟʆ $7$G0&6qc5цжœ6UE*8f'_i*c|[Q;N(G;HLmը\ jy)74e KP\r H@3)I0&%&?&We" !vcu;0ق=,쳓IN?.cb"&`bI=& ^e1 ٳ1;F`D7vĤ"A#@cbg*XbB:yrB ;7U)z<}L2DŽ,SqaY0,YdI6L(/0ߙ0e3ѰG&dO޷ZkN7 .LNP,POʎUGB r0)ʿ)) J}v/2؊ѧZNw?{C;Ulω%0 YvS,#BB)}´,H!12 Vea9˰WMnP|K;E_"t c&́f`" vaY?A5*#0.Y4_!!FϠ)ce~( cx/v{!槏0~C;xB6%b|I_r/nc%܏ ;LX'@DDrFߌGПB=DH;mB/phNc!ι͘clx_9Eq'?RND"!`AtXgʢ"1vJD߫A_0~BDe/vSo1F'O/ԫm@pW;1 -|"nNJի;k_q؍pw!NAJ=fB@hhӈxjD"TagPiyG6aO1,B^t1VKʑd^`OW`l-J(mC6`N&!ڣMG:SS|` lc9~*m7MN0Өۡam(9. H@$0U\|,٦Pv?1)L0 Ǥt™8äiLlS[عYdI.&[d YaI,1uL_gBhC&ș4 PjPŤSg <\D8`L(!l`pϩL;cg.LN_ŏ,`bQ d"L1Io&U&!LêA"-ԩC !,#6,$dz1VLȲ+1"PIZ&>๶Էn&Ig9^D}CPhSYzV$V. K;zgaq4/GDa ?7=мܙ\C jԑnb"U b2b~iaח:o>]-a {,4 Dӟb‘e}wx ؍p1 ,fӯd D WqDz^NTb9b$!u v@4}XY B IDAT3!A:o(D,!Ab9}A#W giw2)ǎ)cSʑm$ >;cr$ IIێG TaD,:)9ܨ{GzVS|Q̫P'7"@D|zncX3(OyD+%)b]q."LBʥ <DIF‹1}Ы`@L=)|AYSvԕ2v-%|G>͢=5/5Mo<\<ǷDM]`9:h ?;7 s{z]Co3K9ޭ}C&"x^=jO" >GY# L#HYHDPM8h:~c~aDJ[l ›aG1(Y) OEM1j$Qb(EX DaxǷJ!D0 .C"<# bD0l$QImcag͚8DDcc"EHx.?hP{ |$!`)"<~OԜneca9j !hXFD?6N!$ cDp㛁a5"NC0$zG"L"!,9*E8,NQ 4ֈ6ƴ-NJQg33O F47rBu-3ʬ?b]8Y:^>G{qWg-eLcN/;e;4U66mP^' H@"#MDG/01`ѕ vuO4˓ݺLEIM(Cu]SvhD\0@tn_4_bDceEA ,%d:ݾ:}h&YM#<U,вi(,:wμưjwjHD(F⛪͈H]ըIHC$-&E$@F"!`E1qDaAQ H@A4{D!X#p4|ZD&7#<\"#\)Q爘3ʑ؇g疣&Al+g t(oqGc!xQ':Q|6ԹAƜzD ߘGx~|π1Ѷc3sQ, 6b! z`w!jb0QD}r|@ʄob.qɘ6t|3e#E{|Au DC7lwp%vG0Edmz>~8 ehCD珃~]~X6e3$  H@ (5/z<}݊P,tLa#vƼ=&`nwgJ &~7ٶf?/}|1vkOT@vj鞢_tFY<6wu~0  ?L" 1ZDACt 70+1rGh8o}ɑt`[;sD8 u=kS@U8ZN\K" m>" NYq8 1`z1澅^8jH1y /7^;S'ayx|wRNPm#r"!D8$qsHh|qrd0(OHP|c* 1r;~Ǐ`R' xB34vhy|~6jO$  L7SA13wh5O{i| D |$04ۗQه6B#* G]{Y䀨#م6d9.h$J+G :D2HQ$(ʙ+%!VT)_D!a;"UCl |RD*-E"*T?3Q^GF]A@?{F ;ݞwJ@$ P|3Yn_ռZp- Xu: DYv{_t_BA˨?1#[mU>և6t|H%e(6Z±KHH&sL\BD0$DgqFpQ F)>O;,b q6͎)<}d lE%p cD! B P"On,)GS$DF]vYRM-ʉc+B L0 T"Qs]w5*KBʒ7D_$kp&_U<!цHQ<2M a[l{iTаm[<}$  H@,7%/^k(xW7@tn_e织O Eo%$ a޾>/ktE3& ַ#x  "x vmE;0H0eʑj8#r!/G g }{+v<}c߉VC DDAC?D2(sO>vxEW2zzDDA AMT)qUPI2 ߝ XwF)!Hɳn7Md"ro ԋ1" HD`hfƌ,DAd G!z3Bh1ƒa~瞩-l}3;[eqhOp<!B*y6>@nm.cܼ/Nzjlij ;v>=y$  H@%fݾūy %74[u| D |$0,ۗQRfA AAo1+Gqc(*5pE½Evmx;Cn L\rI>8%!YfecB" ਢ;a#"~"|Ap| !q;.҄a  FB AQ4{kJ4vel !(d`{'f fP"!&Ջ1e(;/gaQqWށO`?|Ƃ8F%-ʗHJ(S+$l\dEo:DX*ߺ{vmi%[l#)^>=Iկ~!b"DhrT 9Di.C Njnos;%  H@d (,GG/xj^C -8͖A_t,;=|/}/!  @%}rA|@[oE4z~7ї ysDHP{7mxYx$  H@@@MR ;biL@ƅoO"lt_t]?&ݾ1{$@%}`-㠈F27G !b Qm[l>[J`֬YD5jӔvh"9ms&{$  H@@]C~xt\<>~iVݾD Eo ^$ $}n_)+GMM$qG)9g 6#nt]w&PCi6# H@&3\tEuD`M6I dž&߇.@__N/}u>ݾᗐoE zݾaϕhs; -$  H@X`饗3{F[믟n:@[46Dh wȀ}|_/}W D;̝M zݾv{ vy{%  H@@ ,;oH._~y:Sg6c ~?D>ZC&`2`ߗ@tnn/}s'v޾Da$ js^ H@$D ̜9sߞD?- qU!jh0 ä%0\ D_&n0wh7KtN@ۡ6M$  H@M$<0gKl܇B -| !lh C%0d!} D^&n0wh7KtN@ۡ6M$  H@M$'7p@]Q՞{ţqC" XGW$ۧk@tn_I/kw; Hmnڜ7W$  4@`>쓖\rta51\!pᇧ{7wq8@_D8H wD}MW/}U D;̝M zݾv{ vy{%  H@@ `{VXay [omR807$\],k_t- H/knk$0(6Cm۠u$  H y7v)<0-oI;cZn2,fm[_=t '$~T ӑ~?K<-%|D5ԵzP/}r: H Kt╨I@SMP6~$  H@('=Pr̞=;͚5+*/=cf̘暴fuY' _$H?o:f$  H@$  H@$  H@Lb6$  4J@$  H@$  H@$ 7$  H@E@M H@$  H@$  H@$ЗD$  "&Vyh$  H@$  H@$ 0կOSzSf?-&uYC6}K{l3.EJ+no;/^(QP|3jO$  'FI7tS:ҥ^>pZmզ΋_{AngZ{|so\rtW'>Oe] Gҳ< _'w~/Mg?KGyd0@p~;ޑ^W/ˮ\~ӟN¹瞛_e鮻J_Wh.B 7 tw.-)@@W"}{ߛ'_O{L!Ͼ;oHvV|'[l +*(.hZfef<ꫯ>P~O~;yug&wO~_OvmA^Wz0ow}k'>,+O/g?t-5\3WJO/rYCNLM$  H@$  H`R٫M?'pB:O<,Af~M9gy<8|x&pw;>g/KW^6T)O?=x9I7@@M }$  H@C@4wv j$ \׿u|2~3N=ԄP|~N³Y<csZuUsX_򒗤=3Gamo{[_Dj! b ok޹Kdq[oSҗ4}k_—+2G!"D *Q}nU[_Dl~D0=PaCGM|n' (xu{<HdIo|.D D%\22o|<Vɤ bj:_}O6t\D ex]DJB}Lt!VO7B$^{wPK_R~[򖹑W:!>WMzZ|tA$`|G}ͷ3;g䟉|:_EU L!Hwƌ=A=ؕn`rgՄ#2BU?K%RUuLv!:.ʠSDYor>w؅X 6JjMވe$  H@$  Hdzկ~uu]sf8Ha.6yw]y>s\>Ɖ9>6\~yN !d|;si̻1ߪoP- ifVZL IDATA$  "&Vy % A,-.BD~g}5 v{a`~gQb"aQ4_#u & Af` 7ܐw *Lo%`3Gl8"dlB(Žvڐ'"0Ab &D;RgA[d A?,ow-"6hg!<8AC^` )QW09 Wnt1oxr@B$!G@g֬Y9 1tt&l TB /V0"QqG@qFƱbn7l#Yj暀 dw bAĄO}Ss$!w1i$ oh΄0 FP֙QV\COeN@$H  ";$  H@$  H@hapĜ18y61CBhõlhb*SH[,tΓyQLsӰ e O<Ⱦ/cP}F@`J$  QT1H'D(v&z,s\En 6 DGL Uq; Yj!!R  > .e7 baB`1G0aODf">~DbgW3؅05&CX7ͽMox(ǭڪOޙF2U.8o5&#.dLѱ:M3C$  H@$  H@! 0: b1Qַ œ\ sH_Mo4&>*lxb>2Q",7Ioz\K5) R5 o&[%  H@(&=h-,wF!T,!d9O*,Da. c G C|p1E"a ;`ʱD܏*DO!: jBQVwKAD/![^vV  RB\=eG#n5u; d 9IU"zHg5:qQ;:D"wqY đFՄh-D!n1LAHDBP_+eA'bfC"(SDa~X#4!Q5rCUS"*#_'dG* ̹DDDD@O)a^.D#>/Y(:<%BCqVLTT:cxjB|n# ؍$B䀀AW5MD|؈F%dpyޠ)R` ,A'b8N ^DQycP.L`w@ 6,BNaצn!:J->M^g}DYv2#ZA!'1D)Bp/nJ$^ %r)m&AFjB㩺%vQ J4#jmxc;7":1“:DGqXk˄[W$  H@$  H@0†*gtlb3s}D;fS|V&:N&!6qq,U[2K|Æ76ɉ6՘w!~AN?0߄r-o$  H@SH@lX,#ฤ]w5/|YQT7,#@! bĈTqD{gB(kv|ql;(=_׳#D!ξ&RJ7 ;p8ևeЏX"Lb|"nD%CQMĄCo7!ATRMoh  u1LA(# GY!H!H!B"jB.,ʰ3!A`CYr"1!đK0$|̘zs"]x:ʅHR<;\[D2J|_^yI+&s3/|hwA qwς!Ҹ 5F3!&x))M5$ H@$  H@@0F"61Ƽs"% )#}y6N1g-"n/ S1&=y@(ڽlV|,ڱ (WH@$ QP|3JA1ID\A3DeAje8 ! G0^wun)w^pqvİD!jVʎ[{YG€<q/Uf, 'y"G73gig!! zZI:hANgx䍄< 0@|"D I DVAVi=#ΨonQJ䷚GA DA,A'b>DA}DHYGxW52(<?e!yޖ}DLoQ.| !Kƒ"뮻r uYY^{eS( :'H4X !Mww5s Q5YxS"t k}yB Oe"$^;4b^$  qr H@$  H@$  H@$P'7u$  H`~o H@$  H@$  H@$ oTX* H@ fZ$  H@$  H@$  H-ߴ$͇$  H@m!-%i>$  H@$  H@$  H@Lb6$  4J@$  H@$  H@$ 7$  H@E@M H@$  H@$  H@$ЗD$  "&Vyh$  H@$  H@$  H@K@" H@bP| >Go|i_tV+ @%}$~mn[9wzo[|I"2Ag{7]\ 7f8ꨣߞN:и#:0CӝM Ea_t H/knk$0(6CmP/hݺH@GQ/ed !=圎=<&9ى\:6,>}*@tn_RԊaV_tbVH@!}nD{$,mn[|QkG.߭뽖$@큺x0Y9Ozғ=3z0~}ݗZh` Dæ:?n yknk _ta5%ܒr H z>=Hh?6Cuh$du<@Rdm{e,p1_ΜN\pi ѨmY@ G/@j D h|t/}PH EoE% !޾Do%  "P A& H`jfΜe hk{xe,;]wڌ!pWO>9uYyG; };K{t|qƜѽ7@]Ǡnߠ: @tnxX{mD5ĵx<_tkG Ey%@!}n4~E|SD/Y8tgn⛒믿> {ȃmsV޶xwi|#ONna>., w v<vLuuw⤼s<(_7!kW55}tPĚ8 /}@ ׀"Izoj&}n_woğ*$S"iO~~]v%{%Lz׻qGk7>=3{SW|Hmn۸wzo쵛qm•W^V^yy{hzsu'x^~~K,:DC|ᇧn!C: 8r;#YLg]uUow 'mnp|Ƨ@][{Oo ̙3ӥ^=iYP|> q/5X#%rws߃~t8@q-Y ~#)SMo_㵲O⛉qk]+qtU>})h ݾ AkLAkDM7I@!Do_fD,BZ^xaӰV" ay{^bH6o>Ϧ-"mJ'>'G?Qz_a~6C-ixuw_t.EOj7ݾv{@ Do_nN:#߰C?afmO~f j86 .\cuQY|_*GEb׿u>fjmMY`ca0†Smj!vmoݺޒ_ξկfq t<1=yO:3?G>2E"y+_ź;,=я$1/A[ve;tȢ.R|3rmuu44&`oJݾ  I GoL^h/}.|@tnߘ_t]/iN zݾi>~<7~sܹoXBuҟ <)Y|؆E.R߼͑oX|#}_Oox%yի^aK_tm_VZi,2@!^m{ݺ4iW\q|gnT<_c=ӟtWV[-+ E|h|wsGhG|ccҏw]]A]Wh\ N>C矟}+^P. (٭{{O&wޙEby!?M8}?_LB0Y<;ja<=3Gototnr'msU,Hh駟޵3#<TP d{+yd%,!lI2GO_vg?'d/} ~s<9餓%O&1H߇zh-OorkrGnJ[ba% ,ƚ'鴟o*_}cZM-\>҅E~߿nsi Do_7~O&a~Yr[|ĩ#0.aA/|aO v)\捯eDg.xFniӛꫧO<ŀUVY%o8ۏkIw}wnrر|3و<f=7}JFfpgA~6,s=iwO/]o/Aq9f̙3SH(Y,7i7N}k9眓'|fϔW^9/ow^BOznn`qYg}CFĬ|[ۜ_t &0$?{ŹjwJ_裏N,S1}(uo{=QY6-EU|؁>EsL 1V`4az:MaƅݾO`wE.':!7O䉊oߺn?˰/kg /gE|C(3x;xoSe˸0Eq1;OT(9FB*ɲ}|svȼ5/un]ݙN8<|:b]~^s5y9;l@\&ljR|?>?\%}XfW}vzp]A]mg)P7%|9ZG^򒗤 7p|^|_}"viJX z{:mOl&"Аpݧ>K.$G&xcgE|7y#"%},{&UcBbȀCY{GoƣG>yNia>׼&wX[l|'>񉼐TvQߘ$|[b3u'sD8`¹Q1~ݾAˑ([mU. krkIgyf^G,Yu[ΣtGz:K|C]bYz1Gl n@,.ψ )E;LK![ou[ 6&;¹ @_@o߆؝nR"3BIJo|;,ɮL#2YgM Q Q +$ݒѶM0^a%K$XT^>ޫ3{x~/ﬧ$7K1EoEw1~Ie'gU|Sr^'+i ;Gۗ ||$P?6C~҂wz(y$0Luuw,}D|Ä5; &MU|CzD$  `,sX(;hB%*~g"!B!;7e-;4:SU|CD LsE|*E%î " H"\v9c?Q[B=, W xT,P!G!R'69Rg#@:ڜβk("~XXeAPvs,i0>}QN9~%, V@~E͏@H_2 zoSԭ#<2߃Ѐ%o/~1!Ny}-_Zwt&@!3i %!&?h $x饗&DnDB||VY}7~Ͽw1xK|?|zߌ7~E,OꇱiW;in7K1Eo#@15):M|S')W@%}$~mn[9wzo[|Ijzo˪I[|âc9dq 皳mRPq %'шUU 8â%>$1dwygqXI,_b,΄05£0V7.og̘=,ֲ(6 a=j>6EZYI+qta_BŢ>!F`K͊Ѐmjמ+m-aDtt94~ݾA(%汏}lv"jK|ӞDF@^$rB/| *-g-#|xA"T#q Bwo謵ZYԀ=|E!Eo&| _G,C"`n-G![3"2G]YveޫF]տNqgvڭ{IF R?fl|i?-e?< WW⛱9{ż/ӟ$ hs;dma][{ZKM&PW{P{\VM}f>ϓG(,|ߌ@R=xÂΗ,!bQ`XwD&"tF>8 .c9&/tF!jǖ .`xugt;v<*!Z,WM/,+^I. "bDh,taGou]_uQ/΋ob@Yf;F%Kbaf"DsBZ`i0>}PYR? ~y嶘 ߰Zaub3"aS|7oE|C @̎jߦ/}kc";vaY љ 5$\_M% NCjH$ -{Gc5}"WD!*N5!8Rq]d&ԏ)dޫ_"t_1J=Ǵ1)zC=g Qz mI/}m:At7 1ˎv)G `E"t> TӠ'=IyBM7͂!7 7\nr"p\,Fq\ )z%n Gu Bv-+Gq"g>˿ ?Q"a'6zy) `@tn`SϽRK-GjH7_ uo, ߣ=Dab,DLj"n|HM!LEGP O_t(oX[s5s_o ;AX\?x#<2 { w!#A'};%2}c{x'8,ĬM!NA8$}~"-!' ^>ޫKdrOT|ñpK;ucXMP5덈SiϾ4 IDAT7ae_t'i/ S "EȦl$x4&km%}~D;NTƧIa$ld҆11 0De_1_0lͣ@!7@][{ZNv۷]-uuy%L"aI9sEbG%9B Vb}7"Ճ$YdsgWb((!0Q"d&iYa3 */x}zwgzox7!H#8}05>b'ɄԵ^nA7L JCLâ+T7-u]D\e@tnߠYK^m&D7_Ws]`RG}K|fE|{70{zo8d6݀m9-Eg!TCіwXO8J 2>w{EI ,*"8O|_s9c{ǣowq(pDR|3hK8P?"B(#;#r~>ޫƦ^+lトo VF_'B6NQf,O~L= \^=a^ Oh=^q06| A[L&@6zͻy召+1gX1O~r| ?~J }le_R][{V>oѻ$[4jzDK~` Az֢ *˺\g\Yg>ZVK} D0HT 70G;0 @tnxJXCBg](7%0$׿{ oB4k%Xb" VOTMDL 3l (S&NؙlKVC6 x&ԋ5c7ncOe3{a}i hs;d&S}7b6 JBca6)?un]j_[طڃ;2xY??K_@'c !dg:{lDjJR|ӔuWbcW۾Imݭ.H{7c̚~K18(Wڿ]IX?F7I@*" 6c9~:ѹuɉ*C4,%O̵l`9OW$D2lRI4=&L4U"D|3hlnG0pL7͊+val#8#rQ tUy{ jTlX F%X5 %&MHlQQ@ 5H4,hF] ~a3s9sz3{^뻞Uz׼5E3L|hh8rX+Se+2;ccO~"gG\v^DOsOA9ev'מ\is]i6(Q6 #q+ QW/%_)QTϦ=maU_=|k+ʺ~#s-2U;YqfCYƢvr=( lO5zyW6;⶷mt w%J5/7'O}Se^} 'qQ}/Cfji j,cXI!"_18Ģ#i,B&G6UAG- #4N:b/VUY E4bX*o8f/b=E3D~qTr:EX{boOCx"E¹(iA 86H d8L"],Q]hBDO 8S7z׻u]/ ^7 U "|_,y,cD(GQ87yַf>DXSO=D߈sZP(ꠊo8E mͰko.{jv)pN;*W #81{rH"82J$v"7GyphJDbH~w%9*⛭IQ-2Wmd=;Vt1ǔGA񍨕N|c.q"(mo>Op߬1 7] 42oj55W@RW_2>pn>u&΀sH۪K8koy[#e,rij}.R6x+J8hG"b(7U "èַ.NdNb8S5c韚EtsL*ܤ oXD8o9묳.!*_g?[=nva}ַUx+ %RCNwS9o}c+yrV \{ Vov/`P|뤓N*#k:3f%gHm񍾀G IYVzZvQt3"s_,{?$)g}֞EvۭcK|C1μ.!qǬK{yNhF$s`-#YQ2z*&X[Q8~J;Im=is]tXvIxgom)LBj9Ú{3A_vˮ;žd{%^f 򃓊Y2g`OykrΥp>M vC Ekws%JeS^- 8{ÝG~czq}7Q)HXk@ ͚CG&9ȱD!p;$~p}6o}8(ǝx}S'Vɨx8g?.bu{5Mۉws)#=y5}k%R]X.=}RC ByG7qWzyݶivT!=Q*G^+OF?lWjB+>U ^aOmN{\6Ff^"I};PkrGJ?:}RQ1;J/JuY7UGq2bl+s ,c\9Lģ1nl2v7w^9{HMqu]{Lx˒o2.^}l#gh9RO?7y0"y,Kⴝ%;-`ۀ.YdNF>qJsJv`{jRf2EZ'MyLz Eo2 /uʼn([mj}ϨF؎zNu;GI;ʋ_\W-Nvm*1ic,.oBYړ-,͚tmG"z1s ~ _0<g,۳M>ܳ.Bj~7*vm7|;E%,,=q2bjbҘdsEګ,ٵh<ԇ9ZO,3zX2soVcѦ-Z9ݢlqטiCWʀNލwLv>S=d_Lx`_x!>~M#( \,:z8<^5l[E /o.e?q'֒1枳$|gYm )6t%/ěg2}\lN_>,*l^/kD?i`ҏwX '=Is͞9ahH.8X?ikjb{ccSd>?IY ?ۉh;ZyWqXJ7/^>KUI&}B Nzy-lz[沉}&5lvp,yW5ƙ'?hdW6>Y . ;0_E{g{ߨ{gN:s{z3:裋5qƚKZp 6s[5&^indc<}-tcuTFy~>)kZa5lz~DH*pWqT~D$Y;$$yYkV]oԣ D_qosֺ1fs}~Ӌi :(jO͌-D5)OyJ[r*|{+~פz^NA Gmn^ 6gmUdUN-cd9(P9 >M@"zUo.N㱱Jvm=`4VMLxkͰdGT6Og'dd"oe'.v^-cg捋HiN(I h.$Yd+4朄7+ YNFT[CQ#/"pGMęOofM޻;!dܭcE :9>?g)ϬeUhvs#Jzu=QU_{;vzO vX– -:paލ&I]pLň e&!fv`L]WY7wV12~cX4Xv c=[7ksrQ5v$̱@'` 0%q zg"'+ךzEI`V狃k˶uY^ w[6&szl4P㆓'' mXԦD tְEpN{Қ,BsJ{Wl 9B9)-k 2,::s-C3*܇W7/m`Nu8G-L -q8[X ¸%N,N"Izu>I1:n!\K^k,Ȏ,FZ,зgL E=M,s :εu#~v4ݐUi_ˤ-[NOg[+iH$TrN-rxqr\ 1r&N̉\E|.̣rEBg}v#".vک)0 u#fz__]{p|egpOG>O[m+Iそ&3* o,j ~]#pp!@"b<90n9%}5Tvq\"Npqy=Ӫql9ʉ06k>"5r{Ɔ,c($ud J65{BxcSvvb'sܮz$6:=܈mxg]~glbxǻ\-ށpDh.fGI(j\VGmcMܔͩcxX߫~k c@vwTrs)sMӰzwUxx{|[nS}UeUWO1kU(ڱ~jֳ\Ogɗl֬ElYU^ nt-fnWeH?qu `_8TU,صepB]XBYnG N׋L@h2* Ea(J†dBW8SjVBQ8,X[b.iCCXaH. >] ubg50},l #mFfi:I?o\}~ݯص4 U-X6,ds M@PygYVv$ Ñ9{GYQ>hi윣|*oqn~-j&ނ XiQB;ᘬ3p8جghgF,rLz[8E-j{$>5}![N N9؊bYY#`p0ppۅKfA^uzc†%CQ;LѮڂzw" ];ڀv8Ymm:R 19;8\%>T{3v%B; 'ew-A*V>`7Lsmeo ^kimMh]2xQg8 c&_W=©rlo1r\#ϣ5n\p?cC;4=m茝`2w%1Xo\>7*G b!v)Mm`׼+؇99D.jzy˸B$4v;#Jh5͍҆i|YϴQ2̚?v[W=7z=mݓ} y ∟{LzggTו+g&,aX5{=ʼȜm6W]z1>VD]Ƿ IDATaɸ:oU vh?D~j&y4k' (guiڻuD#u'rQ#)[8G"MثgKilV"RǞF~T#6]uO݂;lzIY6#,E;9DŽ`ދk֒f_iMS\!6s?-M&v3qe![XT.Zsi͑oIiGӝȦ9PR0बza,[<;0Q7N#¢{5kq:U; ]-X9 R^,XĴfGg;9D->ejXtXUMԢ9Jy·;iq7K  g(n@: A ;da="fس8jDTߪs/,f"XZˮⴳv6,C ~-humJ% CPٵX(yjvo#!O-/AbISN6;-V >y~78a!i꺝A$ol_[nZ"Ϲf'|‪bH";N 6N"6gѰrsX7Z8\8o=jD8LvۊXb!XUǔ>sNc#fر=]Gշ:2稑9 cwb.h!EmC z\\W{\8͕8EL# &7 c1l0u4mQ37UPfO5"3j#l|G?J\R AMc6s}w k7j%QP찳K; -pڡd7$I^-6)qJ;XQbE8,,Mވq-B\8Z,rZ[kEhڕ,rAe.EQ,jDB]vvzj' 3 x(2cFűK~hjG\e |ڣEʺ;y~7(Q7$=mD`5؎:T#"A%H`Ʃ8%9Z–9DP 8e$P:p7>rs 3՘ǹIM|Tb5ڏϵYm=M| j}G[J$MRYt%dq!u5%$BU 'ּ`ڀq9;8f9J4ζ&|o fvb_ݔz$Zg.C8aNDTػ 'Q"0գy`ޝ7_O]5ww叠bmx\s0bbZ>ps͟4j2_r5Òqڅ9Ҩ48i+\Φ ˟9!f" ~?7\kWF3MugMC[`#ڽ>DS( *1.c\qNRdBu򥮻{qvٜx0MZzw?v=gf]vNJ9m?3k2󦽾<>B`l~(e[>{LE`QC G`Qۿ؜9Z℅="i9m,jXm"I=BN OD7}"3Qp9]iqN;Y\@,$IGȻd'dQRe@oday0YHU]dHJH`ɂk=z&]"afFk,]Nt;9cvCuQ'X9lwQNwIT# 8bc"e;qʃEšˮ89$M0ubm0LԜRNv>B<ȁ\Gh乄:J|0xָ:}0 '¸{3uΉǩPN1ȹP#蟵/N M9ib"aAd:.ikDwo|n3(QuNsL8L8|2~qv7VDJԤ_;G gs 駌ژUbbAG8>~7m~_d5ꅽrD05a-3fp:y*YIMvEN¨cVLgXl?Sվ9@9yBc"ǶghvVE߸|G0i>?1*48,#q_;m1(#y'NcN71tij5":0Aw7[ߵyOGŠ:=Db0%ƨ!!nƹmoQk0k6SY'9zj'su靗ٿlҸon!ژ^?1)ÛxO;<7/?ލ͋=a#~,b 8ڈKL罦F(Ą~=_wnẏ̱ \78y?d-A2vGg6oDu={!n*ʎ~}zlfSaꆭO*FNy\#FzGӇzlIiazQNGd]ulo=Fg6Ϟm䚄߮wq6G8Uy"֗Q]ZӬZ;IˑB fRUlwQ]NC,}͘5o,صhQՂ.;,\A`Q(cCFհӾ& 40E t/;g8(-E 8-4 K0f%8"Xt| Bs}oʢ%GEf>ga\ַ4E# Y-jYL,&=XXXf.o~ECcӃcٹsv? gڔTqρ΁ ؃Ȃ({( %ю$7ehmⱶiY;F9-Z@∤:7eH^op~!1͸#[T0X?l޸Tt՟6hgy i58OӔA;&e5]ME$yܨ BAiٞa*м㎗8h-o< uk#xߨy#Lm̚>^m޹D2b3Xoֆ j1kyeIx2t'w([k9fa [2wjN2{4U2v鉮x?<{$azjy>D`Fʟy=[wkޫΔpW16#<x/׆̓ż vRDB\qM77gy7GHC D@O5 xfjĨ}H~&?]hN$/c,H2z_50WΦ0%IƭxW\#/Guձw<Ȥv;aZѤ3]%XxgZ;>pۻ Y;M_3k2ͳrFϘkFDwG*L')c嬉-[oalz~cZ}Ddsv=놱y=o>qbk UIOd#iYlQDΜ5qU߭lQ3.g~h=2xm#iY$(۝#ʁ ϓ9v8kX}sֱ\̆aGON~BuiNo^ݨt}xϵQ#Oi73`'H3fyՓyybM{s7 >g)87zuXFk'/4:`cK(H;ЂCM9,Z"fKE=:CEvv엌+AEq߮ubR;y5 ,Z@gD=nˈJi]YvqȻ< g{\Q~,qXS'P}o}߸& & ԙP/ ~İs4;)9 2 /$I;,' Hl 'ؤt&N ڥo 137ٳ & M S/-*XX欲ۂlBžEq_z&cV{30]w{?/Y1ST>l>b@բ: =33gt"jZ^N"Q cg21Ĩt9Ĭt橜zx9z7>wsXΫk͵̯ p?ڣc׼8XΩã-,Ntbμ(G`QSmX%v#0pf6j-Oqר|rEr=;ȟ>O;|˻= b;Bk>9 rfoxxFP_e2uL8UNh-Y@%jGX"$.y* eښ}vd_Rb ߮Uq2cv/+ޥڈvOFZ>ya߄7i=@ٶ2c79&s\CH0ڷwEJW8F[Si{6L;)^ աJTn]g4pE[v,vS+9ZGџvy" [pNS5{5yKڿ#tձ;γv)C[>)ku.ȆYޯZQ%RgMu5j~Q>b}OĴi{HH,xa"}},%ZCeDbk8y=}DDmG4_3'cV;-m;Z[`ouz>J6I¿=_}=Dݞ+R|I_n^BCL\\%h/l0' BOo(5k\q=" Xoo'ƸznY8I@yH`NقM yp ༢h'cvY|h}NROluՕy22l'1 c~sIubic\9m0~~N\7of{#{Q 1^Z-,["0Y_Ʋv-k'ߓ=Q^߫f.~/".$@-:GئYL%J*_H"4kq/ m N,s:>geߨgsc`?iD1@"vRߴCJv$s8G$-Q,웳%$GgXky);'`@bE\~"BhDiG\ygsؽMRq&+D=PĽ8ܗH7yq^qqR|8pKX`+yL" @I&РoXG"r3ȯ(\qur cG4.ǷE5̹DZñ9?N"jhnr9Ŷf%dbWlLYذwlfLqLk3"ǪU_f"čIǣQmyX6 =A`Q;sƦ9x9 H 2N=Pјb՗15"yV`:M 6oM`X>MD-Ļ5|Ia$+Qh. ȿjKl\2Nb64-Qbv2N DL􏛈eAQRS7K$1ow:502!,R/5ziW;F&GFv];]RODeS&'fkJF#9“Mwp3iK&am˞c~m`{Nr͢;Ir"aFͣ ;~}o3Dk}/dQZ3'}lVuh7pvz{-~8F-L6kYdlQ F.}+CMF揣C8s84 ~N~GRq G;ӂG2:ZCP@@r8 G"8S3GYDDC\J3FCpqh IDATR%Lv ;O+X DuD@$r#.prdW<4v#&7)9㬋9VEr8e$B1zqSщ,= _mJ}r9&88S9 9DMb ίvmّzS~8h};md?eʶyXN\qd5ΙwK%/%0G1N C""XE>D_&].&09OMI̋X+ s<%J7^9(}:8Bo"8ƕPטlUn9Hg0&wcoG˓D0jC`mn{̱{*CZ fTwBo5%)gzv) ,ez''sd4u^;P?D]<.csZk^."`öMWMLrIYs'[@73ވWp2n}RVz2=1_71\n}RVz2=1_71\!;}_6*Dۄv1ġD@CT!9Y8hY`Fj"(9k'Ⅎ#\/2(:Ąlq C08Y♎gl(GN8bC͑X::'*1E<$B'q;qvE"86Kr1%{8DjA!FO,/NzМy^xa)R;u1TwvL֑"8bc.x6G\t=I\cw ilhQw"Grql.`lO$GfT{iWQi':S]XVhzֻ-z9iomHxN6^ w]CPc4RiD'UFt qqI"j\FNGukE1Hj*㼹V́)"*{eWD6Y #I}`UEg,w,(sVkerv %k|T~sz,g"d^,en%@vȤ0ռSKٵ֓yl}z26S;&}4 -]ǼQIiI75zuf6~K7o)*9\:}o*<@ v.^:}o*<@ v./?G/80Tdž(0v8ۍ.&؄Fٹ]+;c D 8: pǯ|+3mo{r{1DLfwݽ/LLc7<;vۥlc D Vn" oy[HF7Lq''wD2ϖO}1!癏4dnngCx&_RW}({iqE=w<ڵf}o}Tt2Tn}RUv2;5_75|advj}ojB@o٨qeK"qRqCxC! {] 6vdBpCr+\88@`'O((B?98z8M/ϖWQqj"ȑ6q"R g|;'M- Bw88+S8nF%",868]"F=t' E&iy%aF|A^]9$8jɑ8lB8CxF4YwFMue"pZ)qܩCB,B58810gsMF;mT?vmyXN\y@MDIc~5#D0t$D16kފԧ5KUTtvn<%f'w=بĿIXK0ke\q9&}{cc_$c9=9~L[vFaTʬz>q|8J@%H>k⨮?{W~Wl QH-o9qxj(4WOD5M-rxE|}t1-sZqyh|U4>&駦&ߤ,깓/F -ŷވdrdx*}o*عxdx*}o*ع8BW޿=2EȱN&x;rz|om-.Z m->X.}`^y }lմ-DZQǎL?%.?XsoU0߬]=+`")TYn}VU}oUGn _UG@̙@V)R.s-poF@ x;X~h-RPw@`QzAD\Kr,>0_<;NkIr45q"ߙ@钐,I."y/:Co-Lw~w:H\p&w{6%w{6&w{6%w{6y{ͳ.r~POKX.&~<ngqF˟|+_:]|-Eun}F…=+l:+Q_F…=+l:)z,=/}@ !0fR՟ zJ`QXiD.cs^eƞ{ys"H\N;#h=lQ[o$c+Avp!n} Jp!n} NKOK@%ml炞X.=d+Vګ.cs^e|9, }nsݨ4⾦Eun}|}V=k9Q_V=k9)y,?/}[@J!0fRqJ`QXe9&iyUW_ܜ%]R]Rj.fm-Eun}F…=+l:+Q_F…=+l:)z,=/}@ !0fR՟ zJ`QXi~_4elNӫ߸/=y+X*4޽޻ץ_Tk(ܦ'w{6xn}ߊϦ/~=+n>)~,5/}RW~2!0l@.!E*HB` ,?.c_[Q7~5׾n?+Vʪ>{)7d]1Iq7@'}US }oAՖn_AՔ,@[P!s C!='ƗuX.1C]Fb el%q#8?f}iG77 ]z>+o:ַaUhйmPU1!@)mn}os[GJw{bA@֑҅&֑҅@ -6ezS@_2&e\B|^xarq)lͩ\_͗~_s;ޱ6,UunI!zY^VJe*c;@ǔrJHXNaC ZB B`4>]_Y\"e_6܄ ,V2m =R]PdU!!!!!!!!!!!!M`3o6e!!! <(!!!!!!!!!!!!A" yL@@@LH AoP C@@@@7X",Qe%!!!+A ⛕2B B B B B B B B B B B`fd!!!YD|Yj2X ߬D5!!!KD %d5B B B B B B B B B B B " @@@@D|ӯHnB B B B B B B B B B B B`$ob !!!!/>I H@@@@7&B B B B B B B B B B B F&!!!"M# "@@@@D|ӯHnB B B B B B B B B B B B`$ob !!!!/>I H@@@@7&B B B B B B B B B B B F&!!!"M# "@@@@D|ӯHnB B B B B B B B B B B B`$ob !!!!/>I H@@@@7&B B B B B B B B B B B F&!!!"M# "@@@@D|ӯHnB B B B B B B B B B B B`$ob !!!!/>I H@@@@7&B B B B B B B B B B B F&!!!"M# "@@@@D|ӯHnB B B B B B B B B B B B`$Qs9vm?y3>׿'<p@{6R !!!!fBP,B B B B B B B B B B B @`׿~s]93^%˓o7/zы}(j!!!KA ⛥d2B B B B B B B B B B B ~E`f7Y?wۼ,io.%/yIsQG5/}Ke+_:׹Ns6wӝ> 'P͓ciy{6lM?$!!!! ,|!!!!!!!!!!!k!0ƽ7m[7w]WU%O}ySڜy[7>/J\~mo{[x9沗Z@@@f0X%ӈoX??-o4{WyX#l駟\z+}_//e.K_R>qD#U5B B B &!!!!!!!!!!!KD`bz͋_fv*⛣:y^WȨ>͓f})B]wݵy3?o<欳j}{7w\|u7W<3O|b/~/7^"j@@@F !!!!!!!!!!!!\x͝|Z׺V\tÉ}%{g9F1S|pnnQI'T"ɟI~5{n9j{7 IDATSO-B=أ9w}fk^Wo(<,B B B I EϳC B B B B B B B B B B B`J7S*@@@@́@7s[@@@@@@@@@@@FfH9!!!!0o&㔫B B B B B B B B B B B B"E5$!!!!@71X",Qe%!!!+A ⛕2B B B B B B B B B B B`fd!!!YD|Yj2X ߬D5!!!KD %d5B B B B B B B B B B B " @@@@D|ӯHnB B B B B B B B B B B B`$ob !!!!/>I H@@@@7&B B B B B B B B B B B F&!!!"M# "@@@@D|ӯHnB B B B B B B B B B B B`$ob !!!!/>I H@@@@7&B B B B B B B B B B B F&!!!"M# "@@@@D|ӯHnB B B B B B B B B B B B`$ob !!!!/>I H@@@@7&B B B B B B B B B B B F&!!!"M# "@@@@D|ӯHnB B B B B B B B B B B B`$ob !!!!/>I`7W՛s=Q\2;м/ovyRW~yғԜqSw խv壟͵u .y;袋k > WBas+]i} _h9昒cӜy晗,'Ș8f=,M2NE^߶Qu׼斯Mږ&}unw7Ӷ^v~ך|/B`"Y#|-B B B B B B B B B B B A`3omnӜwy |op;4ַ7x-7Ѽ}kL*yK__󒗼d >OsK]j"6ܼE/*lG QF\t߿Էkq// mYn644>"L5Ks+^j`g6w]7 [^ovm%^W̒v5_כk\[fҶg7Ӷ3#Yk {!0;ofg;@@@@@@@@@@@L Bwͫ^{^|Ι/R87Smݶ|!yHo~5'xb{Es߾ysS?SN"QGUx׽y^V׹u ;N~=y&7I4};)O>mݶxVsN}-o)}{';[!f긦|#[{4 npɽOWBH?. [Rڎn/}yݜs9.<"JakVmuF5}o~"aҬ}h:rJa ڍK"C<~qKiڒkxDW~jDܧ-ϯ|jy>蠃=7_WJ`-~5[Uڹ@;ۿfw,<ҖB N ⛵7C B B B B B B B B B B B` Q|9Op@@ԧ>'Օ8kz G4_Ao\)׿>?EdB@AlMsgo9v5yMhD߸e/{ْ׏~:=yϭLpB@hX3LqzQ5Mw""p!N;TAe#"݈XߚN;E0D 7DBQ1wЀ- /14* C-??OX&D]]tqWy0%j7]unwV{c[DJ#<#"%!z__f8Lt~??-3إ4O~r/~ ;ҞkQCҶ`ډGpP-!.$H"hS57J|_a#ab#/}8OR D|vvf@@@@@@@@@@@l8o)89%]0w.ns#(&ȸэnFofy k[E#"{8g("\/(2>g=r滊oDLQD .D#s%T!i ID dms|=(u$op__ִӠ`s7F093*C蠬1A!vmWD2!ıްDم@R1A!R((2'KvUE('<.DèvˆFmbI7]Ůa?Ж cډH-konl-!!›8Q؋DH&!12D=ONE!d#@KpB #tru@@@@@@@@@@@,@7D4qMW&~SUN9E$]wݵ8999kuFb*99"x&BATl1q:oDAG Q@8o8DEPg A9E'MoU8㉊ک-RD'!xq6!_UF gn'}y$Ht Bϔ,D,joD! ^hHD!"Plq\ԋ!lI$$h(K]uL|UavKNauS`Q +ª4xqzN6mEFmu8hKlhEm0D?,oEVTlTT|Ֆ>58j0'!$*!ђh7Dh!D@%"{V#ؼHd}z7~iU} B f#lP}8jIӾ-3L|zN6j% c8Ev K=fw/sF!r\9JD7/P^򒗔#S[|sD!L!!'F5{ q( 71kC|"Igm QI M*b&'r$Mo"ةS/zыQ`!:Hy7?,7D'Na"|D=@Qa^8oO3M]U7C2n #iP|U7x¤-hvDX?D8h'U|C|~%mNإԿD3*y^,cj"(~xEfvNkDǣ9K2=+jՊx׈}ͱh(; _u)wEO;%Q}ćoۥ#D #.\ 5[e6@LD ⛉0oP! C4QeF8EiED*!t1G?ϋhӘX:;"ҩob Z9Іk_ZN;5CуG[|wG,3 "Uȟ|CBpy5/˷:}@>Dq4H ԧ>O$ zV NvVb&DB8RQ4D6"y#mԛ`) q~e@ #q.J|#9O¢: ED?q!H287vEqY긫ŶAuƶ4ό߰?buLUYk7GW@u];M]4(D@:S͎LҖDp9*}Ϡ QDHv '4?ϊq;$4Wun +7ĆsAڣ$#Q'Xi},X?F"@7QOe@@@@@@@@@@@}LS5O"po8EyEE J8 f8E Pq8Fګq 3(&(|"qOaiP|AoD!q<GqD|Q89ΉV"-k_f]vD8F :-LqױS2ωNx B,䈐#z( 6zh+!I'T"xܢ>/(mX&btUǎ 3nk$?bj#"uER!c80yӞV+06D4~)qi '򖷔G7rUyU>RgsL)1groDqliRfҶ"azDQћge7I%5G9g<x>RgyH@̋cE[Kz_7"8/)B`>"%B B B B B B B B B B B 6o6r!!!Sf X4B B B B B B B B B B B M E@!!![&!!!!!!!!!!!KD`7׾ǍrWF7њjQT|+ww?\җ^=7Kx+7O~7ծvGOrE]n/|q,g>*WJsݭy_\,|C|CZovqR_|qsի^=Yup-l7oۛ}k/~ͳRT WBs{߻yӛ\/lN?r\N;;o|ϫ~~>k=+~JWҤrk_fWzN;__7mO9fnlucb7'xb6۔knq[<嘩_W,u;2?Ofb[ '=Ig1wX߬JM!!!!!!!!!!!f|ckvag~-…N;9蠃[Ew8/s4O~߾!hI OxB}nEQMnrf+?C@ӟt\03q! G] Ӧyo9ʋzG>|(Y׾[G=y.EIC0Ňzt`y[R"N;lfSs% !']wU7|9[JG<%Y|HmƧ@nJJ)j:DT){5TW^c5o|/1~ !߬: @A  @A  @S4o(,I(_~y 2 )7L+Ùz; -LA@t$qiu`?(\xᅅl!=u$ϑVavk ͫ^|g! 7t]܁(RTh2̐>Rh({P=BeURƑGY0v*gօ / P?+|E!9wkQ)y)ɪ2j_WTUR1WYUd.dD~6I_ $T<Gi!BC]`m 5*uDp5 ( 5U+jUQq;PԚر[H]8|A!P:.>m8dͳۆ6mLikkƴFo6@Tn 531!CQn!DS.kVkJ8y.hac 0 |3 : @A  @A  @ @V"ˠ SRaaFo4V |b Bnh? WFʬ;N ei(BSa)zIIPMeu(k&gP=Ye6h,g ,b škYKT4P5 y'nii#)nFaSQW$'pY2Ʃ7(j7TrGY`!` *2{%hODi+4=WĈn_.]`Ov-CphCuSF.z_]U ̆ϻ2_ e$8uܦ1@{ы^TF>") ~ŗ1|#S۾/5u4ppW)}\J.d5Dz6(II=wܱBҩQ rXOI5j잊r*B+ @A  @A  @A`oG]ҜP_A<tFijiUT4]iN9CKЙ: JjE!㤲# "e77>< &@|e% ZqϨ77~C ;9_Fo܇ <k!%/Y`BD1Gj0AvdQlfsJ;k_ڥRh2IɃ"9STjs-,y䑅X "Jq RS`Ul}( 3\/D*j)!x> ^aQ?o,DR4ڵmvFAܢ&#e CA8c>_7TO,R]tEGR탔z>%'tc<@AHd4$?~D4̟|ʆf|B6|C !3o2P{)*7Em6Z5^73qw3/6j^GpBL@A  @A  @A  I'ߌj- BFDF`> ,Րo &i(fQ uXXfuj5 @A  @A  @x|seEs=U5M|A`R#!)B%Pѡ.FqVU.R Ѝ@7 @A  @A  @A LLfHQjBGIk 'l%=Ql@vm5R^Hjf@7, @A  @A  @A`9f9ϻ@A  *!+@A  @A  @A  0A|3A @!߬͜J @A  @A  @ӂ@7ҒGA  0-|3--z @A  @A  @!߬͜J @@7X)jA  @A  @A  B @A_|ӯHi@A  @A  @A  0o A  @!=R @A  @A  @A D 8HA  @7j& @A  @A  @#&@A ~!M# A  @A  @A  HB @A_|ӯHi@A  @A  @A  0o A  @!=R @A  @A  @A D 8HA  @7j& @A  @A  @#&@A ~!M# A  @A  @A  HB @A_|ӯHi@A  @A  @A  0o A  @!=R @A  @A  @A D 8HA  @7j& @A  @A  @#&@A ~!M# A  @A  @A  HB @A_|ӯHi@A  @A  @A  0o A  @!=R @A  @A  @A D 8HA  @7j& @A  @A  @#mo{+h~ӟ6W9裛W|?汏}l7׼o;4'p¬׹u_Wu{Y? oxC;7뮻n}g?UW]f7kva5yMsk_{#Xo~z=я~wmf 7,|o?jWZQG뮳~/򗿜}x)|og5ozӛ?-baM7ݴ=ozЃn1g[ jY|A  @A  @A  !0 䛳>y\hw5[n墶B _SNYr!Pxk^9ӻQ*eO/}yh57_\pAg<G?JQJ9W o}kv]G> f}YAx hh9f5\~(+կ6mY!h.Rϯ}k=;}CO~Fm|__)Ă>{BnY{ inڟ'g.wi;찂{|Eyqb-VC Qn'ƍoK[wJZ  oR;|Bu+>E>iOkN>K^J:K7~K[ P80ѶI|떷*>c'?ږNn}[7mo bsܧԂ  7>蠃/(A<9i~67M |0e{Rn4/{˚Q6u6sij=y?U` ~|g]~&ߨ1O1O>HɈJ1FUύCw\?ncZkVx88x΀׿nx;׃b0\mڒ2q?S~ 0v>2fGydGa1X7u5= !}ԸJ/&U7> PG=QE??'^p"c= N:rPiX L)c=R 0gV]h}/BG/}/_Z3 A`6}|{fu A`2quCbo^xe7 I!c$# G // z.T'k( #|*ud_H-b&NGJtdۮm۠@4o_N[W9 ˻o7,~ qGYa$_+½Ri=sbo~d+$mS<1`G47ʵڅ;&AHiͷ3,i&R2wH,H]K]~g9̸ rŶnY D(7q:3!R,شf `ka|3`r73s: eSW$aР~i܃lPb0[ FM;9H.J'?eAAG>R0h(BD}>P6x_qp|G[0v~ofw˿_ېS'rq8DaaC,4vS8c(kaX~ʏDtg@5Vt;9J!`̱MDbʇg I-el~)@]_˷/.}oq['OA`1-fA iV74#0ezj L 2E܀( | PDGJR6_WJD Z;`N%Y 0.n |3:sb u )}}k :w3EpWU "guo(FPz(m f!S0ϥ@Ѷv@ v|#LQ|G'䗮:OӛuY(0Cz׻ފHŸC A!!z/M%h;mX/d$Į*nE|+˯ɼSx6fq"r3 b1'c$0-59?!UD6FU YXW[#+l|mbn152n6eC]׸R?&0ٰjXx s[}+ю@}uK;@[VS@X >|KFyGˋ4C} 0ըP2UO@C  #|CE 47\&@L7AQV@io5 f~ BEa!6 PxE) $Č oT/QPl %(Y0) OxF%P0<$U`(E½%Su]WzboX5p}ի^UH mCd)NBV@B"6|(0OFoڒ6}\wTv,@D2Hчd1~+f>}ɍ*CΐGs;ǩ8O IDAT ok $ʅpa1Nc_JNri$mw fUV2TkoxJ?c!kf2v{)>T(!]m g}E!!]X3֍v=F8߀ P7>>$Q%%Zھ֬o rMjfB wjն=SE-lkiA ,%}_^l+A`ǡM* b0!] T_@Sno( P(ACvABL%>HB%32!Pҡf"n& ;ҋFAM%[|H"QӟH JeD&҅t.ܻz8wE9yQۺ7P[ Jz bWW/DEEڸL=$ԧ>Ui_xQQOj0mCV7or!)~2|3ԫ-)8i61|U^E]Ecs! YD0*M_a6$3FYb㾳@4 OUBB3HAArI'Tڗ3]$]~_q]]X3uWFo|ƝU@/&*kbӅV*6}-S5]匿=S իV۾_׷Ly_˷ }|{ iX?EA౦R ?UuDDQ$iI$ nH2hmQQ|&d@G3,z ; %=ϫ_B Ag?[2MBpm7vJd״7bsrdWg5(xM\s;ݩwܧD$%J.((g(zWq]=x&Xʦ>8h]tmp|#>HD|Z|p@@2Ї>Tbچ,Cųi !C)/Sp򴭋|;t:H3M7o  ]~<Ֆ ]$r ͗|C~_#4hR!f y:#L!X|3)=Ӹ/=q4xJwG"%Icsw_7TM(wD'?Yʆ4 ̗|~Wig/"D3Dk\q7mYg[ kVTUH[]X3u fXEgƝ%孋Kn*LNkBďa@./Hm@2H\8^idl_|*妄d|DfoOㆱoͨua1X7u55{ ՛qk'Wǽʇ#Ħ GۊM`G ~&J@'qD-g݋@[@X,>|.ynA`ǡiLt'D0f* TN~7SF̕W^YSRfU@8=Yi/c4b—1gE|o1Zlz+UYrŦ K/0I)VB_$Z|q B6_7ƹfr|)y}_^xPӏ4C\Q)E*(ThV'bg ×]vYQW9s t/ G`•"(QZ [LM|o1Zlz+WUrUըܾa5rTuE\'}o| uFŷ@\>|Wriկ^ү\jW+7S QJCIZk5_~yI Ҷ޺&mTV6ꪫ&`.M<MsFyT&׽uK()N8f7/L,,-E^6{5RzZ%^VDžCgŢU>=kqU  O^h 0NnMC}/4@Ѝ@o 0}|{&嗯|SI/,/yK:K&|SAQ"04mz=25 A  ouUO^)wTo/ymXo{׾EO}j׼fAojC}O<Ms&R @A |3> x7-e>  @G_7gsD w{&S "[A͕W^YR |ӟ^Ism>4?O>F <ogqFV[5W\qEsg6ycO>-o<ho|3?<Ms뛋.9#;4nms衇ӟԜr)~6G}tWr{?>͛BfohvuwJUy|K_*"kg=YqWnrL:,)A L)!Lij}M{z˄@~k sF8^h2}|{VGo~}I'2ͷ73{EOyS/J67m٦7qsyV묳NN?a{X7o#04m&F@$f|󶷽mo}[o}[ޜs93qߍi/| w9BBy_A|g{[ouoymns}{_sg5Ks޷y39'sSA @ @} x˷MWFWNUߩk҉P囨Nag@5! MCNPAoSOm4_Wo|CFڨ 7p8"^BwSQwݒfjoǗ_zEJ(IqZk5_~ys_ >q[ Xʢ|ՒvjoEqh|G l͑GvkP3V0*1{nw5{w!m'5_Xqa5_Tvw]_EYs7 ox}x3?*{vکO|b)3%/k) \veE As Z)ˎ;X>Ϯ(\l:C[7lBȹ-n12_7i'`QczЃ=y߃7xSV_eCH{󞥼~K!X³Mi5y r5 z@7j/L'}/·HN & |&}o!Y_ A`rMn/_7$ToG7׽V( ᬻ /Yc5|lsܤo^җST xφ|G>CнC?߅||~oqhx$|Md)f"߸MozSQҢȲ6'.c{i{׸hOYs5Wo(SO*JlK>.򍠹Ų,㶍 ׵6r-"Ş{Yo|W<&ٷ$1$M?}PwPGAQ7H,ObS:Hu>k_ڢve$&$d.&nӞܙ?26<-׊uo(}/Ϣcܲw_N9`m A<0Aې|cXd]Gm ɏ"LİhlCi=7;Qx-IAEeNېiU9s O$Q)ĩ|;7RO `)թԴS_!/XbqL %8n A  0)̉|cbbӨ ^CIJ)AҌkJB`/%I{fb^?S/MmV^l} /,8#kM@{N Pjo,.&^}~vϭLB$ߨyA@ccC\зATN֐wwiVY6+w:Bg2 y/TYWtn-96Y7Kۣ'BเLTqbǻa5{|3}/Ҏ6Uk,Fi_mMI'T8Ѷo?F}_^n{4,R @XD!,PĀMt'?I oL:(ǐ1oB4O&U;3=ɊWC'Tm=dusZ7&0&:$wyr/62D02IsܜL'}/8/53;'-$tMɸ(⛓CR)IuEBr$_8멥ϥ®oER 0>7 Ra 8$4Lq9+?:As A}nN՞Bq"JpѦ֨?UzBA1%[I& \2|3o/ĕ?f6)UAM=;ǻa]+)a||V|3}<uw a {BR^-U@Ǘ/#0$ ~5߾G#|[*{b?R5'qy|cN T=S ~w%rO<(8u2+73oFw&:oloZkBf @s"ߨ@ .X]lW2~C~dSO-A{P87эVy8Դ Â6!et 9眳"$\rZx! 9٦6HI$r!MFr`s\G祺nT#i҉×"= iV`eL]xX 0Ya4jG%,.n}{Oo-kAʽα̯l! ˘8<*?ͯAC"6'wf7YD s"krC9YfD݋Tӕ; 6$Q*kfRs^qzN|I`ac}b ]!8l~ovO|\33ʵK͆o| i&n"+P+{Ao=HὈ2` ڡ?N+4?{[cQ!V6,TUeTY!# PWV.Di36vurIKfTJt,&(د0#Ƴ;|pCc;#ߑbA  7Hecr`Ey_^NZw)@Uj.$a'R]gCzM9&54 :Na02i#6PۆjoL LnS7r`s\Gnߙ4c#x#L+#;yr瘓ڈ:7,w`ohSv9w'R 2e]FG=`o$ dde3*w:`s2TdV5yff|Ͷ[85iǻa]+]>$cfxKju IDAT^\ߍ䳱C?Me-vA ,?<Mrݐo ,G8 A  b!0'`J5il.#`& ڴ fO.)aV#׎c,Acz5OIPIS f7T]Dj%KMiDRO+7]909C}R\7SԜ_m6|&`bժ|>JC`82̏x#FGmל]ը"NmdkoFNG4R;vgSf]]Y_jo녺jǨRB:V~_![tְyXU6B`qsO<|D\jyz?Ky b}oZ`9Am뭷.)c{GQ`ơ"}h[ ]kCtJڰr`w. }_^i%D`ǡIRoĝSKy ^w @S7rhڠFqb<-78S׹uf`m@{Cin4;CC"P(7AAڡ#htA<{P  lm|#tW܈8H HEGs㥺f~'H>&O-M*8kQUUJd==*f!s{}k'$ِoFG)oFE񐚚oRu#EL;fu-䗷b$cWo٥r(>W ls.wԫMV f){{פGlވjܠD϶nbm&8Xtxڧ?T|tj̠ۋmO7?-mǗo~RoHg+_Js{cQO3Jϗf91~ʸq-s1<MrݬIj=jsԞ8S, @7 D4R.׺ֵ&  ٦-oY~E1yLY M>H32@q u G:)A?,!MTS''Dvmoln|N9!ޥ|3nqoQM_w]%Ft|Vt%,.%v}zWO!0 w6ʏ> )I%e^gNeCtGNjʷc)*9T aݕKӜy晅X@nvEVEP}݋B8G=Qc+kX7Aoqh6y @A_¿ S@'}/ߔjr1C~+_ټ,6t2O}S >ReGl?RPLʇօ~UT*_\GPTfj5Eqh6VA  @XBYܗ=?޺]%k xQCKH:o G";.R_X-ԏ|#tWf~tX/,][MA @ƿs $ jv~Kʔ/}K.BխEUbɅ^X7"G% znUCARIM^{C38jaRmPPS)70_+37뭷^.2:P&G% Qu{]! #l6g]}ϑ~wm[T?H?((^~兤3RR.MoZ<2(QLcsG`ɹ|;n3>W @A|ӿ6Y-7AsW%[P'a&UE ~A`3}E-w\ƿNJ7߼;F`o~d~ttm7 2#8−U&D*4R5IT ! ntMGlB38csE;YgPg&,"3R6e|~G>B4/ReQQO3{6oP#Ϧ뮻 BjA.HFCxBKEMmSv 6(QA n\pACfXߕ4N>ҡgi\'g.ܯ@A C ~I޼px3B݂k[FKDXzf[qurLh˷癓r_JJ ֢+s-:Uj/H Qcvk>7>jwn>(ioN;ry2I?c/[B|wӝw]4O}]R=!H /~b-V!P׹=VZT u#w,r"!˼n770A`B7 7f?rG?Zc]|sk݃?o/Bi+ߜ~#;|V[5? ŝ$|o!wq%:K~Voc7닊̠[֒.a]vYsk]j׶|3 kxW Ls= ;3o~_6O|jwo[#Їqr}4m0s@A  |&}_EX'qHCS X_.V gGDZllRY,ȼnZ8ϝA/ 0d֛naIaXC"'?f_Ar- xEQsgJcs1+"R-z(xB9CJ9N>i"z|_n;B9 AF}% ʬz-oyB‘K/]|#E囟%׿rӶJ{*?qCZw*8aIE)␴SÔo6lv]i}٧N:R&)bsG/k}0xO@A  0!ߌ^Շ aE3So$ fb+#}s,zn %q,wrb!˼nZ7ϝ/E0 0dԖR&IG$-o~BA(a }o>jN% z(NOyS~Xoۢs޷g>RHvr.G>{?RW򕢬7P%,<oYiO{Z!* 4 붪G⌔WJ<=;7|)-,r(}'4A  @J _5Qд[k<1 Y:ַU=o9M1f!GjWZy޼ oW+yjNd|>OymEܯ{g??^vNw.:uA.*2<[ŅSH6ڭn,|-wԩ ^]ƦEbiڟnr4r?Mw mo{[om٦pэn]+?o=ldTX]mR~um{:{4n'>/6vYg<8s82~O|:S$_o~2UEҿArLh} }-sNsܥ5»rۼ^r%wY5_y1Toy[Jh%kTׅC_8LZ˞3U.DdRz׸n]'g5`}RdO~R/;SFڳYhg1&Ia{eH 6'%^j뮻[@k>5-u}y=co0~{6G|VBq}K_*)bz.'BJ@A  ,*3TRS8n$$/4 V6ss(b՟Xdկ~h5_LU6~W,S 0.3}}$_c`SPwA`hlߙp5oyXG\Eo^+y|KQ)7S?Z]ve%6XΩbIl> Y ;-½}9|'pBN;\pC/x ʜ).35Re)oT#y>l_^^ >`}e,FNXHmCS@xmuLY?_ה{1ң I@bA)bmqQ69fpqQG5~z3,h?2e t㐔}*qBZjԈ}! og˸qް97`3t~r@ @A  d, t&5 6-l^Q&gd8Ɉ:Y YC! 5X$O;As$w*X5'ƍ 5v2S= 4D24SVNSԓ Dɇ :`< <vmWN;C̑鸹r/.>.[SR8>Ȝ־6Vm;1OI"ze#y뭷.s:WQ@آ$gkY6 u6W^!m!ٴ}kџqkyFijP %'n}[ ِWo|j{w, k_m8\A`6=Y}ᅡeHmC\6[ !+x≅$+T8N;1oނiLgdmX TQ53Gpo~bۤqqv@yꡇZGQOloo c=v%Th3,`(h' m.cNdЧSZ0V(=Q*d'4T{H H+niQ`vi|5].$Ͼm %Z2׮}52H/3jd~qFֆ_kfF 8WeA.4&yT.s". LFFuS{ﲟ&// Lf w8* G}~ݿߕݦ>(C2MU}xraSV`6i~g,S c̛i5$`  {Q12p@!Dyuh;e'0)"HEny6D e!4U'њt J0XW7Y!iֳHъ̅dl +),hM! {!/v pV*SMażм aX-'̓跷խJ^KP#Ƶ6W;?[3W4k}5 = lYzꌀ㔳k57[|fDDG㽵ֺԴ~"AI~TPS}㰵rW`MaOA D 3 $d^x78YcOFwc,AH26k5ji̚aZMEET]f!GPڦ{'ŽRoC>bKK;۾;F3;܅?ڃv|7~󬑑vY~oPޏ'6Gd&i'{Dȕjb/^/ >N0 @A`\M9#J FWb֩L5cC!`$4tn s  U°\1ޕZ (|u3FjyN6Ս}ecBS6T2;8`CP#ds9wN|TvA,G~R˱>\3' 2dm$:ls3l֍aic_ۤm+2!}T0ӿl-6Tt\gؔ 4gC E_9*Iۤ\Y)Md ASbmjiþۤ}LR"k.aZж'e2Rs-s2ŜCɥi?} 0htL6ooy&B@;x8NpA ʶU(n65DŽw_PC0f(_9[_.E+9 "2 hi{A<47O+)YZ΋osjHpK 295<_w7Š ,}ik 05*RzTLk-5[||EjkdKDxVy!^z+?1|A44Mkg UtP!/ߑ!t>>> d}_X{u=U~ieʺ#(/k8*3[5>i7XZRkak,$$yݙ0i|E!=e3_)v*ԍiƞ_\C ^">/ !1XZsҾb?XC$b5qgǗi/|q IDAT@?}'`J@A  0.&8fư-r6mDmoeN!IcCxTgN4ض1mc٘;Uİ7x^ 1_?J'ԭ}bƋNScj3H -AUcv+-d&lP"K1a.E7z:ߨ$ 9lNP& =qg5䜨eE(hbSyGA[d/H)EfL`l`,gm3NU#Iioy-@3( kfj68&d~ނO6L'&'Ӕ1,}]D??ZJ aķ*TL'I=CPA}n T+sxo ~RLTT`T~5{ Y>sa[!Kqoߜ\@@LB!mT̡-kauWw'% >++y~Y3YD[wY`91[҆rڂk(5x5 U:׳q?WuDZcY[a/vYX;ty6ҁy?3#}ڙZ>aa]ఉ|A}H\s|D H}|~"0k* }13j>cqE*"c"Ztnuuh:_4> u̴OQ(k/kkMkLlL3=j>To-X]G# 5 TEcnk|g1s |{5clb/a5}/|@?F)aA  yo0BA^AKfNVYl .Ayx6NQ!l݆Z56ɐs&E>e*'& 9|Th4YzDr*ciwI;ęAhQ %CmƵY< |U]$o_AHK';B:A@I)`3*hw?Ì߭ft`8\/o'lluJlSo!#hti Aׇĩ w~6ީ-0\'4LS TSDOs$s,Dc[UX`˨^\p\*XSl ԛs 0 6Q4fHH@y 4=TN:i #њ#V_"T ]3R>ZoW PR\ 5 m&ț~)\Pk ӶQ?kkGs?s;>Ugs@ u`9@:l 5i e>>>+̻A1z=CnpaOwBpoboGXd !igSѭmu $D}uKŒ[ L"u4?~*՜a}w_U7+! |HIŬ|]}A*"!H6 鈬3›ߵS."fxocۨ3f\7llDuҵBNLߣJe݃HM֥.nWk.tg@9!`7%{5s8 o%!j!ǚ_[־oA Co0 @ӂ76!!XacD [Al0̱I#`㤜%ϱD ϰcزcsņ0@0y(d و06ƘMg6m 3 {6E٢~ x6X|8Pri8dH6j}_\|mܑ?6^6\΂6meaφ,_GxdMVjVvuÙo u/nƂl(d矞$v26'?(6ҟ~c3xX736)>y]=.XG68]k tcKùo~0{ +m7 rM ,@ZӅ\!0" S?jC1\|`GZ KW$lL 86܇7ԙpK]}}|g<˜wZPOwHߑUqq05}?ZUv!m}`>D}mRO /v l8pxϘ vJ MȬ7j`{X6TҳH.}C9y`JP8RQn%} : R0w3HƺX1_[oFݿg=o|E;9'_C&qHFʤISiD۬q+|O1ǯ">]C5 }e1#c!ٓ0_P/}7Є1IYYl]W48 &f7l>dӾazKaPlL4[AJm3{AZai v߶ەQ?փZ܈<;1xdE2cg!oi-V 1,22.uv^] vCWgk}bXz>^=@Im5a~Gmx6&jM\%Ͷ q0m[ʲ7a>{nw ڤ`y!(O߽Qf5/mDža0Ŵ.yvA  @ƦMU=&6)X:QDf5$=H A6ج 8iLcV0,/TT`-``G5>5Cf7-&X,f~5΂$sRX _s%E;2}ծLU9(sVs1@=̷Vzv}51}Ǿ+x;@k5`q0)M_v ǰZ#.̶΄L5qQ;d#`K]Xh,uYۧfa?:KUFܤ>w :8~؁衜۔Mz?9@uKIU!q18>rTaj`y]@txV|T6ߪEBC)/"CʷYy?";⾋[Du@67T|NrVH$Omm_qhʜ @A L&!Lf]/.^n]hX4}Z:..-z^~jw(ϬEf.FyGPq>{9|fsT_@*+J@0 gJ1ns @]OJ%sm)Qe)GH*VēQܒ0u*$T(!HHF?\1 }Qp W(*z0*^RSeKWF!i0ebD Wr4ʊQS)RgQ \LقUMDB鉪T7e;.`UnS~SӝeilEΈz1}EU3`Lu^|(*Sh>%MrY*550$a0PF/(PkC Bjjl|Ki_#Y[j;mbCBBPFYq| l/9]7'Y#w'>;?:]ǾUq!FݥyFA  "X% 䁋."?))R3?H@4MOHE$=)2L^>Ǩ *x)H,}|Y! yHYM2i d/$w i !P@]F`TADAؠBeBA^p2IyqA1ABjrj9Tr( P-Fu)+y{$56xFQґ!= ([yt)BOkS@I-'2'm0)뭷^2%.݇TäC%O" howHSg55"_-NPO CBawnq[Br Ú2LG.6B"2Ba<څ7ۨ#<O>mK9-v]V3D6\HA{>!LRĤQ5>&) |9*`٬uv>TŃ! zJ`T`HxcLGbXhQts?B||/3ږtH73XA D D E˷MWWFOߩiʉHFOF t. Aw}|Y VRC? ApVV". mEB*%H2G$y/Qv_?u ͳ6x~d D邤!BbJz"ift S QA7-mQA:SQׁU5 4f1K$FEr7mDvWjj!b#*}ꃨ6u.9f7/mmEa {;dk.dO msC|Ar=|-h/J;Lّ\đ\OU!'A%my~%L! t= M_AvBPc .ʳضTb#oؠ#1>R -3!! ei-Pci q?R}2IU:} ;F ymm-cG@Z,5.aϸ-|wٔM|3(!1?,k};2h'$XA B B~E˷D͔׬fWOu ߩjΉL囸Og@ع8^!e'7A7irvmWRm%ݔ`nuBe B ijTPW0u:^WA^$>e* /mo{[!10)tRT<(7"f(`&ej#pP H.c]v)j2L`ַX> ?7I8j(<я^l/{ G? ,0(X $X%d9uXrZ?@:Bh *Yl,Gut;d-5Ǚ%qQN.Ip0?<V{ e]0aBqC94ր?z@Uj$v 6ؠR9{lI;6hncZC#ݏr|׷uty筶vۮ0l8.塬a.Pui=X\mzpr!ݢ p|s<Ҏ?]}naCJ쵣\EԮYݜo?54ʭ s,Yg\#ߑY짴m.3Ͱc2\ߍhskݸqq5ZW%ktǛ[^:,k}]JG]UwMrݤQ D(0v|3vƲcOAotR ~: FaP 0C YA޾! G>}b(ܧ [$7<R%jBOpq<`# F5H;.x$2 @ ,Pe"p#TH >yF?$}$39,+)B7)%-H@Rr#JpMf⸒Jh MGuTo$x%LA4J8V7%g]?y8=j?-aNh?}C$#Qє-I ddL+ /yKM6٤$9, ܒ68tHa>XtE[W*clIs]j2hrsfn.cC'zt>ly HI,4a\ k2s\ @@eb[W,`QA]@̩ wo15ͮWP gv @]-o+cO{N)`%k0w:K+kP*uzGG ߓ\<v{wkZ{|_Y[5ǿ}Yqylo }ú >4]$@g IDATQ LOLOAo4b*y?=]3 3C9*;2o7*=[A޾ΆQ  z >ꪫyyHfJH&@! .2[CQ?uh=JԱ +GN|0:oـ`dϚig/e7J'7ѾNkDfwM:o;7rHL7@$F_yz7\8yfq{?mS4dЀW.zm)-`n] W{G9p}N:o)A n8tW{g1nG-ts=w0." D(ƭo/޾1>=ҽ@t>à@a1>}C>; } zF Q tT`חAo_-䫄&7) CTn \m$kQui(5&gCS@ d3^WuL( Т 9ͩ qK/䔵ۍ*qZj}k[wvr "h~WZ(q . @\fjІnXJ,jc֎`wuDfaA @vyRBߍ駟.k (2`Ձ@4jnu*>׀ !nF9Z蔈Q D(0=|3=՟_.}`rq@8ty(w ȠϿAoߨ4o} z:F(0p 2TTQ!J|H@J&@󟥼P"sk(MhYqP墸Zlk 0aB)oӔRhq Dϔf 0DyffmV>J8.,ͨN;nZiK [~rn j( lT;?J0d8pYO/% S(rpN1DR|믿~ٶ7; e8Ɓu6_V_}h2Qk| LCNXr I-F!ֱ[9d%@Q Lfw 9W79Z LSy%E)*.*( 53wo.j:[;qd'\,RcWҎp.Q6jw|5|c(Q R @OmO_ oqQi]w-*4<>vp` [mUnjm9眎r[`;K:.hic|vO%\:ëUVYeb@O4iqnwfϟrAh9樔]9Ӫ(@ ~#%Ɩ(_6yP# Ȥ]#@HcGU ꛣV`ߠ/kl+0o7gGzƶ zH@@֡̃i@ ߀`Q D(S`#iG4cKtZ(A3#v#nFV\#o[A޾̯ϿAo؞] 2۳#QY2J1qP3OO>pՠF~ԑIFRT7ǎ#@ߑ7GϿAo_V`ߠolώ. m}}ّE(@CJ%O(@Q`|*0ÿխǧc34S3T382 Фa#@CGV  T`ߠ/kl+0o7gGzƶ zH@@֡̃(@Q Di _}}ݧysVjw3M闽Gs(v[AMGVA޾= DT`חAoHM`(uh0!Q D(xP7:.Tx+}g<{Lq}{j„ 3C# j~9MG} z2ƶ>}c{vwQ`l+0ˠolώ. D d<Q D(@V Lox"T{:3 pyFx_~ysQ@~5CF(7bz*0o7=.y} zF~r(FJA__}#5.9n@֡$ D(@c]I=c=أi6۬ZhX`0jwWsOuYgUGydؠj_~FKçB/F@<>}| F1/޾1<5ҵ(Y2@Q D(/og- 7\]qK,Q]uUӪM9O Z՝wY:+\J8}='ƣxzF7&?>՝1/Q`*7e(ƅNH'@Q D(0NߌQ D(@Q D(@Q D(@Q D(@Q`|3Ue(@Q D(@Q D(@Q D(@Q D@<{Q D(@Q D(@Q D(@Q D(T)fQ D(@Q D(@Q D(@Q D(@Y7y(@Q D(@Q D(@Q D(@Q DR Tɗ@Q D(@Q D(@Q D(@Q D(ƳoQ D(@Q D(@Q D(@Q D(@S@/;G(@Q D(@Q D(@Q D(@Q gߌOߣ@Q D(@Q D(@Q D(@Q D(J7S%_vn+׿{ォ=K_RCUv4裏nꨣ7Wկt~o^-Czl&l2%/)sef1玿ՆnXx4e̹@Q D(@Q D(@Q D(@Q? LV7sOOW}k|93<㩅oU2K_m~ x7o/Қk9Nv|z[R4L#ҥ)ӡ4##|íPڗ}@Q D(@Q D(@Q D(@Q`x|3zuoT @cVN<ꩧ*.!iۮ~Vu>1m.T\pAv%e6_ojM78գ>Zx*Rl*'>Q+_jw>U/vۭ?AuM7UW_}u뮻V>`C=jd7tP9 {_zڴZkUvZE;E_]j|RK-UV{o"C;T[E/>U>x+}sՄ  hD?ڪڸOgix¹K.)c:gm_4/}K\9餓_-P~ӎ_{_jϯjEk}^sif:Cʿsa~O~j6yҎ}gnSU?m/6 D(@Q D(@Q D(@Q D(0 +[믿1qT묳N7'?Y]x,Ld]vYt7Q|B9sE]t)@2,SmRpr뭷V~zu}Ϋ/ >#k^ҞW_g>SqjF?;\.Zp[?\n7r˕(:M@B J_Z=]۹[W/~+ eW.,# ]w]9>Ph=}hf?_-ՙgYƫ9眓 X@S2o}[ 33 8_44ơS~V;S|1Y8 s饗w~86AhJ+5ʚ~D(@Q D(@Q D(@Q D(FoFXoXZϩ[PGKA)& <[7n.?ʴ|Y#P 跿mqcYxᅫ38@!± x衇 o}l(|s1& Ł4OIh Ǘ+k;)>{p1`P }7u]mV4*C3Mh7#X[K3vv?G0_*g:m-,B4$Q38oYgUp ƙV?o]$?Q D(@Q D(@Q {Y9:.O?~/k'z[Ϻ@}._|ny3vxKz׻RhJ­:ep//"rrJqo7ݿŎ;Xr0>Pէ?b܋סr'{;묳VkFo~ڼixW9,"c#O}k%?#~yiwe-fi|+Qm`ys{^uꩧVg}ܾ"xѺCS|p\م^x^A‹!D|90 (WR/yqqs Ȃ-0X l楜U;7m-}7J@f-xȄ.7Q\bio.zK٩s=2rf|MhӋsUWnӟT␣,ԏ'nCnlB~Qk=ܳ´ ߀q5~'9d.eqWEO}S3\JGq뮻z8;8we c0A6\tmݶ7~ "D(@Q D(@Q D(F޻0Y^xG?Zh7Ͻ+g3LܧW+?3~bJt6s!~9ÑWI3bs%*j?pq;A7 DeG}ܿկ.m__J/LQ`$zxLbB\4s}eYvJ6 f(J<5:֩8!̻aຒwnH]?C3M;L;řo$nE ߠ :n#H<5lM~W ,@57񍲝/1cN}6|ÝvAaT /9昣kjK.k;A63U@5/x"|oJWj,7na& mmlSE &MF=Ү&|_@RbФɍ7(y7a:081i3G5%M'̏~ &{>Q D(@Q D(@Q 綞,.,xѳ^i{VkwN+ML/P>/'nsyoKr:oN3J :M4nW8SOU}c=VrG'b׾5Da5׺e3~^ 7i{6lS֭UW]ac+''|v(k!\sMu i)2YdO,Y/|~T%cȋږ3/]1u)fĤBmEȗ/C/~ ~Co| q#Sk7o%+\y啓 rq#>7nĺ7_NkmX@Fn8XӍ&\q<7\p,R - F3a6|kljF? kG4Kv6|URsCd.\w}'i2n}5Ɠo1~\a(@Q D(@Q D(Uy睷v8Ĭ.曯$%cm/ی6|O!H ׌6|M30` sHsoH9(_;*4v5%HC{14p.Q* `%l fT(0aK D)5HcIQ;ei ߀ּ41F <@A@v8psKi 9=أ|7^(C%o 5j2\ sbn.úTu tQ3#[w~ia>*kQ'F_ .-Ⓦ ԩZ4vܬ?@L*4P5tc 4!s v-s.8X;}e@`7/X"GoF0AցQn<(@Q D(@Q D(@Ej"":L Cp8p'>Q1u{k7 Y\1Hfےj_'v5i\n (Cp7A3 ߀@Efec.l!'CC F68HD7G Okc4CB&\elxI3|?7p6sp1U$i` in U@,)vhomn6l/ <6<-ʄ)䜎 0" `6ɝѭ츀a~3׸*s'~?,wo 5u*󂩔|7$ K飲bJ0|.g Z3WA* ~ hAto@11 :T7{n5v]T!qmol:g\ml e>뉰&d6|Ctas";lV% |3pC o_bJ3Q D(@Q D(@Q D(ƓN 7Ē 26ukkFI >(Ƒ|=餓JA"@ .9I .xo$%{%^p]KJy.d0RRF9*m Ww޹ egz0aB%/ z{+ EY.7`nj@zjѥ|svo=yOap#1!{.z@St)pQ+jSo&HxZ{+e>z術oxPQJj5(mptk]{\sM5\su9/+c@P`^@1ݸ&ۈq./(mLZ!O=T6!\]@%"1u0OOk © Lf}v{N8NJPS5|״ʾtvNkavr YkID':>V_}ڥnc Xgf"^9A xwGoئgQ D(@Q D(@Q D(@(p"q.`ͲSlLH&` -J裏d?`zNK4s6mmP%$9p$-!F@phFE]T |s`Fhᾤ78pn8D7+b+s`ľM^P~FrJ+T/eE9p m z%6榾oN9唢 "eq(Ɯ1EvPgݵm׋[ ol6jXogr- mpc.3+Ќnc $]v@( z^No􇻊`= sϹhC'0ZSN]>KkU/ "y^7o3DNwd,Ho0:m;E3oo\c2h "Y} d-8c7cbӉ(@Q D(@Q D(@Q N-\\sH+O]2g<둾G(`=(˥ 7匀I矿LSpnvzeߌ}C viŎK4LŒ /}KwB۲or-8I8ƺb-ڶHs #Q,ږf{6k@-}FX?ٴGp#QHW㧮:?%!QڙQ D(@Q D(@Q D+fʴ{gJN2Q MN0tPo8p!QHpgQG kLƑswځ}^c;m@/d~ m>5,0bE-6j,YfbC'7_?ոA] KtԀ j`'ȾprBM{~m]1R/r7W)eg(@Q D(@Q D(xTছn*%_TIPƳvۭu]ǣT{goL" D(0^8K6ymvt; Ń7AݨN$YgcLK<pzTs8>?MަX;ݴڇO=T[ʹHȹ\rBmnVzs=wO47NG?Q$:rfbTҿՑn*xu@ 5@/jm[x{e\w dOjf6fMnԎ#8yHJ5+79oRVm馏/qmoJ׍ԋo90쳏5}Q D(@Q D(@Q D(@Q Do25\ZoJ9"@G{ R|fmُ&|g1P .j!*+ԗ?0J3ՆnX :H\zviꫯsj 6(^jX7 v_^O)-`Zϵ^;ytkB9B -TmfK/]گ 9Af0u٩&|ӭSq9+*&lR?P>\pAm _J}f4oH(@Q D(@Q D(@Q D(@cS7cs\GU)1\^7gqFq:N}{K=ru-Xc|_rb)! }٧VJݞzg.+ l>ռ[r駗p Y[9y;ǺK2ΡGKiπO+rqxr뭷x e|1:_.~=mӭ^zi5<cN7kMoG}98hW|h7CHD(@Q D(@Q D(.K~a/>=_ܛkt|{i)A^ceQ9&qDcCUvpTJ|HzE8emq>/[㩧Zrt {L3&:״jEfV?7.O}\*p!+_<`*廈n:ܧƈo@nR wNv%˝pJ۷cV=j5|A٩;,&̷ /5QCCn?ϔ+JQ(36s5W Hi/l}{]Ĺk7ԋ/$Rg\dn3ڸh74n h/>t܍C ~QLD(@Q D(@Q D(sUVYt뙸| Z$%_הd98}uq m3{-+?eZ/r-Li 7|yW~/ {-_25#tm'9]tQ[wo:-I,F9Nsq(:KM. Q D(@Q D(@Q LG_,/|;B/ :%Fnz[ K| 7,s׿^yUb ˢrs}ဳ;wrڕݹ=wfT, A-Єoz꩒; *s}}_<?餓i̙o|.+kkqYg\wznIp.-^9` dj}$ZdE5`ݮwȰt66 9PGsO/NAnjcX7f7@ X'&X/E})M2]S)ovjl"\_ nݼivf7w`iAw&`m-v p|f[7<ܰpq^mvMk/Jm}N7(R7Mn|!:4 =`wcI7Io/vS^ˑDQ D(@Q D(@Q`<+Lm@ mY?H~ϗ +dϛ{E7F%xkE].7sp@ ܁-u)//. W^y5@s?ca>Bhn@m,/#󷟶i8cELS1݈_?ctǗJns hzu\ӛ2{ hK_2\NHnRHխOrk6цoq}w=i|\^;F6z zM_P45m1Q/ܻT)1O)o\'< ۼjhgL]_ݢ׺k85T;#G[\n;: Xh-(X#|˗/|ts7R M0 t=fʗj)^ur䦏휺ot#nvmY>/Y7S_O>dt\$c2A >1o|uk7/_~)F7~f/*_0nLuk' ~r.7Pm}~1GXb2|ҧ|sͥcM؀BQ D(@Q D(@Q g$=}Hs$}3$=TC*`/Er3R*S @a=sp/mvS =4C+)zܷ-rf<^}m^'߀1kFY?I9aځ$P_}i!Nk밎Zw>sXI>nvݾCQ EZFoܜXQSӲ쒛/D_utaZ#Q D(@Q D(@Q ʠ\~kK;8trőL$m%(m'n8 By(. tG H p%E`fp??@v N& ~pu^ h˻A㮻*/;n `nN^f t ќeWuրD9hcr 84!UYDK^mF/]ߜ2)SCH{csQp\~Q#q3̩QI86۫ Q `8/&K\ QuCV6Gi7祹h^V]eq/Aum%i IDAT[cv+h`n nBN:1ל`NN4m0 %s,hFv(mizI@5c39nNߎ-ƠcÂs-Ѽ]Nf]0]c^kqmAk!75|vݾCt*7M8 g,ߌ9D(@Q D(@Q D(0r JKK+ySsrW.H4)کuI+ڥ7؀ GnE)\nBW{ũ RЅ/^|%G=ܳwri74FOe˲p8d(< =!l);t0?7Lj&҆o$̏=زOM ا >mt,첓L%# $!o\붷u?O=Ti'͜4W9qh俊`,~D{c@M^N6j U hs ߀hʽ:7x<#@^q6T$j\ij85{̿n}$5 @tnW\x+H'hg(Ռ^ԶU X3`o88.hFX8YG@v nqXk@;V jhLf^p7C1O{L{s(0pEfV_֍ v<@Ϛ#Ֆn:r(@Q D(@Q D(hTW229`%y$A0vZqh\A$SLiGQdIBRKK H{ U`Ts5%qpeX{ !<pP67y-9<%(RII>n=u8HsՇSV]uJX­ErӉD:.R?@4S.dػKٿ]Vk>;65Osn \rIqYЦcRc< NJÈ>T'曹! Q.c^7c[lcKƢyyWg@&|oۺ767HT=&q?qd 0M<:,uӔ7ѭOohC?s #zg.I9 ={kހ@!m-cNDA P EX?@T838'X ۶n7 /6ɥ9&fsEh>H?]7|prYSub$ܸ~CMז5+8oYkWLn-) (0k|ܘ+ok_fL-ugricik$tꤡ>-mkʼ^yO9~!^!='@~8M|3Mɢ@Q D(@Q D(@Q D(WI[P5\pIN 53uz%z܏c %UL\8pA=MqYf4"k Ͷl .\ 2%fq[;pID@~=&P?ŘEHК΢Miڵ8zn49馛5G99jvf(@Q D(@Q D(@é7W#8 rp/#S\'xbqƳ>[rOO\K8nVS{fg:蠒t[~wvQ`T*fT[=H onEHb5 Y,Nm a5F\KF)?c(K?6u87D>lAzv+G(@Q D(@Q D(@v2ࡔPFIQ D(T z4k2Kc]񨣎?!Rbrv(6alRVQ5[խdƲvnլ=X袋lnJ׽u5ݣ@Q D(@Q D(@Q D(@Q oFFu)0%wy睷ۊRK-U\q8kM7T=C^ xGMէzEg7sΙgY GuŒK.Ys饗V3O)$~T}{_\uU:S uo򗿔ϞRos5W;>dl97}GGc^{U>kO9Ҩ(@Q D(@Q D(@KK.g?[{Yr-}ݮz~{/x6m{/zuf#^"-gfxf~ꩧN"HaJ+/%/}KOӒÐmQq_dE /4˕cLg}JBKp@wXDhtW7oPڣ*^¶ t:GsIvS&vM~S[Vy>ж}ke]+7kcf*/Y:ꨉ/7ҏ^ֳ7Ţ a3]R ͘tfz(0% f+ͬew_P˖;_f|]|yD7kf^Ík^ŸRJ\tE ,TW^ye+NSFPZBw'7e(+y%9M քo^WU/˧9S x"VWVSFSD s=_򒻗OWzd굞5׽)|;-ko9XR`j+|"P:䬳Ϊ^{nqQ~SL7djS٠P}a?QVj.7Thie?auI'7r<`P'f„ ^@/Tn,ώ?}W=쳥~"%@Q D(@Q D(@Q`)pgT'xė5˜u]S/| wy˳#8~v^Wg-Xc ~w,7\pA7c qLEo.cTs9hXb%o~eMjWsQǁXjRGyd/T/2'\^"el.,t㡟7ۮuYoU@8x+^Q^w1O9[->9^9o<7x#svm)nQkiGsC_˵裏VkVui !w4jm s{ы^T:6qv+͓Kz'{Ϸ~[&|k^xag֋mcz$\oUmm嚲N&_2orzq238cyq|kKzv`ݣud45wx{X >zٿN #ߌNWGF7~pFmXPj;7(Q@Q D(@Q D(@Q`xǫ_%+<3.r]wMr~k|__K7 C=(Qуk77|s$6IxͲS A(i#Q߆o L #ǕW^Y@vrf4#HW}Y+I/{ 'p ~4| E8PP C܎n?o)so8t[ 8D_jF5%GjLa~Ɠ#޹97\xCs 7KnlZ4.4sNO@Ǥg ߀#9 z`$?3F`8sO52uN?M0Zl;߸|qmoצZ/k |_|qqnY{3_]B0/ # i@ʬqu1p\׉Dq@q4jQ D(@Q D(@Q D(sLnW_e$%Mƹ:72@ϵd3$%ȕRRBO, s}>?\!$_WU~xIX]*9M$}Wu $@!|C믿el/XG;sDzS6^{ (͔hþ;az}>s \܋@@p%2p+1gA3:(:cAYsL[f7`P{N\_s@Ot1Gnh_3u9[$t| {|ե]\T9؆o@~Juo[l.pN uo2391[n dINE`:d6nT" WߌבO@Q D(@Q D(@Q D(0bQ}4O]ܲ~Mv;I5>vЊBsyJ\fH&K7C$HBGO.q 827\E$A` :4 vn@m=6|SlO` dr0!D?5>;V IDAT8'tg]v)E7F(\O  ?܁$7 F@$ {Vpy;7` }mKqzkc\X,0 G{Q/|cn(v08quq~渀n:iucCGsGY%T][0f)㏯7che\Ys:>m ߸9眳@<,o4e饗.g \`McT8+{8] S^Hߘk=e2` -&M[o]~n\>vmWܫ^K(0^|3^G>Q D(@Q D(@Q D((U()(S2R@ x X ;p Cn@I`/*w_?hj].kj5N9C-ek`WS}?lo:S/}KCٽ󲗽۟7;hʼ~{.m}jM6نۮ:?Ղ .8q|Z`-ܲ:K?^T=Xu}u .^WUz׻/~Ռ3X :8mg?[m/_,i_Ww^kiZ ' J@Q D(@Q D(@Q D1.Z WGy|7/Ut'?I{W?я{g,7OuVe]vZ駟^VtP>ϏXzAofI`uW;1׿^gz77tS4^8h|gck::T>h^z_]XCU'owƚkYr-Fz_޵ oy 9RQ D(@Q D(@Q D(@ to$s1ũev+?Ϫwܱ8?>яV﫥Z,ғuJGYߪeY:䓫%\^mYh* [z__/)|kK;8H~.ozK^8J+rb1W{bӟt9?ޯ}k !Q\veܜ=%}A3+s9ֿ/e[zZkU}Hs'?8.2Qf|N;UW^yeejrhcO~[ڿf3z鷿mnX?yrcw饗=${Q?+_Jmra6_Z\r0n҇vء[\vmw^+5p` O;jYgM\zB50~wo$9ꗿe9]wݵ[.yc@*q}ݫo*Kϓxs7.vDw$\rIq1:4õsn`6ЗK51>9rW-Xqy[Z>UܤUW]~Z.*u|67#TFVXa[VE{='(sǜ}#֨ou#8b2:0F5Զ6wOsK}CEwa̻}ݷ{::lk'^kfsN7 ;߶G46_7׽+rxZvhsPOk r 7P8coOnr9^{]'c  ʘ.=ʵ/ Xh=1/5G`<멵3_8M7mi p 6(`#`krZZmJo|?3mz+_Y挱ZdZq6mѰ߶Z5o]5|noHJ(Ks9 pp#)o;pt3w+Ab{f+n@^eU뮻$9gHoGmkJb0>-h:C j99{U]W%ae*7x̒EVe*@ /dx/šxgXlCKρ1;gw?wcc:6Ѧ}AU(иM6>Qէ Af#(PcMmg׾Om9Lf+Uإ (YҼheёBfCUh'm*t\V g_> uNtXn6 99GgVc>|uZZOhoƽ XT ڢMEst2k=pbkxiSUS }к̾Q0rzޝBqz'}`?sGw/ߺ(|OV:)0S ZjH:*wݿ>{,~Vu(|X/|Buj[w]FШ3m/ | @ @ @+'(|sZ.ߴI݆sGuYZ 6mH|(xPE6; ݣ6_ ammnT*v, ߴۨo U)2l*Ӵ9\xPIh@@Z! Jzֵ5[U0\/S#uj&߂?=GjYYOuSԙg9Y}yҺ/.#Ym)S4 "4(f|h6~6Y 4mַ^ώju>k $\ozG (êRO:(S`cUtN}yj\ZӅr"Z=o! 7RKa X7{{[ YkMVf\Pb6,(|S{WL{Y*ZDžgsڳZYaBLpZl)|g f!4|gEWɧVᛪ6kSUFݳfĂb]#_%A Q~!~ߑwVȤ WYfW`wQ!Rӽ dRv'\fJ]Yfnq|Cc(|X/|S5~+ױo! |}̃Q @ @ @VJ`QM)R$Rf^_M]GT+_ʖK *T%M7m\kSBaB6*-~APâMUr tM 6@70Ϯ R:M*\QРg,0SUtDMϸ59[QՐl껍*tdi=<9:yUW}@.PZhs &T1KIAn>;i-66Y7fxԜ m>k8Z7U)1 @?*:G!Vd[+=z:`'>T1 )ZbUЙVf{kg*@U~Vf:q/UD*Rf2bS5BG_}.3M՘Z9~ 6MBe|`d>|S@̯Wz lRjUu*N.螵žwa-Z/]2/Hlvu\TIZ;M;4U.U\ڬքo2(ZE dܻ3CU͌u~kѻ(|ӿ%ݫڣ.5y=r @ @ @@W$|SUF*QhO2,|Q,itGD@mh@mʶ]6 ^ 6b|C|azNk1-mZɿUjGq(fu7U(Po b(ΰJ?kL7 }tM8Q!맍 Uv q1oGBB"E:(SEr)wGH#|ӦtAB$n ChoJ?Yok7_||hu]7ZU(sWţHy*,|Ӻ*xR*>3ͼ5]xug)ZU Kt$YrHދ^ɢMUդ_k Xo67#TӸӻ(՘Su.z }lMgȴz,,1 t$^ᰂh}ƪ8M*՜vi#@Uи ${w.3z|+xZzZKOV}O~}pfFF 1VY5]M;4Cz4 nrk7j7M^xUm8㌱rmٱ^^߇rѻ(|{Tu;m>( @ @ @+%pE7?R`A!6mr; ̎~RHmώ),ґ1\ Qަm;W&Mᢌ *UŠN7U-Zn6Şgk}gK_[ƽ nKsuм< /[ͮ7qꩧjNTEw]UZӭ5y1XnM4wQk%*e_?6|7;֍7|:nj>[ލ﵎ݛ?-BQG@f #!@ @ @ 04Oڬ$Uhø B]P \͂]tut \V{j_U_TGtiۏ3FB @ @ UqN:isUksTu\s+Qcvei]w޿vM׹u]N>鱏}1|2o+ @ @ @ @Y @ @ @ @KY" @ @ @ @VK@f @ @ @+_-oy|ӗiwܪm7 +^oo6seSO= }~hј9wtA]ѮsOyS=y[>/|a::kmn3/nrL?z<-N>Lyc|3//L{}t%LvtoxѢy?c2}ofʯn!|}m:F|C:7~:裧38cz[:/<o}[Mozq}{qo#>Og|;O;N&_G[m:L뿦~'r7 d?}j<l7< @ @ @*/PXooGn6/cU)q]m_QQGՐߘn|Oo{ۦ]wu3r71z#?wo.1O4tn*[[I'42sQSNs^򒗫bIDATL}c~ӻiO{#<S /xӟկ~uo4kUW)O~r=92o|׽u'<.#cot}{kT)U][p$E]%|߻w.23ݫqk>LO|*dG?z|O/| 婢SuL[>9 *lꙟk^[ڪ\U{otm|Wcz6.|s[j:ӆwaܭ|7qGN~.c TM7=h[ٷ~F^@fρ @ @ @VBꯎK-}k:~{N???17]9)QUBD bz(Pe&Zyֳ5?U)sm4 TE{>IO=qӇ>1B>UqW20CT W̎J*QHG&[E=ݿ@Ң1/7g2,lO37/NMKᎂAmc)T&?Z%|S'%ֳ7/?S?5(5]S׼f| /@x#F߿9~Wx~;li iQcب1}uXmss!BN9 y{;};kOo:"GGcm) z=~^HG囕\odf @ @ @+@*TYL׿GPJ2-F5Z>OdKo~z<]:יRN! U̩=qֆoJRHj"я~tT9Q-K}(TR>?O]w[ᛝwy5]z*UX@W~Z7.G?$@6٦nN @ @XBUͬT)0ѱB # oxQdM!}cm $T(hq;qO~tk]2*ZAT#nv]jB QK_Q=<Ȭ |PӸglPOA tTUuܢWe1ɡcjiThRֶB>RN7͆o t?-]W-S}jYߘ{6~ ̷~zP8nMPW6j*U{<87Srz5{ة~o.lۅv1 A @ @ )o<췾2(XRu^&}scNj/yKe[7M(|QS??0rUZme @`ٮ` @ @ @W]7U0y8x*ƩNYN;׾:r|EB tRU[ֆogOx;O>ygNt'NUi3|X/QEo}XT`Hᛋ/xU 7Ux0qӞ{9z#P}jxt駏qӢYooN9)a᥂*bN=ԩֶl>M?Ϝǯ(;E\9餓F5XU$z_>TZUoFUg~fumk7y[ |3y{王j\UAUlF7s׼fo NuVN?C?tdo 2< @ @ @UEpLG ]tEGru]RIac 5l&|Sp OxTݦ@n6=)O_})AUuy;9d)L߫Sώ83G0:U?a__{>Oj6M7m,Ul]٪ S+SHΛnzӛ-FcC9dկ>B!_UW=1?%U/Un77Uȣڶ^Q WbK՛7pM5׽F@S!9l׶͆om}WY:qǺmnwyqYF+*騯ڂ4YZTTEyaɪ @6J @ @ D.d:*hsGت3bkn^xs=w:ծ6va#=m*CUΩRSOwU~ h p̕ @ @ @,0~#P{ԣmX%k#<> U-eGsЮ@w tVxu13"mmbsM՟q{lm7'@m, |'  @ @ @VKFGudN;]IH @Y @ @ @ @VH@f&ۣ @ @ @ @,W@fz#@ @ @ @X!lJ @ @ @ \z @ @ @ @`oVh=* @ @ @ @ro7 @ @ @ @Y @ @ @ @Y @ @ @ @VH@f&ۣ @ @ @ @,W@fz#@ @ @ @X!lJ @ @ @ \z @ @ @ @`oVh=* @ @ @ @ro7 @ @ @ @Y @ @ @ @Y @ @ @ @VH@f&ۣ @ @ @ @,W@fz#@ @ @ @X!lJ @ @ @ \z @ @ @ @`oVh=* @ @ @ @ro7 @ @ @ @Y @ @ @ @Y @ @ @ @VH@f&ۣ @ @ @ @,W@fz#@ @ @ @X!lJ @ @ @ \z @ @ @ @`oVh=* @ @ @ @ro7 @ @ @ @Y @ @ @ @Y @ @ @ @VH@f&ۣ @ @ @ @,W@fz#@ @ @ @X!lJ @ @ @ \z @ @ @ @`oVh=* @ @ @ @ro7 @ @ @ @Y @ @ @ @Y @ @ @ @VH@f&ۣ @ @ @ @,W@fz#@ @ @ @X!lJ @ @ @ \z @ @ @ @`oVh=* @ @ @ @ro7 @ @ @ @?IENDB`ValveSoftware-gamescope-eb620ab/src/drm_include.h000066400000000000000000000052031502457270500221620ustar00rootroot00000000000000#pragma once #include #include #include #include #include "wlr_begin.hpp" #include #include #include "wlr_end.hpp" #include "hdmi.h" // Josh: Okay whatever, this header isn't // available for whatever stupid reason. :v //#include enum drm_color_encoding { DRM_COLOR_YCBCR_BT601, DRM_COLOR_YCBCR_BT709, DRM_COLOR_YCBCR_BT2020, DRM_COLOR_ENCODING_MAX, }; enum drm_color_range { DRM_COLOR_YCBCR_LIMITED_RANGE, DRM_COLOR_YCBCR_FULL_RANGE, DRM_COLOR_RANGE_MAX, }; enum amdgpu_transfer_function { AMDGPU_TRANSFER_FUNCTION_DEFAULT, AMDGPU_TRANSFER_FUNCTION_SRGB_EOTF, AMDGPU_TRANSFER_FUNCTION_BT709_INV_OETF, AMDGPU_TRANSFER_FUNCTION_PQ_EOTF, AMDGPU_TRANSFER_FUNCTION_IDENTITY, AMDGPU_TRANSFER_FUNCTION_GAMMA22_EOTF, AMDGPU_TRANSFER_FUNCTION_GAMMA24_EOTF, AMDGPU_TRANSFER_FUNCTION_GAMMA26_EOTF, AMDGPU_TRANSFER_FUNCTION_SRGB_INV_EOTF, AMDGPU_TRANSFER_FUNCTION_BT709_OETF, AMDGPU_TRANSFER_FUNCTION_PQ_INV_EOTF, AMDGPU_TRANSFER_FUNCTION_GAMMA22_INV_EOTF, AMDGPU_TRANSFER_FUNCTION_GAMMA24_INV_EOTF, AMDGPU_TRANSFER_FUNCTION_GAMMA26_INV_EOTF, AMDGPU_TRANSFER_FUNCTION_COUNT }; enum drm_panel_orientation { DRM_MODE_PANEL_ORIENTATION_UNKNOWN = -1, DRM_MODE_PANEL_ORIENTATION_NORMAL = 0, DRM_MODE_PANEL_ORIENTATION_BOTTOM_UP, DRM_MODE_PANEL_ORIENTATION_LEFT_UP, DRM_MODE_PANEL_ORIENTATION_RIGHT_UP, }; enum drm_colorspace { /* For Default case, driver will set the colorspace */ DRM_MODE_COLORIMETRY_DEFAULT = 0, /* CEA 861 Normal Colorimetry options */ DRM_MODE_COLORIMETRY_NO_DATA = 0, DRM_MODE_COLORIMETRY_SMPTE_170M_YCC = 1, DRM_MODE_COLORIMETRY_BT709_YCC = 2, /* CEA 861 Extended Colorimetry Options */ DRM_MODE_COLORIMETRY_XVYCC_601 = 3, DRM_MODE_COLORIMETRY_XVYCC_709 = 4, DRM_MODE_COLORIMETRY_SYCC_601 = 5, DRM_MODE_COLORIMETRY_OPYCC_601 = 6, DRM_MODE_COLORIMETRY_OPRGB = 7, DRM_MODE_COLORIMETRY_BT2020_CYCC = 8, DRM_MODE_COLORIMETRY_BT2020_RGB = 9, DRM_MODE_COLORIMETRY_BT2020_YCC = 10, /* Additional Colorimetry extension added as part of CTA 861.G */ DRM_MODE_COLORIMETRY_DCI_P3_RGB_D65 = 11, DRM_MODE_COLORIMETRY_DCI_P3_RGB_THEATER = 12, /* Additional Colorimetry Options added for DP 1.4a VSC Colorimetry Format */ DRM_MODE_COLORIMETRY_RGB_WIDE_FIXED = 13, DRM_MODE_COLORIMETRY_RGB_WIDE_FLOAT = 14, DRM_MODE_COLORIMETRY_BT601_YCC = 15, DRM_MODE_COLORIMETRY_COUNT }; /* Content type options */ #define DRM_MODE_CONTENT_TYPE_NO_DATA 0 #define DRM_MODE_CONTENT_TYPE_GRAPHICS 1 #define DRM_MODE_CONTENT_TYPE_PHOTO 2 #define DRM_MODE_CONTENT_TYPE_CINEMA 3 #define DRM_MODE_CONTENT_TYPE_GAME 4 ValveSoftware-gamescope-eb620ab/src/edid.cpp000066400000000000000000000344761502457270500211530ustar00rootroot00000000000000#include "edid.h" #include "backend.h" #include "log.hpp" #include "hdmi.h" #include #include #include extern "C" { #include "libdisplay-info/info.h" #include "libdisplay-info/edid.h" #include "libdisplay-info/cta.h" } static LogScope edid_log("edid"); namespace gamescope { static constexpr uint32_t EDID_MAX_BLOCK_COUNT = 256; static constexpr uint32_t EDID_BLOCK_SIZE = 128; static constexpr uint32_t EDID_MAX_STANDARD_TIMING_COUNT = 8; static constexpr uint32_t EDID_BYTE_DESCRIPTOR_COUNT = 4; static constexpr uint32_t EDID_BYTE_DESCRIPTOR_SIZE = 18; static constexpr uint32_t EDID_MAX_DESCRIPTOR_STANDARD_TIMING_COUNT = 6; static constexpr uint32_t EDID_MAX_DESCRIPTOR_COLOR_POINT_COUNT = 2; static constexpr uint32_t EDID_MAX_DESCRIPTOR_ESTABLISHED_TIMING_III_COUNT = 44; static constexpr uint32_t EDID_MAX_DESCRIPTOR_CVT_TIMING_CODES_COUNT = 4; static inline uint8_t get_bit_range(uint8_t val, size_t high, size_t low) { size_t n; uint8_t bitmask; assert(high <= 7 && high >= low); n = high - low + 1; bitmask = (uint8_t) ((1 << n) - 1); return (uint8_t) (val >> low) & bitmask; } static inline void set_bit_range(uint8_t *val, size_t high, size_t low, uint8_t bits) { size_t n; uint8_t bitmask; assert(high <= 7 && high >= low); n = high - low + 1; bitmask = (uint8_t) ((1 << n) - 1); assert((bits & ~bitmask) == 0); *val &= ~(bitmask << low); *val |= (uint8_t)(bits << low); } static inline void patch_edid_checksum(uint8_t* block) { uint8_t sum = 0; for (uint32_t i = 0; i < EDID_BLOCK_SIZE - 1; i++) sum += block[i]; uint8_t checksum = uint32_t(256) - uint32_t(sum); block[127] = checksum; } static bool validate_block_checksum(const uint8_t* data) { uint8_t sum = 0; size_t i; for (i = 0; i < EDID_BLOCK_SIZE; i++) { sum += data[i]; } return sum == 0; } static uint8_t encode_max_luminance(float nits) { if (nits == 0.0f) return 0; return ceilf((logf(nits / 50.0f) / logf(2.0f)) * 32.0f); } std::optional> PatchEdid( std::span pEdid, const BackendConnectorHDRInfo &hdrInfo, bool bRotate ) { // A zero length indicates that the edid parsing failed. if ( pEdid.empty() ) return std::nullopt; std::vector edid( pEdid.begin(), pEdid.end() ); if ( bRotate ) { // Patch width, height. edid_log.infof("Patching dims %ux%u -> %ux%u", edid[0x15], edid[0x16], edid[0x16], edid[0x15]); std::swap(edid[0x15], edid[0x16]); for (uint32_t i = 0; i < EDID_BYTE_DESCRIPTOR_COUNT; i++) { uint8_t *byte_desc_data = &edid[0x36 + i * EDID_BYTE_DESCRIPTOR_SIZE]; if (byte_desc_data[0] || byte_desc_data[1]) { uint32_t horiz = (get_bit_range(byte_desc_data[4], 7, 4) << 8) | byte_desc_data[2]; uint32_t vert = (get_bit_range(byte_desc_data[7], 7, 4) << 8) | byte_desc_data[5]; edid_log.infof("Patching res %ux%u -> %ux%u", horiz, vert, vert, horiz); std::swap(byte_desc_data[4], byte_desc_data[7]); std::swap(byte_desc_data[2], byte_desc_data[5]); break; } } patch_edid_checksum(&edid[0]); } // If we are debugging HDR support lazily on a regular Deck, // just hotpatch the edid for the game so we get values we want as if we had // an external display attached. // (Allows for debugging undocked fallback without undocking/redocking) if ( !hdrInfo.ShouldPatchEDID() ) return std::nullopt; // TODO: Allow for override of min luminance #if 0 float flMaxPeakLuminance = g_ColorMgmt.pending.hdrTonemapDisplayMetadata.BIsValid() ? g_ColorMgmt.pending.hdrTonemapDisplayMetadata.flWhitePointNits : g_ColorMgmt.pending.flInternalDisplayBrightness; #endif // TODO(JoshA): Need to resolve flInternalDisplayBrightness vs new connector hdrinfo mechanism. edid_log.infof("[edid] Patching HDR static metadata:\n" " - Max peak luminance = %u nits\n" " - Max frame average luminance = %u nits", hdrInfo.uMaxContentLightLevel, hdrInfo.uMaxFrameAverageLuminance ); const uint8_t new_hdr_static_metadata_block[] { (1 << HDMI_EOTF_SDR) | (1 << HDMI_EOTF_TRADITIONAL_HDR) | (1 << HDMI_EOTF_ST2084), /* supported eotfs */ 1, /* type 1 */ encode_max_luminance( float( hdrInfo.uMaxContentLightLevel ) ), /* desired content max peak luminance */ encode_max_luminance( float( hdrInfo.uMaxFrameAverageLuminance ) ), /* desired content max frame avg luminance */ 0, /* desired content min luminance -- 0 is technically "undefined" */ }; int ext_count = int(edid.size() / EDID_BLOCK_SIZE) - 1; assert(ext_count == edid[0x7E]); bool has_cta_block = false; bool has_hdr_metadata_block = false; for (int i = 0; i < ext_count; i++) { uint8_t *ext_data = &edid[EDID_BLOCK_SIZE + i * EDID_BLOCK_SIZE]; uint8_t tag = ext_data[0]; if (tag == DI_EDID_EXT_CEA) { has_cta_block = true; uint8_t dtd_start = ext_data[2]; uint8_t flags = ext_data[3]; if (dtd_start == 0) { edid_log.infof("Hmmmm.... dtd start is 0. Interesting... Not going further! :-("); continue; } if (flags != 0) { edid_log.infof("Hmmmm.... non-zero CTA flags. Interesting... Not going further! :-("); continue; } const int CTA_HEADER_SIZE = 4; int j = CTA_HEADER_SIZE; while (j < dtd_start) { uint8_t data_block_header = ext_data[j]; uint8_t data_block_tag = get_bit_range(data_block_header, 7, 5); uint8_t data_block_size = get_bit_range(data_block_header, 4, 0); if (j + 1 + data_block_size > dtd_start) { edid_log.infof("Hmmmm.... CTA malformatted. Interesting... Not going further! :-("); break; } uint8_t *data_block = &ext_data[j + 1]; if (data_block_tag == 7) // extended { uint8_t extended_tag = data_block[0]; uint8_t *extended_block = &data_block[1]; uint8_t extended_block_size = data_block_size - 1; if (extended_tag == 6) // hdr static { if (extended_block_size >= sizeof(new_hdr_static_metadata_block)) { edid_log.infof("Patching existing HDR Metadata with our own!"); memcpy(extended_block, new_hdr_static_metadata_block, sizeof(new_hdr_static_metadata_block)); has_hdr_metadata_block = true; } } } j += 1 + data_block_size; // account for header size. } if (!has_hdr_metadata_block) { const int hdr_metadata_block_size_plus_headers = sizeof(new_hdr_static_metadata_block) + 2; // +1 for header, +1 for extended header -> +2 edid_log.infof("No HDR metadata block to patch... Trying to insert one."); // Assert that the end of the data blocks == dtd_start if (dtd_start != j) { edid_log.infof("dtd_start != end of blocks. Giving up patching. I'm too scared to attempt it."); } // Move back the dtd to make way for our block at the end. uint8_t *dtd = &ext_data[dtd_start]; memmove(dtd + hdr_metadata_block_size_plus_headers, dtd, hdr_metadata_block_size_plus_headers); dtd_start += hdr_metadata_block_size_plus_headers; // Data block is where the dtd was. uint8_t *data_block = dtd; // header data_block[0] = 0; set_bit_range(&data_block[0], 7, 5, 7); // extended tag set_bit_range(&data_block[0], 4, 0, sizeof(new_hdr_static_metadata_block) + 1); // size (+1 for extended header, does not include normal header) // extended header data_block[1] = 6; // hdr metadata extended tag memcpy(&data_block[2], new_hdr_static_metadata_block, sizeof(new_hdr_static_metadata_block)); } patch_edid_checksum(ext_data); bool sum_valid = validate_block_checksum(ext_data); edid_log.infof("CTA Checksum valid? %s", sum_valid ? "Y" : "N"); } } if (!has_cta_block) { edid_log.infof("Couldn't patch for HDR metadata as we had no CTA block! Womp womp =c"); } bool sum_valid = validate_block_checksum(&edid[0]); edid_log.infof("BASE Checksum valid? %s", sum_valid ? "Y" : "N"); return edid; } const char *GetPatchedEdidPath() { const char *pszPatchedEdidPath = getenv( "GAMESCOPE_PATCHED_EDID_FILE" ); if ( !pszPatchedEdidPath || !*pszPatchedEdidPath ) return nullptr; return pszPatchedEdidPath; } void WritePatchedEdid( std::span pEdid, const BackendConnectorHDRInfo &hdrInfo, bool bRotate ) { const char *pszPatchedEdidPath = GetPatchedEdidPath(); if ( !pszPatchedEdidPath ) return; std::span pEdidToWrite = pEdid; auto oPatchedEdid = PatchEdid( pEdid, hdrInfo, bRotate ); if ( oPatchedEdid ) pEdidToWrite = std::span{ oPatchedEdid->begin(), oPatchedEdid->end() }; char szTmpFilename[PATH_MAX]; snprintf( szTmpFilename, sizeof( szTmpFilename ), "%s.tmp", pszPatchedEdidPath ); FILE *pFile = fopen( szTmpFilename, "wb" ); if ( !pFile ) { edid_log.errorf( "Couldn't open file: %s", szTmpFilename ); return; } fwrite( pEdidToWrite.data(), 1, pEdidToWrite.size(), pFile ); fflush( pFile ); fclose( pFile ); // Flip it over. rename( szTmpFilename, pszPatchedEdidPath ); edid_log.infof( "Wrote new edid to: %s", pszPatchedEdidPath ); } // From gamescope_base_edid.bin // Fake little thing we can patch. // It has one modeline of 90Hz, like Deck OLED. static constexpr uint8_t s_GamescopeBaseEdid[] = { 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x1a, 0x47, 0x69, 0x69, 0x38, 0xf4, 0x01, 0x00, 0xff, 0x20, 0x01, 0x04, 0xa5, 0x0a, 0x10, 0x78, 0x17, 0x3c, 0x71, 0xae, 0x51, 0x3c, 0xb9, 0x23, 0x0c, 0x50, 0x54, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x08, 0x34, 0x20, 0x48, 0x31, 0x00, 0x20, 0x50, 0x14, 0x08, 0x91, 0x40, 0x64, 0xa0, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x47, 0x61, 0x6d, 0x65, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x0a, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x2d, 0x5a, 0x76, 0x77, 0x0e, 0x00, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x03, 0x00, 0x00, 0xe6, 0x06, 0x01, 0x01, 0x6a, 0x6a, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36 }; std::vector GenerateSimpleEdid( uint32_t uWidth, uint32_t uHeight ) { uWidth = std::min( uWidth, 3840 ); uHeight = std::min( uHeight, 3840 ); // Does not patch refresh, nothing has cared about this yet. std::vector edid( s_GamescopeBaseEdid, s_GamescopeBaseEdid + std::size( s_GamescopeBaseEdid ) ); for (uint32_t i = 0; i < EDID_BYTE_DESCRIPTOR_COUNT; i++) { uint8_t *byte_desc_data = &edid[0x36 + i * EDID_BYTE_DESCRIPTOR_SIZE]; if (byte_desc_data[0] || byte_desc_data[1]) { uint32_t oldHoriz = (get_bit_range(byte_desc_data[4], 7, 4) << 8) | byte_desc_data[2]; uint32_t oldVert = (get_bit_range(byte_desc_data[7], 7, 4) << 8) | byte_desc_data[5]; set_bit_range( &byte_desc_data[4], 7, 4, ( uWidth >> 8 ) & 0xff ); byte_desc_data[2] = uWidth & 0xff; set_bit_range( &byte_desc_data[7], 7, 4, ( uHeight >> 8 ) & 0xff ); byte_desc_data[5] = uHeight & 0xff; uint32_t horiz = (get_bit_range(byte_desc_data[4], 7, 4) << 8) | byte_desc_data[2]; uint32_t vert = (get_bit_range(byte_desc_data[7], 7, 4) << 8) | byte_desc_data[5]; edid_log.infof( "Patching res %ux%u -> %ux%u", oldHoriz, oldVert, horiz, vert ); break; } } patch_edid_checksum(&edid[0]); return edid; } } ValveSoftware-gamescope-eb620ab/src/edid.h000066400000000000000000000010131502457270500205750ustar00rootroot00000000000000#pragma once #include #include #include #include namespace gamescope { struct BackendConnectorHDRInfo; const char *GetPatchedEdidPath(); void WritePatchedEdid( std::span pEdid, const BackendConnectorHDRInfo &hdrInfo, bool bRotate ); std::vector GenerateSimpleEdid( uint32_t uWidth, uint32_t uHeight ); std::optional> PatchEdid( std::span pEdid, const BackendConnectorHDRInfo &hdrInfo, bool bRotate ); }ValveSoftware-gamescope-eb620ab/src/gamescope_base_edid.bin000066400000000000000000000004001502457270500241320ustar00rootroot00000000000000Gii8  x namespace gamescope { class BackendBlob; enum GamescopeModeGeneration { GAMESCOPE_MODE_GENERATE_CVT, GAMESCOPE_MODE_GENERATE_FIXED, }; enum GamescopeScreenType { GAMESCOPE_SCREEN_TYPE_INTERNAL, GAMESCOPE_SCREEN_TYPE_EXTERNAL, GAMESCOPE_SCREEN_TYPE_COUNT }; } enum GamescopeAppTextureColorspace { GAMESCOPE_APP_TEXTURE_COLORSPACE_LINEAR = 0, GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB, GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB, GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ, GAMESCOPE_APP_TEXTURE_COLORSPACE_PASSTHRU, }; const uint32_t GamescopeAppTextureColorspace_Count = 5; const uint32_t GamescopeAppTextureColorspace_Bits = 3; inline bool ColorspaceIsHDR( GamescopeAppTextureColorspace colorspace ) { return colorspace == GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB || colorspace == GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ; } enum GamescopeSelection { GAMESCOPE_SELECTION_CLIPBOARD, GAMESCOPE_SELECTION_PRIMARY, GAMESCOPE_SELECTION_COUNT, }; enum GamescopePanelOrientation { GAMESCOPE_PANEL_ORIENTATION_0, // normal GAMESCOPE_PANEL_ORIENTATION_270, // right GAMESCOPE_PANEL_ORIENTATION_90, // left GAMESCOPE_PANEL_ORIENTATION_180, // upside down GAMESCOPE_PANEL_ORIENTATION_AUTO, }; // Disable partial composition for now until we get // composite priorities working in libliftoff + also // use the proper libliftoff composite plane system. static constexpr bool kDisablePartialComposition = true; ValveSoftware-gamescope-eb620ab/src/gpuvis_trace_utils.h000066400000000000000000000550011502457270500236110ustar00rootroot00000000000000////////////////////////////////////////////////////////////////////////////// // gpuvis_trace_utils.h - v0.10 - public domain // no warranty is offered or implied; use this code at your own risk // // This is a single header file with useful utilities for gpuvis linux tracing // // ============================================================================ // You MUST define GPUVIS_TRACE_IMPLEMENTATION in EXACTLY _one_ C or C++ file // that includes this header, BEFORE the include, like this: // // #define GPUVIS_TRACE_IMPLEMENTATION // #include "gpuvis_trace_utils.h" // // All other files should just #include "gpuvis_trace_utils.h" w/o the #define. // ============================================================================ // // Credits // // Michael Sartain // // LICENSE // // This software is dual-licensed to the public domain and under the following // license: you are granted a perpetual, irrevocable license to copy, modify, // publish, and distribute this file as you see fit. ////////////////////////////////////////////////////////////////////////////// // // INCLUDE SECTION // #ifndef _GPUVIS_TRACE_UTILS_H_ #define _GPUVIS_TRACE_UTILS_H_ #include #if !defined( __linux__ ) #define GPUVIS_TRACE_UTILS_DISABLE #endif #if defined( __clang__ ) || defined( __GNUC__ ) // printf-style warnings for user functions. #define GPUVIS_ATTR_PRINTF( _x, _y ) __attribute__( ( __format__( __printf__, _x, _y ) ) ) #define GPUVIS_MAY_BE_UNUSED __attribute__( ( unused ) ) #define GPUVIS_CLEANUP_FUNC( x ) __attribute__( ( __cleanup__( x ) ) ) #else #define GPUVIS_ATTR_PRINTF( _x, _y ) #define GPUVIS_MAY_BE_UNUSED #define GPUVIS_CLEANUP_FUNC( x ) #endif #if !defined( GPUVIS_TRACE_UTILS_DISABLE ) #include #include #include #ifdef __cplusplus #define GPUVIS_EXTERN extern "C" #if __cplusplus>=201103L #define THREAD_LOCAL thread_local #else #define THREAD_LOCAL __thread #endif #else #define GPUVIS_EXTERN extern #endif // From kernel/trace/trace.h #ifndef TRACE_BUF_SIZE #define TRACE_BUF_SIZE 1024 #endif // Try to open tracefs trace_marker file for writing. Returns -1 on error. GPUVIS_EXTERN int gpuvis_trace_init( void ); // Close tracefs trace_marker file. GPUVIS_EXTERN void gpuvis_trace_shutdown( void ); // Write user event to tracefs trace_marker. GPUVIS_EXTERN int gpuvis_trace_printf( const char *fmt, ... ) GPUVIS_ATTR_PRINTF( 1, 2 ); GPUVIS_EXTERN int gpuvis_trace_vprintf( const char *fmt, va_list ap ) GPUVIS_ATTR_PRINTF( 1, 0 ); // Write user event (with duration=XXms) to tracefs trace_marker. GPUVIS_EXTERN int gpuvis_trace_duration_printf( float duration, const char *fmt, ... ) GPUVIS_ATTR_PRINTF( 2, 3 ); GPUVIS_EXTERN int gpuvis_trace_duration_vprintf( float duration, const char *fmt, va_list ap ) GPUVIS_ATTR_PRINTF( 2, 0 ); // Write user event (with begin_ctx=XX) to tracefs trace_marker. GPUVIS_EXTERN int gpuvis_trace_begin_ctx_printf( unsigned int ctx, const char *fmt, ... ) GPUVIS_ATTR_PRINTF( 2, 3 ); GPUVIS_EXTERN int gpuvis_trace_begin_ctx_vprintf( unsigned int ctx, const char *fmt, va_list ap ) GPUVIS_ATTR_PRINTF( 2, 0 ); // Write user event (with end_ctx=XX) to tracefs trace_marker. GPUVIS_EXTERN int gpuvis_trace_end_ctx_printf( unsigned int ctx, const char *fmt, ... ) GPUVIS_ATTR_PRINTF( 2, 3 ); GPUVIS_EXTERN int gpuvis_trace_end_ctx_vprintf( unsigned int ctx, const char *fmt, va_list ap ) GPUVIS_ATTR_PRINTF( 2, 0 ); // Execute "trace-cmd start -b 2000 -D -i -e sched:sched_switch -e ..." GPUVIS_EXTERN int gpuvis_start_tracing( unsigned int kbuffersize ); // Execute "trace-cmd extract" GPUVIS_EXTERN int gpuvis_trigger_capture_and_keep_tracing( char *filename, size_t size ); // Execute "trace-cmd reset" GPUVIS_EXTERN int gpuvis_stop_tracing( void ); // -1: tracing not setup, 0: tracing disabled, 1: tracing enabled. GPUVIS_EXTERN int gpuvis_tracing_on( void ); // Get tracefs directory. Ie: /sys/kernel/tracing. Returns "" on error. GPUVIS_EXTERN const char *gpuvis_get_tracefs_dir( void ); // Get tracefs file path in buf. Ie: /sys/kernel/tracing/trace_marker. Returns NULL on error. GPUVIS_EXTERN const char *gpuvis_get_tracefs_filename( char *buf, size_t buflen, const char *file ); // Internal function used by GPUVIS_COUNT_HOT_FUNC_CALLS macro GPUVIS_EXTERN void gpuvis_count_hot_func_calls_internal_( const char *func ); struct GpuvisTraceBlock; static inline void gpuvis_trace_block_begin( struct GpuvisTraceBlock *block, const char *str ); static inline void gpuvis_trace_block_end( struct GpuvisTraceBlock *block ); struct GpuvisTraceBlockf; static inline void gpuvis_trace_blockf_vbegin( struct GpuvisTraceBlockf *block, const char *fmt, va_list ap ); static inline void gpuvis_trace_blockf_begin( struct GpuvisTraceBlockf *block, const char *fmt, ... ) GPUVIS_ATTR_PRINTF( 2, 3 ); static inline void gpuvis_trace_blockf_end( struct GpuvisTraceBlockf *block ); #define LNAME3( _name, _line ) _name ## _line #define LNAME2( _name, _line ) LNAME3( _name, _line ) #define LNAME( _name ) LNAME2( _name, __LINE__ ) struct GpuvisTraceBlock { uint64_t m_t0; const char *m_str; #ifdef __cplusplus GpuvisTraceBlock( const char *str ) { gpuvis_trace_block_begin( this, str ); } ~GpuvisTraceBlock() { gpuvis_trace_block_end( this ); } #endif }; struct GpuvisTraceBlockf { uint64_t m_t0; char m_buf[ TRACE_BUF_SIZE ]; #ifdef __cplusplus GpuvisTraceBlockf( const char *fmt, ... ) GPUVIS_ATTR_PRINTF( 2, 3 ) { va_list args; va_start( args, fmt ); gpuvis_trace_blockf_vbegin( this, fmt, args ); va_end( args ); } ~GpuvisTraceBlockf() { gpuvis_trace_blockf_end( this ); } #endif }; #ifdef __cplusplus #define GPUVIS_TRACE_BLOCK( _conststr ) GpuvisTraceBlock LNAME( gpuvistimeblock )( _conststr ) #define GPUVIS_TRACE_BLOCKF( _fmt, ... ) GpuvisTraceBlockf LNAME( gpuvistimeblock )( _fmt, __VA_ARGS__ ) #else #if defined( __clang__ ) || defined( __GNUC__ ) #define GPUVIS_TRACE_BLOCKF_INIT( _unique, _fmt, ... ) \ ({ \ struct GpuvisTraceBlockf _unique; \ gpuvis_trace_blockf_begin( & _unique, _fmt, __VA_ARGS__ ); \ _unique; \ }) #define GPUVIS_TRACE_BLOCKF( _fmt, ...) \ GPUVIS_CLEANUP_FUNC( gpuvis_trace_blockf_end ) GPUVIS_MAY_BE_UNUSED struct GpuvisTraceBlockf LNAME( gpuvistimeblock ) = \ GPUVIS_TRACE_BLOCKF_INIT( LNAME( gpuvistimeblock_init ), _fmt, __VA_ARGS__ ) #define GPUVIS_TRACE_BLOCK( _conststr ) \ GPUVIS_CLEANUP_FUNC( gpuvis_trace_block_end ) GPUVIS_MAY_BE_UNUSED struct GpuvisTraceBlock LNAME( gpuvistimeblock ) = \ {\ .m_t0 = gpuvis_gettime_u64(), \ .m_str = _conststr \ } #else #define GPUVIS_TRACE_BLOCKF( _fmt, ... ) #define GPUVIS_TRACE_BLOCK( _conststr ) #endif // __clang__ || __GNUC__ #endif // __cplusplus static inline uint64_t gpuvis_gettime_u64( void ) { struct timespec ts; clock_gettime( CLOCK_MONOTONIC, &ts ); return ( ( uint64_t )ts.tv_sec * 1000000000LL) + ts.tv_nsec; } static inline void gpuvis_trace_block_finalize( uint64_t m_t0, const char *str ) { uint64_t dt = gpuvis_gettime_u64() - m_t0; // The cpu clock_gettime() functions seems to vary compared to the // ftrace event timestamps. If we don't reduce the duration here, // scopes oftentimes won't stack correctly when they're drawn. if ( dt > 11000 ) dt -= 11000; gpuvis_trace_printf( "%s (lduration=-%lu)", str, dt ); } static inline void gpuvis_trace_block_begin( struct GpuvisTraceBlock* block, const char *str ) { block->m_str = str; block->m_t0 = gpuvis_gettime_u64(); } static inline void gpuvis_trace_block_end( struct GpuvisTraceBlock *block ) { gpuvis_trace_block_finalize(block->m_t0, block->m_str); } static inline void gpuvis_trace_blockf_vbegin( struct GpuvisTraceBlockf *block, const char *fmt, va_list ap) { vsnprintf(block->m_buf, sizeof(block->m_buf), fmt, ap); block->m_t0 = gpuvis_gettime_u64(); } static inline void gpuvis_trace_blockf_begin( struct GpuvisTraceBlockf *block, const char *fmt, ... ) { va_list args; va_start( args, fmt ); gpuvis_trace_blockf_vbegin( block, fmt, args ); va_end( args ); } static inline void gpuvis_trace_blockf_end( struct GpuvisTraceBlockf *block ) { gpuvis_trace_block_finalize( block->m_t0, block->m_buf ); } #define GPUVIS_COUNT_HOT_FUNC_CALLS() gpuvis_count_hot_func_calls_internal_( __func__ ); #else static inline int gpuvis_trace_init() { return -1; } static inline void gpuvis_trace_shutdown() {} static inline int gpuvis_trace_printf( const char *fmt, ... ) { return 0; } static inline int gpuvis_trace_vprintf( const char *fmt, va_list ap ) { return 0; } static inline int gpuvis_trace_duration_printf( float duration, const char *fmt, ... ) { return 0; } static inline int gpuvis_trace_duration_vprintf( float duration, const char *fmt, va_list ap ) { return 0; } static inline int gpuvis_trace_begin_ctx_printf( unsigned int ctx, const char *fmt, ... ) { return 0; } static inline int gpuvis_trace_begin_ctx_vprintf( unsigned int ctx, const char *fmt, va_list ap ) { return 0; } static inline int gpuvis_trace_end_ctx_printf( unsigned int ctx, const char *fmt, ... ) { return 0; } static inline int gpuvis_trace_end_ctx_vprintf( unsigned int ctx, const char *fmt, va_list ap ) { return 0; } static inline int gpuvis_start_tracing( unsigned int kbuffersize ) { return 0; } static inline int gpuvis_trigger_capture_and_keep_tracing( char *filename, size_t size ) { return 0; } static inline int gpuvis_stop_tracing() { return 0; } static inline int gpuvis_tracing_on() { return -1; } static inline const char *gpuvis_get_tracefs_dir() { return ""; } static inline const char *gpuvis_get_tracefs_filename( char *buf, size_t buflen, const char *file ) { return NULL; } struct GpuvisTraceBlock; static inline void gpuvis_trace_block_begin( struct GpuvisTraceBlock *block, const char *str ) {} static inline void gpuvis_trace_block_end( struct GpuvisTraceBlock *block ) {} struct GpuvisTraceBlockf; static inline void gpuvis_trace_blockf_vbegin( struct GpuvisTraceBlockf *block, const char *fmt, va_list ap ) {} static inline void gpuvis_trace_blockf_begin( struct GpuvisTraceBlockf *block, const char *fmt, ... ) {} static inline void gpuvis_trace_blockf_end( struct GpuvisTraceBlockf *block ) {} #define GPUVIS_TRACE_BLOCK( _conststr ) #define GPUVIS_TRACE_BLOCKF( _fmt, ... ) #define GPUVIS_COUNT_HOT_FUNC_CALLS() #endif // !GPUVIS_TRACE_UTILS_DISABLE #if defined( GPUVIS_TRACE_IMPLEMENTATION ) && !defined( GPUVIS_TRACE_UTILS_DISABLE ) ////////////////////////////////////////////////////////////////////////////// // // IMPLEMENTATION SECTION // #define _GNU_SOURCE 1 #include #include #include #include #include #include #include #include #include #undef GPUVIS_EXTERN #ifdef __cplusplus #define GPUVIS_EXTERN extern "C" #else #define GPUVIS_EXTERN #endif #ifndef TRACEFS_MAGIC #define TRACEFS_MAGIC 0x74726163 #endif #define GPUVIS_STR( x ) #x #define GPUVIS_STR_VALUE( x ) GPUVIS_STR( x ) static int g_trace_fd = -2; static int g_tracefs_dir_inited = 0; static char g_tracefs_dir[ PATH_MAX ]; #ifdef __cplusplus #include struct funcinfo_t { uint64_t tfirst = 0; uint64_t tlast = 0; uint32_t count = 0; }; static std::unordered_map< pid_t, std::unordered_map< const char *, funcinfo_t > > g_hotfuncs; #endif // __cplusplus static pid_t gpuvis_gettid() { return ( pid_t )syscall( SYS_gettid ); } static int exec_tracecmd( const char *cmd ) { int ret; FILE *fh = popen( cmd, "r" ); if ( !fh ) { //$ TODO: popen() failed: errno ret = -1; } else { char buf[ 8192 ]; while ( fgets( buf, sizeof( buf ), fh ) ) { //$ TODO printf( "%s: %s", __func__, buf ); } if ( feof( fh ) ) { int pclose_ret = pclose( fh ); ret = WEXITSTATUS( pclose_ret ); } else { //$ TODO: Failed to read pipe to end: errno pclose( fh ); ret = -1; } } return ret; } GPUVIS_EXTERN int gpuvis_trace_init() { if ( g_trace_fd == -2 ) { char filename[ PATH_MAX ]; // The "trace_marker" file allows userspace to write into the ftrace buffer. if ( !gpuvis_get_tracefs_filename( filename, sizeof( filename ), "trace_marker" ) ) g_trace_fd = -1; else g_trace_fd = open( filename, O_WRONLY ); } return g_trace_fd; } #if !defined( __cplusplus ) static void flush_hot_func_calls() { //$ TODO: hot func calls for C } #else static void flush_hot_func_calls() { if ( g_hotfuncs.empty() ) return; uint64_t t0 = gpuvis_gettime_u64(); for ( auto &x : g_hotfuncs ) { for ( auto &y : x.second ) { if ( y.second.count ) { pid_t tid = x.first; const char *func = y.first; uint64_t offset = t0 - y.second.tfirst; uint64_t duration = y.second.tlast - y.second.tfirst; gpuvis_trace_printf( "%s calls:%u (lduration=%lu tid=%d offset=-%lu)\n", func, y.second.count, duration, tid, offset ); } } } g_hotfuncs.clear(); } GPUVIS_EXTERN void gpuvis_count_hot_func_calls_internal_( const char *func ) { static THREAD_LOCAL pid_t s_tid = gpuvis_gettid(); uint64_t t0 = gpuvis_gettime_u64(); auto &x = g_hotfuncs[ s_tid ]; auto &y = x[ func ]; if ( !y.count ) { y.count = 1; y.tfirst = t0; y.tlast = t0 + 1; } else if ( t0 - y.tlast >= 3 * 1000000 ) // 3ms { gpuvis_trace_printf( "%s calls:%u (lduration=%lu offset=-%lu)\n", func, y.count, y.tlast - y.tfirst, t0 - y.tfirst ); y.count = 1; y.tfirst = t0; y.tlast = t0 + 1; } else { y.tlast = t0; y.count++; } } #endif // __cplusplus GPUVIS_EXTERN void gpuvis_trace_shutdown() { flush_hot_func_calls(); if ( g_trace_fd >= 0 ) close( g_trace_fd ); g_trace_fd = -2; g_tracefs_dir_inited = 0; g_tracefs_dir[ 0 ] = 0; } static int trace_printf_impl( const char *keystr, const char *fmt, va_list ap ) GPUVIS_ATTR_PRINTF( 2, 0 ); static int trace_printf_impl( const char *keystr, const char *fmt, va_list ap ) { int ret = -1; if ( gpuvis_trace_init() >= 0 ) { int n; char buf[ TRACE_BUF_SIZE ]; n = vsnprintf( buf, sizeof( buf ), fmt, ap ); if ( ( n > 0 ) || ( !n && keystr ) ) { if ( ( size_t )n >= sizeof( buf ) ) n = sizeof( buf ) - 1; if ( keystr && keystr[ 0 ] ) { int keystrlen = strlen( keystr ); if ( ( size_t )n + keystrlen >= sizeof( buf ) ) n = sizeof( buf ) - keystrlen - 1; strcpy( buf + n, keystr ); n += keystrlen; } ret = write( g_trace_fd, buf, n ); } } return ret; } GPUVIS_EXTERN int gpuvis_trace_printf( const char *fmt, ... ) { int ret; va_list ap; va_start( ap, fmt ); ret = gpuvis_trace_vprintf( fmt, ap ); va_end( ap ); return ret; } GPUVIS_EXTERN int gpuvis_trace_vprintf( const char *fmt, va_list ap ) { return trace_printf_impl( NULL, fmt, ap ); } GPUVIS_EXTERN int gpuvis_trace_duration_printf( float duration, const char *fmt, ... ) { int ret; va_list ap; va_start( ap, fmt ); ret = gpuvis_trace_duration_vprintf( duration, fmt, ap ); va_end( ap ); return ret; } GPUVIS_EXTERN int gpuvis_trace_duration_vprintf( float duration, const char *fmt, va_list ap ) { char keystr[ 128 ]; snprintf( keystr, sizeof( keystr ), " (duration=%f)", duration ); //$ TODO: Try this with more precision? return trace_printf_impl( keystr, fmt, ap ); } GPUVIS_EXTERN int gpuvis_trace_begin_ctx_printf( unsigned int ctx, const char *fmt, ... ) { int ret; va_list ap; va_start( ap, fmt ); ret = gpuvis_trace_begin_ctx_vprintf( ctx, fmt, ap ); va_end( ap ); return ret; } GPUVIS_EXTERN int gpuvis_trace_begin_ctx_vprintf( unsigned int ctx, const char *fmt, va_list ap ) { char keystr[ 128 ]; snprintf( keystr, sizeof( keystr ), " (begin_ctx=%u)", ctx ); return trace_printf_impl( keystr, fmt, ap ); } GPUVIS_EXTERN int gpuvis_trace_end_ctx_printf( unsigned int ctx, const char *fmt, ... ) { int ret; va_list ap; va_start( ap, fmt ); ret = gpuvis_trace_end_ctx_vprintf( ctx, fmt, ap ); va_end( ap ); return ret; } GPUVIS_EXTERN int gpuvis_trace_end_ctx_vprintf( unsigned int ctx, const char *fmt, va_list ap ) { char keystr[ 128 ]; snprintf( keystr, sizeof( keystr ), " (end_ctx=%u)", ctx ); return trace_printf_impl( keystr, fmt, ap ); } GPUVIS_EXTERN int gpuvis_start_tracing( unsigned int kbuffersize ) { static const char fmt[] = "trace-cmd start -b %u -D -i " // https://github.com/mikesart/gpuvis/wiki/TechDocs-Linux-Scheduler " -e sched:sched_switch" " -e sched:sched_process_fork" " -e sched:sched_process_exec" " -e sched:sched_process_exit" " -e drm:drm_vblank_event" " -e drm:drm_vblank_event_queued" " -e drm:drm_vblank_event_delivered" // https://github.com/mikesart/gpuvis/wiki/TechDocs-AMDGpu " -e amdgpu:amdgpu_vm_flush" " -e amdgpu:amdgpu_cs_ioctl" " -e amdgpu:amdgpu_sched_run_job" " -e *fence:*fence_signaled" // https://github.com/mikesart/gpuvis/wiki/TechDocs-Intel " -e i915:i915_flip_request" " -e i915:i915_flip_complete" " -e i915:intel_gpu_freq_change" " -e i915:i915_gem_request_add" " -e i915:i915_gem_request_submit" // Require CONFIG_DRM_I915_LOW_LEVEL_TRACEPOINTS " -e i915:i915_gem_request_in" // Kconfig option to be enabled. " -e i915:i915_gem_request_out" // " -e i915:intel_engine_notify" " -e i915:i915_gem_request_wait_begin" " -e i915:i915_gem_request_wait_end 2>&1"; char cmd[ 8192 ]; if ( !kbuffersize ) kbuffersize = 16 * 1024; snprintf( cmd, sizeof( cmd ), fmt, kbuffersize ); return exec_tracecmd( cmd ); } GPUVIS_EXTERN int gpuvis_trigger_capture_and_keep_tracing( char *filename, size_t size ) { int ret = -1; if ( filename ) filename[ 0 ] = 0; flush_hot_func_calls(); if ( gpuvis_tracing_on() ) { char datetime[ 128 ]; char cmd[ PATH_MAX ]; char exebuf[ PATH_MAX ]; const char *exename = NULL; time_t t = time( NULL ); struct tm *tmp = localtime( &t ); strftime( datetime, sizeof( datetime ), "%Y-%m-%d_%H-%M-%S", tmp ); datetime[ sizeof( datetime ) - 1 ] = 0; ssize_t cbytes = readlink( "/proc/self/exe", exebuf, sizeof( exebuf ) - 1 ); if ( cbytes > 0 ) { exebuf[ cbytes ] = 0; exename = strrchr( exebuf, '/' ); } exename = exename ? ( exename + 1 ) : "trace"; // Stop tracing exec_tracecmd( "trace-cmd stop 2>&1" ); // Save the trace data to something like "glxgears_2017-10-13_17-52-56.dat" snprintf( cmd, sizeof( cmd ), "trace-cmd extract -k -o \"%s_%s.dat\" > /tmp/blah.log 2>&1 &", exename, datetime ); cmd[ sizeof( cmd ) - 1 ] = 0; ret = system( cmd ); if ( filename && !ret ) snprintf( filename, size, "%s_%s.dat", exename, datetime ); // Restart tracing exec_tracecmd( "trace-cmd restart 2>&1" ); } return ret; } GPUVIS_EXTERN int gpuvis_stop_tracing() { flush_hot_func_calls(); int ret = exec_tracecmd( "trace-cmd reset 2>&1"); // Try freeing any snapshot buffers as well exec_tracecmd( "trace-cmd snapshot -f 2>&1" ); return ret; } GPUVIS_EXTERN int gpuvis_tracing_on() { int ret = -1; char buf[ 32 ]; char filename[ PATH_MAX ]; if ( gpuvis_get_tracefs_filename( filename, PATH_MAX, "tracing_on" ) ) { int fd = open( filename, O_RDONLY ); if ( fd >= 0 ) { if ( read( fd, buf, sizeof( buf ) ) > 0 ) ret = atoi( buf ); close( fd ); } } return ret; } static int is_tracefs_dir( const char *dir ) { struct statfs stat; return !statfs( dir, &stat ) && ( stat.f_type == TRACEFS_MAGIC ); } GPUVIS_EXTERN const char *gpuvis_get_tracefs_dir() { if ( !g_tracefs_dir_inited ) { size_t i; static const char *tracefs_dirs[] = { "/sys/kernel/tracing", "/sys/kernel/debug/tracing", "/tracing", "/trace", }; for ( i = 0; i < sizeof( tracefs_dirs ) / sizeof( tracefs_dirs[ 0 ] ); i++ ) { if ( is_tracefs_dir( tracefs_dirs[ i ] ) ) { strncpy( g_tracefs_dir, tracefs_dirs[ i ], PATH_MAX ); g_tracefs_dir[ PATH_MAX - 1 ] = 0; break; } } if ( !g_tracefs_dir[ 0 ] ) { FILE *fp; char type[ 128 ]; char dir[ PATH_MAX + 1 ]; fp = fopen( "/proc/mounts", "r" ); if ( fp ) { while ( fscanf( fp, "%*s %" GPUVIS_STR_VALUE( PATH_MAX ) "s %127s %*s %*d %*d\n", dir, type ) == 2 ) { if ( !strcmp( type, "tracefs" ) && is_tracefs_dir( dir ) ) { strncpy( g_tracefs_dir, dir, PATH_MAX ); g_tracefs_dir[ PATH_MAX - 1 ] = 0; break; } } fclose( fp ); } } g_tracefs_dir_inited = 1; } return g_tracefs_dir; } GPUVIS_EXTERN const char *gpuvis_get_tracefs_filename( char *buf, size_t buflen, const char *file ) { const char *tracefs_dir = gpuvis_get_tracefs_dir(); if ( tracefs_dir[ 0 ] ) { // truncation is ok here #if defined( __GNUC__ ) && !defined(__llvm__) && !defined(__INTEL_COMPILER) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-truncation" #endif snprintf( buf, buflen, "%s/%s", tracefs_dir, file ); #if defined( __GNUC__ ) && !defined(__llvm__) && !defined(__INTEL_COMPILER) #pragma GCC diagnostic pop #endif buf[ buflen - 1 ] = 0; return buf; } return NULL; } #endif // GPUVIS_TRACE_IMPLEMENTATION #endif // _GPUVIS_TRACE_UTILS_H_ ValveSoftware-gamescope-eb620ab/src/hdmi.h000066400000000000000000000053431502457270500206230ustar00rootroot00000000000000#pragma once #include /* from CTA-861-G */ #define HDMI_EOTF_SDR 0 #define HDMI_EOTF_TRADITIONAL_HDR 1 #define HDMI_EOTF_ST2084 2 #define HDMI_EOTF_HLG 3 #if HAVE_DRM #include #else /** * struct hdr_metadata_infoframe - HDR Metadata Infoframe Data. * * HDR Metadata Infoframe as per CTA 861.G spec. This is expected * to match exactly with the spec. * * Userspace is expected to pass the metadata information as per * the format described in this structure. */ struct hdr_metadata_infoframe { /** * @eotf: Electro-Optical Transfer Function (EOTF) * used in the stream. */ __u8 eotf; /** * @metadata_type: Static_Metadata_Descriptor_ID. */ __u8 metadata_type; /** * @display_primaries: Color Primaries of the Data. * These are coded as unsigned 16-bit values in units of * 0.00002, where 0x0000 represents zero and 0xC350 * represents 1.0000. * @display_primaries.x: X cordinate of color primary. * @display_primaries.y: Y cordinate of color primary. */ struct { uint16_t x, y; } display_primaries[3]; /** * @white_point: White Point of Colorspace Data. * These are coded as unsigned 16-bit values in units of * 0.00002, where 0x0000 represents zero and 0xC350 * represents 1.0000. * @white_point.x: X cordinate of whitepoint of color primary. * @white_point.y: Y cordinate of whitepoint of color primary. */ struct { uint16_t x, y; } white_point; /** * @max_display_mastering_luminance: Max Mastering Display Luminance. * This value is coded as an unsigned 16-bit value in units of 1 cd/m2, * where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. */ uint16_t max_display_mastering_luminance; /** * @min_display_mastering_luminance: Min Mastering Display Luminance. * This value is coded as an unsigned 16-bit value in units of * 0.0001 cd/m2, where 0x0001 represents 0.0001 cd/m2 and 0xFFFF * represents 6.5535 cd/m2. */ uint16_t min_display_mastering_luminance; /** * @max_cll: Max Content Light Level. * This value is coded as an unsigned 16-bit value in units of 1 cd/m2, * where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. */ uint16_t max_cll; /** * @max_fall: Max Frame Average Light Level. * This value is coded as an unsigned 16-bit value in units of 1 cd/m2, * where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2. */ uint16_t max_fall; }; /** * struct hdr_output_metadata - HDR output metadata * * Metadata Information to be passed from userspace */ struct hdr_output_metadata { /** * @metadata_type: Static_Metadata_Descriptor_ID. */ uint32_t metadata_type; /** * @hdmi_metadata_type1: HDR Metadata Infoframe. */ union { struct hdr_metadata_infoframe hdmi_metadata_type1; }; }; #endif ValveSoftware-gamescope-eb620ab/src/ime.cpp000066400000000000000000000512351502457270500210100ustar00rootroot00000000000000#include "ime.hpp" #include "wlserver.hpp" #include "log.hpp" #include #include #include #include #include #include #include #include "wlr_begin.hpp" #include #include #include #include "wlr_end.hpp" #include "gamescope-input-method-protocol.h" struct wlserver_input_method_manager *global_manager = nullptr; /* The C/C++ standard library doesn't expose a reliable way to decode UTF-8, * so we need to ship our own implementation. Yay for locales. */ static const uint32_t UTF8_INVALID = 0xFFFD; static size_t utf8_size(const char *str) { uint8_t u8 = (uint8_t)str[0]; if (u8 == 0) { return 0; } else if ((u8 & 0x80) == 0) { return 1; } else if ((u8 & 0xE0) == 0xC0) { return 2; } else if ((u8 & 0xF0) == 0xE0) { return 3; } else if ((u8 & 0xF8) == 0xF0) { return 4; } else { return 0; } } static uint32_t utf8_decode(const char **str_ptr) { const char *str = *str_ptr; size_t size = utf8_size(str); if (size == 0) { *str_ptr = &str[1]; return UTF8_INVALID; } *str_ptr = &str[size]; const uint32_t masks[] = { 0x7F, 0x1F, 0x0F, 0x07 }; uint32_t ret = (uint32_t)str[0] & masks[size - 1]; for (size_t i = 1; i < size; i++) { ret <<= 6; ret |= str[i] & 0x3F; } return ret; } #define IME_MANAGER_VERSION 3 /* Some clients assume keycodes are coming from evdev and interpret them. Only * use keys that would normally produce characters for our emulated events. */ static const uint32_t allow_keycodes[] = { KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0, KEY_MINUS, KEY_EQUAL, KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_U, KEY_I, KEY_O, KEY_P, KEY_LEFTBRACE, KEY_RIGHTBRACE, KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, KEY_H, KEY_J, KEY_K, KEY_L, KEY_SEMICOLON, KEY_APOSTROPHE, KEY_GRAVE, KEY_BACKSLASH, KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B, KEY_N, KEY_M, KEY_COMMA, KEY_DOT, KEY_SLASH, }; static const size_t allow_keycodes_len = sizeof(allow_keycodes) / sizeof(allow_keycodes[0]); struct wlserver_input_method_key { uint32_t keycode; xkb_keysym_t keysym; }; static std::unordered_map actions = { { GAMESCOPE_INPUT_METHOD_ACTION_SUBMIT, { KEY_ENTER, XKB_KEY_Return } }, { GAMESCOPE_INPUT_METHOD_ACTION_DELETE_LEFT, { KEY_BACKSPACE, XKB_KEY_BackSpace } }, { GAMESCOPE_INPUT_METHOD_ACTION_DELETE_RIGHT, { KEY_DELETE, XKB_KEY_Delete } }, { GAMESCOPE_INPUT_METHOD_ACTION_MOVE_LEFT, { KEY_LEFT, XKB_KEY_Left } }, { GAMESCOPE_INPUT_METHOD_ACTION_MOVE_RIGHT, { KEY_RIGHT, XKB_KEY_Right } }, { GAMESCOPE_INPUT_METHOD_ACTION_MOVE_UP, { KEY_UP, XKB_KEY_Up } }, { GAMESCOPE_INPUT_METHOD_ACTION_MOVE_DOWN, { KEY_DOWN, XKB_KEY_Down } }, }; struct wlserver_input_method { struct wl_resource *resource; struct wlserver_input_method_manager *manager; uint32_t serial; struct { char *string; enum gamescope_input_method_action action; } pending; // Used to send emulated input events struct wlr_keyboard keyboard; std::deque keys; uint32_t next_keycode_index; int32_t held_keycode; xkb_mod_mask_t held_modifier_mask; struct wlr_keyboard_modifiers prev_mods; struct wl_event_source *ime_reset_ime_keyboard_event_source; struct wl_event_source *ime_release_ime_keypress_event_source; uint32_t uFakeTimestamp = 0; }; struct wlserver_input_method_manager { struct wl_global *global; struct wlserver_t *server; struct wl_event_source *ime_reset_keyboard_event_source; }; static LogScope ime_log("ime"); static xkb_keysym_t keysym_from_ch(uint32_t ch) { // There's a bug in libxkbcommon where the EURO symbol doesn't map to the correct keysym if (ch == 0x20ac) { return XKB_KEY_EuroSign; } if ((ch >= 0x3130 && ch <= 0x318f) // Hangul Compatibility Jamo that CEF does not like as keysyms || (ch >= 0x11A8 && ch <= 0x11C2) // subset of Hangul Jamo that CEF does not like as keysyms || (ch == 0x11EB) // continued || (ch == 0x11F0) // continued || (ch == 0x11F9) // continued || (ch == 0x2030)) // PER MILLE SIGN { return ch | 0x1000000; } return xkb_utf32_to_keysym(ch); } static uint32_t keycode_from_ch(struct wlserver_input_method *ime, uint32_t ch) { xkb_keysym_t keysym = keysym_from_ch(ch); if (keysym == XKB_KEY_NoSymbol) { return XKB_KEYCODE_INVALID; } // Repeated chars can re-use keycode if (!ime->keys.empty() && ime->keys.back().keysym == keysym) { return ime->keys.back().keycode; } if (ime->keys.size() >= allow_keycodes_len) { // TODO: maybe use keycodes above KEY_MAX? ime_log.errorf("Key codes wrapped within 100ms!"); ime->keys.pop_front(); // FALLTHROUGH and allow re-use (oldest key probably fine anyway) } uint32_t keycode = allow_keycodes[ime->next_keycode_index++ % allow_keycodes_len]; ime->keys.push_back((struct wlserver_input_method_key){ keycode, keysym }); return keycode; } static bool generate_keymap_key(FILE *f, uint32_t keycode, xkb_keysym_t keysym) { char keysym_name[256]; int ret = xkb_keysym_get_name(keysym, keysym_name, sizeof(keysym_name)); if (ret <= 0) { ime_log.errorf("xkb_keysym_get_name failed for keysym %u", keysym); return false; } fprintf(f, " key {[ %s ]};\n", keycode, keysym_name); return true; } static struct xkb_keymap *generate_keymap(struct wlserver_input_method *ime) { uint32_t keycode_offset = 8; char *str = NULL; size_t str_size = 0; FILE *f = open_memstream(&str, &str_size); // min/max from the set of all allow_keycodes and actions uint32_t min_keycode = KEY_1; uint32_t max_keycode = KEY_DELETE; fprintf(f, "xkb_keymap {\n" "\n" "xkb_keycodes \"(unnamed)\" {\n" " minimum = %u;\n" " maximum = %u;\n", keycode_offset + min_keycode, keycode_offset + max_keycode ); for (const auto& kk : ime->keys) { uint32_t keycode = kk.keycode; fprintf(f, " = %u;\n", keycode, keycode + keycode_offset); } for (const auto& kv : actions) { uint32_t keycode = kv.second.keycode; fprintf(f, " = %u;\n", keycode, keycode + keycode_offset); } // TODO: should we really be including "complete" here? squeekboard seems // to get away with some other workarounds: // https://gitlab.gnome.org/World/Phosh/squeekboard/-/blob/fc411d680b0138042b95b8a630401607726113d4/src/keyboard.rs#L180 fprintf(f, "};\n" "\n" "xkb_types \"(unnamed)\" { include \"complete\" };\n" "\n" "xkb_compatibility \"(unnamed)\" { include \"complete\" };\n" "\n" "xkb_symbols \"(unnamed)\" {\n" ); for (const auto& kk : ime->keys) { if (!generate_keymap_key(f, kk.keycode, kk.keysym)) { fclose(f); free(str); return nullptr; } } for (const auto& kv : actions) { const auto kk = kv.second; if (!generate_keymap_key(f, kk.keycode, kk.keysym)) { fclose(f); free(str); return nullptr; } } fprintf(f, "};\n" "\n" "};\n" ); fclose(f); struct xkb_context *context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); struct xkb_keymap *keymap = xkb_keymap_new_from_buffer(context, str, str_size, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); xkb_context_unref(context); free(str); return keymap; } static int release_key_if_needed(void *data) { struct wlserver_input_method *ime = (struct wlserver_input_method *)data; struct wlr_seat *seat = ime->manager->server->wlr.seat; if (ime->held_keycode >= 0) wlr_seat_keyboard_notify_key(seat, 0, ime->held_keycode, WL_KEYBOARD_KEY_STATE_RELEASED); ime->held_keycode = -1; if (ime->held_modifier_mask) { if (ime->held_modifier_mask & WLR_MODIFIER_ALT) wlr_seat_keyboard_notify_key(seat, 0, KEY_LEFTALT, WL_KEYBOARD_KEY_STATE_RELEASED); if (ime->held_modifier_mask & WLR_MODIFIER_CTRL) wlr_seat_keyboard_notify_key(seat, 0, KEY_LEFTCTRL, WL_KEYBOARD_KEY_STATE_RELEASED); if (ime->held_modifier_mask & WLR_MODIFIER_SHIFT) wlr_seat_keyboard_notify_key(seat, 0, KEY_LEFTSHIFT, WL_KEYBOARD_KEY_STATE_RELEASED); wlr_seat_keyboard_notify_modifiers(seat, &ime->prev_mods); } ime->prev_mods = wlr_keyboard_modifiers{0}; ime->held_modifier_mask = 0; return 0; } static void press_key(struct wlserver_input_method *ime, uint32_t keycode, struct wlr_keyboard_modifiers *pmods = nullptr) { struct wlr_seat *seat = ime->manager->server->wlr.seat; release_key_if_needed(ime); if (pmods) { if (seat->keyboard_state.keyboard != nullptr) ime->prev_mods = seat->keyboard_state.keyboard->modifiers; wlr_seat_keyboard_notify_modifiers(seat, pmods); ime->held_modifier_mask = pmods->depressed & ~ime->prev_mods.depressed; if (ime->held_modifier_mask & WLR_MODIFIER_SHIFT) wlr_seat_keyboard_notify_key(seat, 0, KEY_LEFTSHIFT, WL_KEYBOARD_KEY_STATE_PRESSED); if (ime->held_modifier_mask & WLR_MODIFIER_CTRL) wlr_seat_keyboard_notify_key(seat, 0, KEY_LEFTCTRL, WL_KEYBOARD_KEY_STATE_PRESSED); if (ime->held_modifier_mask & WLR_MODIFIER_ALT) wlr_seat_keyboard_notify_key(seat, 0, KEY_LEFTALT, WL_KEYBOARD_KEY_STATE_PRESSED); } // Note: Xwayland doesn't care about the time field of the events wlr_seat_keyboard_notify_key(seat, 0, keycode, WL_KEYBOARD_KEY_STATE_PRESSED); ime->held_keycode = keycode; wl_event_source_timer_update(ime->ime_reset_ime_keyboard_event_source, 30 /* ms */); } static bool try_type_keysym(struct wlserver_input_method *ime, xkb_keysym_t keysym) { struct wlr_seat *seat = ime->manager->server->wlr.seat; struct wlr_keyboard *keyboard = ime->manager->server->wlr.virtual_keyboard_device; struct xkb_keymap *keymap = keyboard->keymap; xkb_keycode_t min_keycode = xkb_keymap_min_keycode(keymap); xkb_keycode_t max_keycode = xkb_keymap_max_keycode(keymap); for (xkb_keycode_t keycode = min_keycode; keycode <= max_keycode; keycode++) { xkb_layout_index_t num_layouts = xkb_keymap_num_layouts_for_key(keymap, keycode); for (xkb_layout_index_t layout = 0; layout < num_layouts; layout++) { xkb_level_index_t num_levels = xkb_keymap_num_levels_for_key(keymap, keycode, layout); for (xkb_level_index_t level = 0; level < num_levels; level++) { const xkb_keysym_t *syms = nullptr; int num_syms = xkb_keymap_key_get_syms_by_level(keymap, keycode, layout, level, &syms); if (num_syms != 1) { continue; } if (syms[0] != keysym) { continue; } xkb_mod_mask_t mask; size_t num_masks = xkb_keymap_key_get_mods_for_level(keymap, keycode, layout, level, &mask, 1); if (num_masks != 1) { continue; } xkb_mod_mask_t allowed = WLR_MODIFIER_SHIFT | WLR_MODIFIER_CTRL | WLR_MODIFIER_ALT; if ((mask & allowed) != mask) { continue; } release_key_if_needed(ime); // before keymap change wlr_seat_set_keyboard(seat, keyboard); struct wlr_keyboard_modifiers mods = { .depressed = mask, }; assert(keycode >= 8); press_key(ime, keycode - 8, &mods); return true; } } } return false; } void type_text(struct wlserver_input_method *ime, const char *text) { // If possible, try to type the character without switching the keymap // ...unless we're already using a fancy keymap if (utf8_size(text) == 1 && text[1] == '\0' && ime->keys.empty()) { xkb_keysym_t keysym = keysym_from_ch(text[0]); if (keysym != XKB_KEY_NoSymbol && try_type_keysym(ime, keysym)) { return; } } std::vector keycodes; while (text[0] != '\0') { uint32_t ch = utf8_decode(&text); xkb_keycode_t keycode = keycode_from_ch(ime, ch); if (keycode == XKB_KEYCODE_INVALID) { ime_log.errorf("warning: cannot type character U+%X", ch); continue; } keycodes.push_back(keycode); } struct xkb_keymap *keymap = generate_keymap(ime); if (keymap == nullptr) { ime_log.errorf("failed to generate keymap"); return; } wlr_keyboard_set_keymap(&ime->keyboard, keymap); xkb_keymap_unref(keymap); struct wlr_seat *seat = ime->manager->server->wlr.seat; release_key_if_needed(ime); // before keymap change wlr_seat_set_keyboard(seat, &ime->keyboard); // Note: Xwayland doesn't care about the time field of the events for (size_t i = 0; i < keycodes.size(); i++) { press_key(ime, keycodes[i]); } // Reset keymap when we're idle for a while wl_event_source_timer_update(ime->ime_reset_ime_keyboard_event_source, 100 /* ms */); } static void perform_action(struct wlserver_input_method *ime, enum gamescope_input_method_action action) { if (actions.count(action) == 0) { ime_log.errorf("unsupported action %d", action); return; } const struct wlserver_input_method_key key = actions[action]; // type with default keymap if no crazy keymap is currently active if (ime->keys.empty() && try_type_keysym(ime, key.keysym)) { return; } // Keymap always contains all actions[] struct xkb_keymap *keymap = generate_keymap(ime); if (keymap == nullptr) { ime_log.errorf("failed to generate keymap"); return; } wlr_keyboard_set_keymap(&ime->keyboard, keymap); xkb_keymap_unref(keymap); struct wlr_seat *seat = ime->manager->server->wlr.seat; release_key_if_needed(ime); // before keymap change wlr_seat_set_keyboard(seat, &ime->keyboard); press_key(ime, key.keycode); // Reset keymap when we're idle for a while wl_event_source_timer_update(ime->ime_reset_ime_keyboard_event_source, 100 /* ms */); } static void ime_handle_commit(struct wl_client *client, struct wl_resource *ime_resource, uint32_t serial) { struct wlserver_input_method *ime = (struct wlserver_input_method *)wl_resource_get_user_data(ime_resource); if (serial != ime->serial) { return; } if (ime->pending.string != nullptr) { type_text(ime, ime->pending.string); } if (ime->pending.action != GAMESCOPE_INPUT_METHOD_ACTION_NONE) { perform_action(ime, ime->pending.action); } free(ime->pending.string); ime->pending.string = nullptr; ime->pending.action = GAMESCOPE_INPUT_METHOD_ACTION_NONE; // Steam's virtual keyboard is based on XTest and relies on the keymap to // be reset. However, resetting it immediately is racy: clients will // interpret the keycodes we've just sent with the new keymap. To // workaround these issues, wait for a bit before resetting the keymap. wl_event_source_timer_update(ime->manager->ime_reset_keyboard_event_source, 100 /* ms */); } static void ime_handle_set_string(struct wl_client *client, struct wl_resource *ime_resource, const char *text) { struct wlserver_input_method *ime = (struct wlserver_input_method *)wl_resource_get_user_data(ime_resource); free(ime->pending.string); ime->pending.string = strdup(text); } static void ime_handle_set_action(struct wl_client *client, struct wl_resource *ime_resource, uint32_t action) { struct wlserver_input_method *ime = (struct wlserver_input_method *)wl_resource_get_user_data(ime_resource); ime->pending.action = (enum gamescope_input_method_action)action; } static void ime_handle_destroy(struct wl_client *client, struct wl_resource *ime_resource) { wl_resource_destroy(ime_resource); } static void ime_handle_pointer_motion(struct wl_client *client, struct wl_resource *ime_resource, wl_fixed_t dx, wl_fixed_t dy) { struct wlserver_input_method *ime = (struct wlserver_input_method *)wl_resource_get_user_data(ime_resource); wlserver_mousemotion(wl_fixed_to_double(dx), wl_fixed_to_double(dy), ++ime->uFakeTimestamp); } static void ime_handle_pointer_warp(struct wl_client *client, struct wl_resource *ime_resource, wl_fixed_t x, wl_fixed_t y) { struct wlserver_input_method *ime = (struct wlserver_input_method *)wl_resource_get_user_data(ime_resource); wlserver_mousewarp(wl_fixed_to_double(x), wl_fixed_to_double(y), ++ime->uFakeTimestamp, false ); } static void ime_handle_pointer_wheel(struct wl_client *client, struct wl_resource *ime_resource, int32_t x, int32_t y) { struct wlserver_input_method *ime = (struct wlserver_input_method *)wl_resource_get_user_data(ime_resource); wlserver_mousewheel( x / 120.0, y / 120.0, ++ime->uFakeTimestamp); } static void ime_handle_pointer_button(struct wl_client *client, struct wl_resource *ime_resource, uint32_t button, uint32_t state) { struct wlserver_input_method *ime = (struct wlserver_input_method *)wl_resource_get_user_data(ime_resource); wlserver_mousebutton( button, state == GAMESCOPE_INPUT_METHOD_BUTTON_STATE_PRESSED, ++ime->uFakeTimestamp); } static const struct gamescope_input_method_interface ime_impl = { .destroy = ime_handle_destroy, .commit = ime_handle_commit, .set_string = ime_handle_set_string, .set_action = ime_handle_set_action, .pointer_motion = ime_handle_pointer_motion, .pointer_warp = ime_handle_pointer_warp, .pointer_wheel = ime_handle_pointer_wheel, .pointer_button = ime_handle_pointer_button, }; void destroy_ime(struct wlserver_input_method *ime) { wlr_keyboard_finish(&ime->keyboard); } static void ime_handle_resource_destroy(struct wl_resource *ime_resource) { struct wlserver_input_method *ime = (struct wlserver_input_method *)wl_resource_get_user_data(ime_resource); if (ime == nullptr) return; destroy_ime(ime); delete ime; } static void handle_led_update(struct wlr_keyboard *keyboard, uint32_t leds) {} static const struct wlr_keyboard_impl keyboard_impl = { .led_update = handle_led_update, }; static int reset_ime_keyboard(void *data) { struct wlserver_input_method *ime = (struct wlserver_input_method *)data; release_key_if_needed(ime); ime->keys.clear(); ime->next_keycode_index = 0; // preserve old behavior; could just let this keep going return 0; } static void manager_handle_create_input_method(struct wl_client *client, struct wl_resource *manager_resource, struct wl_resource *seat_resource, uint32_t id) { struct wlserver_input_method_manager *manager = (struct wlserver_input_method_manager *)wl_resource_get_user_data(manager_resource); uint32_t version = wl_resource_get_version(manager_resource); struct wl_resource *ime_resource = wl_resource_create(client, &gamescope_input_method_interface, version, id); wl_resource_set_implementation(ime_resource, &ime_impl, nullptr, ime_handle_resource_destroy); struct wlserver_input_method *ime = new wlserver_input_method(); ime->resource = ime_resource; ime->manager = manager; ime->serial = 1; ime->next_keycode_index = 0; ime->held_keycode = -1; ime->held_modifier_mask = 0; ime->prev_mods = wlr_keyboard_modifiers{0}; wlr_keyboard_init(&ime->keyboard, &keyboard_impl, "ime"); wlr_keyboard_set_repeat_info(&ime->keyboard, 0, 0); wl_resource_set_user_data(ime->resource, ime); gamescope_input_method_send_done(ime->resource, ime->serial); ime->ime_reset_ime_keyboard_event_source = wl_event_loop_add_timer(manager->server->event_loop, reset_ime_keyboard, ime); ime->ime_release_ime_keypress_event_source = wl_event_loop_add_timer(manager->server->event_loop, release_key_if_needed, ime); } struct wlserver_input_method *create_local_ime() { struct wlserver_input_method *ime = new wlserver_input_method(); ime->resource = nullptr; ime->manager = global_manager; ime->serial = 1; ime->next_keycode_index = 0; ime->held_keycode = -1; ime->held_modifier_mask = 0; ime->prev_mods = wlr_keyboard_modifiers{0}; wlr_keyboard_init(&ime->keyboard, &keyboard_impl, "local_ime"); wlr_keyboard_set_repeat_info(&ime->keyboard, 0, 0); ime->ime_reset_ime_keyboard_event_source = wl_event_loop_add_timer(global_manager->server->event_loop, reset_ime_keyboard, ime); ime->ime_release_ime_keypress_event_source = wl_event_loop_add_timer(global_manager->server->event_loop, release_key_if_needed, ime); return ime; } static void manager_handle_destroy(struct wl_client *client, struct wl_resource *manager_resource) { wl_resource_destroy(manager_resource); } static const struct gamescope_input_method_manager_interface manager_impl = { .destroy = manager_handle_destroy, .create_input_method = manager_handle_create_input_method, }; static void manager_bind(struct wl_client *client, void *data, uint32_t version, uint32_t id) { struct wlserver_input_method_manager *manager = (struct wlserver_input_method_manager *)data; struct wl_resource *resource = wl_resource_create(client, &gamescope_input_method_manager_interface, version, id); wl_resource_set_implementation(resource, &manager_impl, manager, nullptr); } static int reset_keyboard(void *data) { struct wlserver_t *wlserver = (struct wlserver_t *)data; // Reset the keyboard if it's not set or set to an IME's struct wlr_seat *seat = wlserver->wlr.seat; if (seat->keyboard_state.keyboard == nullptr || seat->keyboard_state.keyboard->data == nullptr) { wlr_seat_set_keyboard(seat, wlserver->wlr.virtual_keyboard_device); } return 0; } void create_ime_manager(struct wlserver_t *wlserver) { struct wlserver_input_method_manager *manager = new wlserver_input_method_manager(); manager->server = wlserver; manager->global = wl_global_create(wlserver->display, &gamescope_input_method_manager_interface, IME_MANAGER_VERSION, manager, manager_bind); manager->ime_reset_keyboard_event_source = wl_event_loop_add_timer(wlserver->event_loop, reset_keyboard, wlserver); global_manager = manager; } ValveSoftware-gamescope-eb620ab/src/ime.hpp000066400000000000000000000004431502457270500210100ustar00rootroot00000000000000// Input Method Editor #pragma once #include "wlserver.hpp" void create_ime_manager(struct wlserver_t *wlserver); struct wlserver_input_method *create_local_ime(); void destroy_ime(struct wlserver_input_method * ime); void type_text(struct wlserver_input_method *ime, const char *text); ValveSoftware-gamescope-eb620ab/src/layer_defines.h000066400000000000000000000007431502457270500225120ustar00rootroot00000000000000#pragma once #include #include namespace GamescopeLayerClient { // GAMESCOPE_LAYER_CLIENT_FLAGS namespace Flag { static constexpr uint32_t DisableHDR = 1u << 0; static constexpr uint32_t ForceBypass = 1u << 1; static constexpr uint32_t FrameLimiterAware = 1u << 2; static constexpr uint32_t NoSuboptimal = 1u << 3; static constexpr uint32_t ForceSwapchainExtent = 1u << 4; } using Flags = uint32_t; }ValveSoftware-gamescope-eb620ab/src/log.cpp000066400000000000000000000101301502457270500210040ustar00rootroot00000000000000#include #include #include #include #include #include "Utils/Process.h" #include "Utils/Defer.h" #include "convar.h" #include "log.hpp" static constexpr std::string_view GetLogPriorityText( LogPriority ePriority ) { switch ( ePriority ) { case LOG_SILENT: return "[\e[0;37m" "Shh.." "\e[0m]"; case LOG_ERROR: return "[\e[0;31m" "Error" "\e[0m]"; case LOG_WARNING: return "[\e[0;33m" "Warn" "\e[0m] "; case LOG_DEBUG: return "[\e[0;35m" "Debug" "\e[0m]"; default: case LOG_INFO: return "[\e[0;34m" "Info" "\e[0m] "; } } static constexpr std::string_view GetLogName( LogPriority ePriority ) { switch ( ePriority ) { case LOG_SILENT: return "silent"; case LOG_ERROR: return "error"; case LOG_WARNING: return "warning"; case LOG_DEBUG: return "debug"; default: case LOG_INFO: return "info"; } } static constexpr LogPriority GetPriorityFromString( std::string_view psvScope ) { if ( psvScope == "silent" ) return LOG_SILENT; else if ( psvScope == "error" ) return LOG_ERROR; else if ( psvScope == "warning" ) return LOG_WARNING; else if ( psvScope == "debug" ) return LOG_DEBUG; else return LOG_INFO; } struct LogConVar_t { LogConVar_t( LogScope *pScope, std::string_view psvName, LogPriority eDefaultPriority ) : sName{ std::format( "log_{}", psvName ) } , sDescription{ std::format( "Max logging priority for the {} channel. Valid options are: [ silent, error, warning, debug, info ].", psvName ) } , convar { sName, std::string( GetLogName( eDefaultPriority ) ), sDescription, [ pScope ]( gamescope::ConVar &cvar ) { pScope->SetPriority( GetPriorityFromString( cvar ) ); }, } { } std::string sName; std::string sDescription; gamescope::ConVar convar; }; LogScope::LogScope( std::string_view psvName, LogPriority eMaxPriority ) : LogScope( psvName, psvName, eMaxPriority ) { } LogScope::LogScope( std::string_view psvName, std::string_view psvPrefix, LogPriority eMaxPriority ) : m_psvName{ psvName } , m_psvPrefix{ psvPrefix } , m_eMaxPriority{ eMaxPriority } , m_pEnableConVar{ std::make_unique( this, psvName, eMaxPriority ) } { } LogScope::~LogScope() { } bool LogScope::Enabled( LogPriority ePriority ) const { return ePriority <= m_eMaxPriority; } void LogScope::vlogf(enum LogPriority priority, const char *fmt, va_list args) { if ( !Enabled( priority ) ) return; char *buf = nullptr; vasprintf(&buf, fmt, args); if (!buf) return; defer( free(buf); ); std::string_view svBuf = buf; log(priority, svBuf); } void LogScope::log(enum LogPriority priority, std::string_view psvText) { if ( !Enabled( priority ) ) return; for (auto& listener : m_LoggingListeners) listener.second( priority, m_psvPrefix, psvText ); std::string_view psvLogName = GetLogPriorityText( priority ); if ( bPrefixEnabled ) fprintf(stderr, "[%s] %.*s \e[0;37m%.*s:\e[0m %.*s\n", gamescope::Process::GetProcessName(), (int)psvLogName.size(), psvLogName.data(), (int)this->m_psvPrefix.size(), this->m_psvPrefix.data(), (int)psvText.size(), psvText.data()); else fprintf(stderr, "%.*s\n", (int)psvText.size(), psvText.data()); } void LogScope::logf(enum LogPriority priority, const char *fmt, ...) { va_list args; va_start(args, fmt); this->vlogf(priority, fmt, args); va_end(args); } void LogScope::warnf(const char *fmt, ...) { va_list args; va_start(args, fmt); this->vlogf(LOG_WARNING, fmt, args); va_end(args); } void LogScope::errorf(const char *fmt, ...) { va_list args; va_start(args, fmt); this->vlogf(LOG_ERROR, fmt, args); va_end(args); } void LogScope::infof(const char *fmt, ...) { va_list args; va_start(args, fmt); this->vlogf(LOG_INFO, fmt, args); va_end(args); } void LogScope::debugf(const char *fmt, ...) { va_list args; va_start(args, fmt); this->vlogf(LOG_DEBUG, fmt, args); va_end(args); } void LogScope::errorf_errno(const char *fmt, ...) { const char *err = strerror(errno); static char buf[1024]; va_list args; va_start(args, fmt); vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); this->logf(LOG_ERROR, "%s: %s", buf, err); } ValveSoftware-gamescope-eb620ab/src/log.hpp000066400000000000000000000032651502457270500210240ustar00rootroot00000000000000#pragma once #include #include #include #include #include #ifdef __GNUC__ #define ATTRIB_PRINTF(start, end) __attribute__((format(printf, start, end))) #else #define ATTRIB_PRINTF(start, end) #endif enum LogPriority { LOG_SILENT, LOG_ERROR, LOG_WARNING, LOG_INFO, LOG_DEBUG, }; struct LogConVar_t; class LogScope { public: LogScope( std::string_view psvName, LogPriority eMaxPriority = LOG_INFO ); LogScope( std::string_view psvName, std::string_view psvPrefix, LogPriority eMaxPriority = LOG_INFO ); ~LogScope(); bool Enabled( LogPriority ePriority ) const; void SetPriority( LogPriority ePriority ) { m_eMaxPriority = ePriority; } void vlogf(enum LogPriority priority, const char *fmt, va_list args) ATTRIB_PRINTF(3, 0); void log(enum LogPriority priority, std::string_view psvText); void warnf(const char *fmt, ...) ATTRIB_PRINTF(2, 3); void errorf(const char *fmt, ...) ATTRIB_PRINTF(2, 3); void infof(const char *fmt, ...) ATTRIB_PRINTF(2, 3); void debugf(const char *fmt, ...) ATTRIB_PRINTF(2, 3); void errorf_errno(const char *fmt, ...) ATTRIB_PRINTF(2, 3); bool bPrefixEnabled = true; using LoggingListenerFunc = std::function; std::unordered_map m_LoggingListeners; private: void vprintf(enum LogPriority priority, const char *fmt, va_list args) ATTRIB_PRINTF(3, 0); void logf(enum LogPriority priority, const char *fmt, ...) ATTRIB_PRINTF(3, 4); std::string_view m_psvName; std::string_view m_psvPrefix; LogPriority m_eMaxPriority = LOG_INFO; std::unique_ptr m_pEnableConVar; }; ValveSoftware-gamescope-eb620ab/src/main.cpp000066400000000000000000001100421502457270500211520ustar00rootroot00000000000000#include "Script/Script.h" #include #include #include #include #include #include #include #if defined(__linux__) #include #endif #include #include #include #include #include #include #include #include #include "main.hpp" #include "steamcompmgr.hpp" #include "rendervulkan.hpp" #include "wlserver.hpp" #include "convar.h" #include "gpuvis_trace_utils.h" #include "Utils/TempFiles.h" #include "Utils/Version.h" #include "Utils/Process.h" #include "Utils/Defer.h" #include "backends.h" #include "refresh_rate.h" #if HAVE_PIPEWIRE #include "pipewire.hpp" #endif #include using namespace std::literals; EStreamColorspace g_ForcedNV12ColorSpace = k_EStreamColorspace_Unknown; extern gamescope::ConVar cv_adaptive_sync; const char *gamescope_optstring = nullptr; const char *g_pOriginalDisplay = nullptr; const char *g_pOriginalWaylandDisplay = nullptr; int g_nCursorScaleHeight = -1; const struct option *gamescope_options = (struct option[]){ { "help", no_argument, nullptr, 0 }, { "version", no_argument, nullptr, 0 }, { "nested-width", required_argument, nullptr, 'w' }, { "nested-height", required_argument, nullptr, 'h' }, { "nested-refresh", required_argument, nullptr, 'r' }, { "max-scale", required_argument, nullptr, 'm' }, { "scaler", required_argument, nullptr, 'S' }, { "filter", required_argument, nullptr, 'F' }, { "output-width", required_argument, nullptr, 'W' }, { "output-height", required_argument, nullptr, 'H' }, { "sharpness", required_argument, nullptr, 0 }, { "fsr-sharpness", required_argument, nullptr, 0 }, { "rt", no_argument, nullptr, 0 }, { "prefer-vk-device", required_argument, 0 }, { "expose-wayland", no_argument, 0 }, { "mouse-sensitivity", required_argument, nullptr, 's' }, { "mangoapp", no_argument, nullptr, 0 }, { "adaptive-sync", no_argument, nullptr, 0 }, { "backend", required_argument, nullptr, 0 }, // nested mode options { "nested-unfocused-refresh", required_argument, nullptr, 'o' }, { "borderless", no_argument, nullptr, 'b' }, { "fullscreen", no_argument, nullptr, 'f' }, { "grab", no_argument, nullptr, 'g' }, { "force-grab-cursor", no_argument, nullptr, 0 }, { "display-index", required_argument, nullptr, 0 }, // embedded mode options { "disable-layers", no_argument, nullptr, 0 }, { "debug-layers", no_argument, nullptr, 0 }, { "prefer-output", required_argument, nullptr, 'O' }, { "default-touch-mode", required_argument, nullptr, 0 }, { "generate-drm-mode", required_argument, nullptr, 0 }, { "immediate-flips", no_argument, nullptr, 0 }, { "framerate-limit", required_argument, nullptr, 0 }, // openvr options #if HAVE_OPENVR { "vr-overlay-key", required_argument, nullptr, 0 }, { "vr-app-overlay-key", required_argument, nullptr, 0 }, { "vr-overlay-explicit-name", required_argument, nullptr, 0 }, { "vr-overlay-default-name", required_argument, nullptr, 0 }, { "vr-overlay-icon", required_argument, nullptr, 0 }, { "vr-overlay-show-immediately", no_argument, nullptr, 0 }, { "vr-overlay-enable-control-bar", no_argument, nullptr, 0 }, { "vr-overlay-enable-control-bar-keyboard", no_argument, nullptr, 0 }, { "vr-overlay-enable-control-bar-close", no_argument, nullptr, 0 }, { "vr-overlay-enable-click-stabilization", no_argument, nullptr, 0 }, { "vr-overlay-modal", no_argument, nullptr, 0 }, { "vr-overlay-physical-width", required_argument, nullptr, 0 }, { "vr-overlay-physical-curvature", required_argument, nullptr, 0 }, { "vr-overlay-physical-pre-curve-pitch", required_argument, nullptr, 0 }, { "vr-scroll-speed", required_argument, nullptr, 0 }, { "vr-session-manager", no_argument, nullptr, 0 }, #endif // wlserver options { "xwayland-count", required_argument, nullptr, 0 }, // steamcompmgr options { "cursor", required_argument, nullptr, 0 }, { "cursor-hotspot", required_argument, nullptr, 0 }, { "cursor-scale-height", required_argument, nullptr, 0 }, { "virtual-connector-strategy", required_argument, nullptr, 0 }, { "ready-fd", required_argument, nullptr, 'R' }, { "stats-path", required_argument, nullptr, 'T' }, { "hide-cursor-delay", required_argument, nullptr, 'C' }, { "debug-focus", no_argument, nullptr, 0 }, { "synchronous-x11", no_argument, nullptr, 0 }, { "debug-hud", no_argument, nullptr, 'v' }, { "debug-events", no_argument, nullptr, 0 }, { "steam", no_argument, nullptr, 'e' }, { "force-composition", no_argument, nullptr, 'c' }, { "composite-debug", no_argument, nullptr, 0 }, { "disable-xres", no_argument, nullptr, 'x' }, { "fade-out-duration", required_argument, nullptr, 0 }, { "force-orientation", required_argument, nullptr, 0 }, { "force-windows-fullscreen", no_argument, nullptr, 0 }, { "disable-color-management", no_argument, nullptr, 0 }, { "sdr-gamut-wideness", required_argument, nullptr, 0 }, { "hdr-enabled", no_argument, nullptr, 0 }, { "hdr-sdr-content-nits", required_argument, nullptr, 0 }, { "hdr-itm-enabled", no_argument, nullptr, 0 }, { "hdr-itm-sdr-nits", required_argument, nullptr, 0 }, { "hdr-itm-target-nits", required_argument, nullptr, 0 }, { "hdr-debug-force-support", no_argument, nullptr, 0 }, { "hdr-debug-force-output", no_argument, nullptr, 0 }, { "hdr-debug-heatmap", no_argument, nullptr, 0 }, { "reshade-effect", required_argument, nullptr, 0 }, { "reshade-technique-idx", required_argument, nullptr, 0 }, // Steam Deck options { "mura-map", required_argument, nullptr, 0 }, {} // keep last }; const char usage[] = "usage: gamescope [options...] -- [command...]\n" "\n" "Options:\n" " --help show help message\n" " -W, --output-width output width\n" " -H, --output-height output height\n" " -w, --nested-width game width\n" " -h, --nested-height game height\n" " -r, --nested-refresh game refresh rate (frames per second)\n" " -m, --max-scale maximum scale factor\n" " -S, --scaler upscaler type (auto, integer, fit, fill, stretch)\n" " -F, --filter upscaler filter (linear, nearest, fsr, nis, pixel)\n" " fsr => AMD FidelityFX™ Super Resolution 1.0\n" " nis => NVIDIA Image Scaling v1.0.3\n" " --sharpness, --fsr-sharpness upscaler sharpness from 0 (max) to 20 (min)\n" " --expose-wayland support wayland clients using xdg-shell\n" " -s, --mouse-sensitivity multiply mouse movement by given decimal number\n" " --backend select rendering backend\n" " auto => autodetect (default)\n" #if HAVE_DRM " drm => use DRM backend (standalone display session)\n" #endif #if HAVE_SDL2 " sdl => use SDL backend\n" #endif #if HAVE_OPENVR " openvr => use OpenVR backend (outputs as a VR overlay)\n" #endif " headless => use headless backend (no window, no DRM output)\n" " wayland => use Wayland backend\n" " --cursor path to default cursor image\n" " -R, --ready-fd notify FD when ready\n" " --rt Use realtime scheduling\n" " -T, --stats-path write statistics to path\n" " -C, --hide-cursor-delay hide cursor image after delay\n" " -e, --steam enable Steam integration\n" " --xwayland-count create N xwayland servers\n" " --prefer-vk-device prefer Vulkan device for compositing (ex: 1002:7300)\n" " --force-orientation rotate the internal display (left, right, normal, upsidedown)\n" " --force-windows-fullscreen force windows inside of gamescope to be the size of the nested display (fullscreen)\n" " --cursor-scale-height if specified, sets a base output height to linearly scale the cursor against.\n" " --virtual-connector-strategy Specifies how we should make virtual connectors.\n" " --hdr-enabled enable HDR output (needs Gamescope WSI layer enabled for support from clients)\n" " If this is not set, and there is a HDR client, it will be tonemapped SDR.\n" " --sdr-gamut-wideness Set the 'wideness' of the gamut for SDR comment. 0 - 1.\n" " --hdr-sdr-content-nits set the luminance of SDR content in nits. Default: 400 nits.\n" " --hdr-itm-enabled enable SDR->HDR inverse tone mapping. only works for SDR input.\n" " --hdr-itm-sdr-nits set the luminance of SDR content in nits used as the input for the inverse tone mapping process.\n" " Default: 100 nits, Max: 1000 nits\n" " --hdr-itm-target-nits set the target luminace of the inverse tone mapping process.\n" " Default: 1000 nits, Max: 10000 nits\n" " --framerate-limit Set a simple framerate limit. Used as a divisor of the refresh rate, rounds down eg 60 / 59 -> 60fps, 60 / 25 -> 30fps. Default: 0, disabled.\n" " --mangoapp Launch with the mangoapp (mangohud) performance overlay enabled. You should use this instead of using mangohud on the game or gamescope.\n" " --adaptive-sync Enable adaptive sync if available (variable rate refresh)\n" "\n" "Nested mode options:\n" " -o, --nested-unfocused-refresh game refresh rate when unfocused\n" " -b, --borderless make the window borderless\n" " -f, --fullscreen make the window fullscreen\n" " -g, --grab grab the keyboard\n" " --force-grab-cursor always use relative mouse mode instead of flipping dependent on cursor visibility.\n" " --display-index forces gamescope to use a specific display in nested mode." "\n" "Embedded mode options:\n" " -O, --prefer-output list of connectors in order of preference (ex: DP-1,DP-2,DP-3,HDMI-A-1)\n" " --default-touch-mode 0: hover, 1: left, 2: right, 3: middle, 4: passthrough\n" " --generate-drm-mode DRM mode generation algorithm (cvt, fixed)\n" " --immediate-flips Enable immediate flips, may result in tearing\n" "\n" #if HAVE_OPENVR "VR mode options:\n" " --vr-overlay-key Sets the SteamVR overlay key to this string\n" " --vr-app-overlay-key Sets the SteamVR overlay key to use for child apps\n" " --vr-overlay-explicit-name Force the SteamVR overlay name to always be this string\n" " --vr-overlay-default-name Sets the fallback SteamVR overlay name when there is no window title\n" " --vr-overlay-icon Sets the SteamVR overlay icon to this file\n" " --vr-overlay-show-immediately Makes our VR overlay take focus immediately\n" " --vr-overlay-enable-control-bar Enables the SteamVR control bar\n" " --vr-overlay-enable-control-bar-keyboard Enables the SteamVR keyboard button on the control bar\n" " --vr-overlay-enable-control-bar-close Enables the SteamVR close button on the control bar\n" " --vr-overlay-enable-click-stabilization Enables the SteamVR click stabilization\n" " --vr-overlay-modal Makes our VR overlay appear as a modal\n" " --vr-overlay-physical-width Sets the physical width of our VR overlay in metres\n" " --vr-overlay-physical-curvature Sets the curvature of our VR overlay\n" " --vr-overlay-physical-pre-curve-pitch Sets the pre-curve pitch of our VR overlay\n" " --vr-scrolls-speed Mouse scrolling speed of trackpad scroll in VR. Default: 8.0\n" "\n" #endif "Debug options:\n" " --disable-layers disable libliftoff (hardware planes)\n" " --debug-layers debug libliftoff\n" " --debug-focus debug XWM focus\n" " --synchronous-x11 force X11 connection synchronization\n" " --debug-hud paint HUD with debug info\n" " --debug-events debug X11 events\n" " --force-composition disable direct scan-out\n" " --composite-debug draw frame markers on alternating corners of the screen when compositing\n" " --disable-color-management disable color management\n" " --disable-xres disable XRes for PID lookup\n" " --hdr-debug-force-support forces support for HDR, etc even if the display doesn't support it. HDR clients will be outputted as SDR still in that case.\n" " --hdr-debug-force-output forces support and output to HDR10 PQ even if the output does not support it (will look very wrong if it doesn't)\n" " --hdr-debug-heatmap displays a heatmap-style debug view of HDR luminence across the scene in nits." "\n" "Reshade shader options:\n" " --reshade-effect sets the name of a reshade shader to use in either /usr/share/gamescope/reshade/Shaders or ~/.local/share/gamescope/reshade/Shaders\n" " --reshade-technique-idx sets technique idx to use from the reshade effect\n" "\n" "Steam Deck options:\n" " --mura-map Set the mura compensation map to use for the display. Takes in a path to the mura map.\n" "\n" "Keyboard shortcuts:\n" " Super + F toggle fullscreen\n" " Super + N toggle nearest neighbour filtering\n" " Super + U toggle FSR upscaling\n" " Super + Y toggle NIS upscaling\n" " Super + I increase FSR sharpness by 1\n" " Super + O decrease FSR sharpness by 1\n" " Super + S take a screenshot\n" " Super + G toggle keyboard grab\n" ""; std::atomic< bool > g_bRun{true}; int g_nNestedWidth = 0; int g_nNestedHeight = 0; int g_nNestedRefresh = 0; int g_nNestedUnfocusedRefresh = 0; int g_nNestedDisplayIndex = 0; uint32_t g_nOutputWidth = 0; uint32_t g_nOutputHeight = 0; int g_nOutputRefresh = 0; bool g_bOutputHDREnabled = false; bool g_bFullscreen = false; bool g_bForceRelativeMouse = false; bool g_bGrabbed = false; float g_mouseSensitivity = 1.0; GamescopeUpscaleFilter g_upscaleFilter = GamescopeUpscaleFilter::LINEAR; GamescopeUpscaleScaler g_upscaleScaler = GamescopeUpscaleScaler::AUTO; GamescopeUpscaleFilter g_wantedUpscaleFilter = GamescopeUpscaleFilter::LINEAR; GamescopeUpscaleScaler g_wantedUpscaleScaler = GamescopeUpscaleScaler::AUTO; int g_upscaleFilterSharpness = 2; gamescope::GamescopeModeGeneration g_eGamescopeModeGeneration = gamescope::GAMESCOPE_MODE_GENERATE_CVT; bool g_bBorderlessOutputWindow = false; int g_nXWaylandCount = 1; float g_flMaxWindowScale = FLT_MAX; uint32_t g_preferVendorID = 0; uint32_t g_preferDeviceID = 0; pthread_t g_mainThread; static void steamCompMgrThreadRun(int argc, char **argv); static std::string build_optstring(const struct option *options) { std::string optstring; for (size_t i = 0; options[i].name != nullptr; i++) { if (!options[i].name || !options[i].val) continue; assert(optstring.find((char) options[i].val) == std::string::npos); char str[] = { (char) options[i].val, '\0' }; optstring.append(str); if (options[i].has_arg) optstring.append(":"); } return optstring; } static gamescope::GamescopeModeGeneration parse_gamescope_mode_generation( const char *str ) { if ( str == "cvt"sv ) { return gamescope::GAMESCOPE_MODE_GENERATE_CVT; } else if ( str == "fixed"sv ) { return gamescope::GAMESCOPE_MODE_GENERATE_FIXED; } else { fprintf( stderr, "gamescope: invalid value for --generate-drm-mode\n" ); exit(1); } } GamescopePanelOrientation g_DesiredInternalOrientation = GAMESCOPE_PANEL_ORIENTATION_AUTO; static GamescopePanelOrientation force_orientation(const char *str) { if (strcmp(str, "normal") == 0) { return GAMESCOPE_PANEL_ORIENTATION_0; } else if (strcmp(str, "right") == 0) { return GAMESCOPE_PANEL_ORIENTATION_270; } else if (strcmp(str, "left") == 0) { return GAMESCOPE_PANEL_ORIENTATION_90; } else if (strcmp(str, "upsidedown") == 0) { return GAMESCOPE_PANEL_ORIENTATION_180; } else { fprintf( stderr, "gamescope: invalid value for --force-orientation\n" ); exit(1); } } static enum GamescopeUpscaleScaler parse_upscaler_scaler(const char *str) { if (strcmp(str, "auto") == 0) { return GamescopeUpscaleScaler::AUTO; } else if (strcmp(str, "integer") == 0) { return GamescopeUpscaleScaler::INTEGER; } else if (strcmp(str, "fit") == 0) { return GamescopeUpscaleScaler::FIT; } else if (strcmp(str, "fill") == 0) { return GamescopeUpscaleScaler::FILL; } else if (strcmp(str, "stretch") == 0) { return GamescopeUpscaleScaler::STRETCH; } else { fprintf( stderr, "gamescope: invalid value for --scaler\n" ); exit(1); } } static enum GamescopeUpscaleFilter parse_upscaler_filter(const char *str) { if (strcmp(str, "linear") == 0) { return GamescopeUpscaleFilter::LINEAR; } else if (strcmp(str, "nearest") == 0) { return GamescopeUpscaleFilter::NEAREST; } else if (strcmp(str, "fsr") == 0) { return GamescopeUpscaleFilter::FSR; } else if (strcmp(str, "nis") == 0) { return GamescopeUpscaleFilter::NIS; } else if (strcmp(str, "pixel") == 0) { return GamescopeUpscaleFilter::PIXEL; } else { fprintf( stderr, "gamescope: invalid value for --filter\n" ); exit(1); } } static enum gamescope::GamescopeBackend parse_backend_name(const char *str) { if (strcmp(str, "auto") == 0) { return gamescope::GamescopeBackend::Auto; #if HAVE_DRM } else if (strcmp(str, "drm") == 0) { return gamescope::GamescopeBackend::DRM; #endif #if HAVE_SDL2 } else if (strcmp(str, "sdl") == 0) { return gamescope::GamescopeBackend::SDL; #endif #if HAVE_OPENVR } else if (strcmp(str, "openvr") == 0) { return gamescope::GamescopeBackend::OpenVR; #endif } else if (strcmp(str, "headless") == 0) { return gamescope::GamescopeBackend::Headless; } else if (strcmp(str, "wayland") == 0) { return gamescope::GamescopeBackend::Wayland; } else { fprintf( stderr, "gamescope: invalid value for --backend\n" ); exit(1); } } static int parse_integer(const char *str, const char *optionName) { auto result = gamescope::Parse(str); if ( result.has_value() ) { return result.value(); } else { fprintf( stderr, "gamescope: invalid value for --%s, \"%s\" is either not an integer or is far too large\n", optionName, str ); exit(1); } } static float parse_float(const char *str, const char *optionName) { auto result = gamescope::Parse(str); if ( result.has_value() ) { return result.value(); } else { fprintf( stderr, "gamescope: invalid value for --%s, \"%s\" could not be interpreted as a real number\n", optionName, str ); exit(1); } } struct sigaction handle_signal_action = {}; void ShutdownGamescope() { g_bRun = false; nudge_steamcompmgr(); } static gamescope::ConCommand cc_shutdown( "shutdown", "Cleanly shutdown gamescope", []( std::span svArgs ) { console_log.infof( "Shutting down..." ); ShutdownGamescope(); }); static void handle_signal( int sig ) { switch ( sig ) { case SIGUSR2: gamescope::CScreenshotManager::Get().TakeScreenshot( true ); break; case SIGHUP: case SIGQUIT: case SIGTERM: case SIGINT: ShutdownGamescope(); break; case SIGUSR1: fprintf( stderr, "gamescope: hi :3\n" ); break; default: assert( false ); // unreachable } } static EStreamColorspace parse_colorspace_string( const char *pszStr ) { if ( !pszStr || !*pszStr ) return k_EStreamColorspace_Unknown; if ( !strcmp( pszStr, "k_EStreamColorspace_BT601" ) ) return k_EStreamColorspace_BT601; else if ( !strcmp( pszStr, "k_EStreamColorspace_BT601_Full" ) ) return k_EStreamColorspace_BT601_Full; else if ( !strcmp( pszStr, "k_EStreamColorspace_BT709" ) ) return k_EStreamColorspace_BT709; else if ( !strcmp( pszStr, "k_EStreamColorspace_BT709_Full" ) ) return k_EStreamColorspace_BT709_Full; else return k_EStreamColorspace_Unknown; } static bool g_bSupportsWaylandPresentationTime = false; static constexpr wl_registry_listener s_registryListener = { .global = [](void* data, wl_registry* registry, uint32_t name, const char* interface, uint32_t version) { if (interface == "wp_presentation"sv) g_bSupportsWaylandPresentationTime = true; }, .global_remove = [](void* data, wl_registry* registry, uint32_t name) { }, }; static bool CheckWaylandPresentationTime() { wl_display *display = wl_display_connect(g_pOriginalWaylandDisplay); if (!display) { fprintf(stderr, "Failed to connect to wayland socket: %s.\n", g_pOriginalWaylandDisplay); exit(1); return false; } wl_registry *registry = wl_display_get_registry(display); wl_registry_add_listener(registry, &s_registryListener, nullptr); wl_display_dispatch(display); wl_display_roundtrip(display); wl_registry_destroy(registry); wl_display_disconnect(display); return g_bSupportsWaylandPresentationTime; } #if 0 static bool IsInDebugSession() { static FILE *fp; if ( !fp ) { fp = fopen( "/proc/self/status", "r" ); } char rgchLine[256]; rgchLine[0] = '\0'; int nTracePid = 0; if ( fp ) { const char *pszSearchString = "TracerPid:"; const uint cchSearchString = strlen( pszSearchString ); rewind( fp ); fflush( fp ); while ( fgets( rgchLine, sizeof(rgchLine), fp ) ) { if ( !strncasecmp( pszSearchString, rgchLine, cchSearchString ) ) { char *pszVal = rgchLine+cchSearchString+1; nTracePid = atoi( pszVal ); break; } } } return nTracePid != 0; } #endif bool steamMode = false; bool g_bLaunchMangoapp = false; static void UpdateCompatEnvVars() { // Legacy env vars for compat. if ( steamMode ) { // We have NIS support. setenv( "STEAM_GAMESCOPE_NIS_SUPPORTED", "1", 0 ); // Have SteamRT's xdg-open send http:// and https:// URLs to Steam setenv( "SRT_URLOPEN_PREFER_STEAM", "1", 0 ); if ( g_nXWaylandCount > 1 ) { setenv( "STEAM_MULTIPLE_XWAYLANDS", "1", 0 ); } // If the backend exposes tearing, expose that to Steam. if ( GetBackend()->SupportsTearing() ) { setenv( "STEAM_GAMESCOPE_TEARING_SUPPORTED", "1", 0 ); setenv( "STEAM_GAMESCOPE_HAS_TEARING_SUPPORT", "1", 0 ); } // We always support VRR (but not necessarily on every connector, etc.) setenv( "STEAM_GAMESCOPE_VRR_SUPPORTED", "1", 0 ); // We no longer need to set GAMESCOPE_EXTERNAL_OVERLAY from steam, mangoapp now does it itself setenv( "STEAM_DISABLE_MANGOAPP_ATOM_WORKAROUND", "1", 0 ); // Enable horizontal mangoapp bar setenv( "STEAM_MANGOAPP_HORIZONTAL_SUPPORTED", "1", 0 ); // Scaling support setenv( "STEAM_GAMESCOPE_FANCY_SCALING_SUPPORT", "1", 0 ); // We support HDR. setenv( "STEAM_GAMESCOPE_HDR_SUPPORTED", "1", 0 ); // Gamescope WSI layer implements this. setenv( "STEAM_GAMESCOPE_DYNAMIC_FPSLIMITER", "1", 0 ); // Set input method modules for Qt/GTK that will show the Steam keyboard // These are mostly SteamOS specific, and are set by our Gamescope session, // but might be useful for you. //setenv( "QT_IM_MODULE", "steam", 1 ); //setenv( "GTK_IM_MODULE", "Steam", 1 ); //setenv( "QT_QPA_PLATFORM_THEME", "kde", 1 ); // Maybe we should expose a backend check for this... // STEAM_GAMESCOPE_COLOR_MANAGED // STEAM_GAMESCOPE_VIRTUAL_WHITE // STEAM_USE_DYNAMIC_VRS is RADV specific, so don't expose this right now. setenv( "STEAM_MANGOAPP_PRESETS_SUPPORTED", "1", 0 ); setenv( "STEAM_USE_MANGOAPP", "1", 0 ); } // Always set this to false, we never want buffers to be waited on by Mesa. // That is our job! setenv( "vk_xwayland_wait_ready", "false", 1 ); if ( g_nCursorScaleHeight > 0 ) { // We always want the biggest cursor size so we can scale it. setenv( "XCURSOR_SIZE", "256", 1 ); } // Legacy support for SteamOS. setenv( "XWAYLAND_FORCE_ENABLE_EXTRA_MODES", "1", 1 ); // Don't minimise stuff on focus loss with SDL. setenv( "SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS", "0", 1 ); const char *pszMangoConfigPath = getenv( "MANGOHUD_CONFIGFILE" ); if ( (g_bLaunchMangoapp && steamMode) && ( !pszMangoConfigPath || !*pszMangoConfigPath ) ) { char szMangoConfigPath[ PATH_MAX ]; FILE *pMangoConfigFile = gamescope::MakeTempFile( szMangoConfigPath, gamescope::k_szGamescopeTempMangoappTemplate, "w", true ); if ( pMangoConfigFile ) { setenv( "MANGOHUD_CONFIGFILE", szMangoConfigPath, 1 ); if ( steamMode ) { const char szDefaultConfig[] = "no_display"; fwrite( szDefaultConfig, 1, sizeof( szDefaultConfig ), pMangoConfigFile ); } fclose( pMangoConfigFile ); } } const char *pszLimiterFile = getenv( "GAMESCOPE_LIMITER_FILE" ); if ( !pszLimiterFile || !*pszLimiterFile ) { char szLimiterPath[ PATH_MAX ]; int nLimiterFd = gamescope::MakeTempFile( szLimiterPath, gamescope::k_szGamescopeTempLimiterTemplate, true ); if ( nLimiterFd >= 0 ) { setenv( "GAMESCOPE_LIMITER_FILE", szLimiterPath, 1 ); gamescope::Process::CloseFd( nLimiterFd ); } } } int g_nPreferredOutputWidth = 0; int g_nPreferredOutputHeight = 0; bool g_bExposeWayland = false; const char *g_sOutputName = nullptr; bool g_bDebugLayers = false; bool g_bForceDisableColorMgmt = false; bool g_bRt = false; // This will go away when we remove the getopt stuff from vr session. // For now... int g_argc; char **g_argv; int main(int argc, char **argv) { g_argc = argc; g_argv = argv; // Force disable this horrible broken layer. setenv("DISABLE_LAYER_AMD_SWITCHABLE_GRAPHICS_1", "1", 1); static std::string optstring = build_optstring(gamescope_options); gamescope_optstring = optstring.c_str(); gamescope::GamescopeBackend eCurrentBackend = gamescope::GamescopeBackend::Auto; gamescope::PrintVersion(); int o; int opt_index = -1; while ((o = getopt_long(argc, argv, gamescope_optstring, gamescope_options, &opt_index)) != -1) { const char *opt_name; switch (o) { case 'w': g_nNestedWidth = parse_integer( optarg, "nested-width" ); break; case 'h': g_nNestedHeight = parse_integer( optarg, "nested-height" ); break; case 'r': g_nNestedRefresh = gamescope::ConvertHztomHz( parse_integer( optarg, "nested-refresh" ) ); break; case 'W': g_nPreferredOutputWidth = parse_integer( optarg, "output-width" ); break; case 'H': g_nPreferredOutputHeight = parse_integer( optarg, "output-height" ); break; case 'o': g_nNestedUnfocusedRefresh = gamescope::ConvertHztomHz( parse_integer( optarg, "nested-unfocused-refresh" ) ); break; case 'm': g_flMaxWindowScale = parse_float( optarg, "max-scale" ); break; case 'S': g_wantedUpscaleScaler = parse_upscaler_scaler(optarg); break; case 'F': g_wantedUpscaleFilter = parse_upscaler_filter(optarg); break; case 'b': g_bBorderlessOutputWindow = true; break; case 'f': g_bFullscreen = true; break; case 'O': g_sOutputName = optarg; break; case 'g': g_bGrabbed = true; break; case 's': g_mouseSensitivity = parse_float( optarg, "mouse-sensitivity" ); break; case 'e': steamMode = true; if ( gamescope::cv_backend_virtual_connector_strategy == gamescope::VirtualConnectorStrategies::SingleApplication ) gamescope::cv_backend_virtual_connector_strategy = gamescope::VirtualConnectorStrategies::SteamControlled; break; case 0: // long options without a short option opt_name = gamescope_options[opt_index].name; if (strcmp(opt_name, "help") == 0) { fprintf(stderr, "%s", usage); return 0; } else if (strcmp(opt_name, "version") == 0) { // We always print the version to stderr anyway. return 0; } else if (strcmp(opt_name, "debug-layers") == 0) { g_bDebugLayers = true; } else if (strcmp(opt_name, "disable-color-management") == 0) { g_bForceDisableColorMgmt = true; } else if (strcmp(opt_name, "xwayland-count") == 0) { g_nXWaylandCount = parse_integer( optarg, opt_name ); } else if (strcmp(opt_name, "composite-debug") == 0) { cv_composite_debug |= CompositeDebugFlag::Markers; cv_composite_debug |= CompositeDebugFlag::PlaneBorders; } else if (strcmp(opt_name, "hdr-debug-heatmap") == 0) { cv_composite_debug |= CompositeDebugFlag::Heatmap; } else if (strcmp(opt_name, "default-touch-mode") == 0) { gamescope::cv_touch_click_mode = (gamescope::TouchClickMode) parse_integer( optarg, opt_name ); } else if (strcmp(opt_name, "generate-drm-mode") == 0) { g_eGamescopeModeGeneration = parse_gamescope_mode_generation( optarg ); } else if (strcmp(opt_name, "force-orientation") == 0) { g_DesiredInternalOrientation = force_orientation( optarg ); } else if (strcmp(opt_name, "sharpness") == 0 || strcmp(opt_name, "fsr-sharpness") == 0) { g_upscaleFilterSharpness = parse_integer( optarg, opt_name ); } else if (strcmp(opt_name, "rt") == 0) { g_bRt = true; } else if (strcmp(opt_name, "prefer-vk-device") == 0) { unsigned vendorID; unsigned deviceID; sscanf( optarg, "%X:%X", &vendorID, &deviceID ); g_preferVendorID = vendorID; g_preferDeviceID = deviceID; } else if (strcmp(opt_name, "immediate-flips") == 0) { cv_tearing_enabled = true; } else if (strcmp(opt_name, "force-grab-cursor") == 0) { g_bForceRelativeMouse = true; } else if (strcmp(opt_name, "display-index") == 0) { g_nNestedDisplayIndex = parse_integer( optarg, opt_name ); } else if (strcmp(opt_name, "adaptive-sync") == 0) { cv_adaptive_sync = true; } else if (strcmp(opt_name, "expose-wayland") == 0) { g_bExposeWayland = true; } else if (strcmp(opt_name, "backend") == 0) { eCurrentBackend = parse_backend_name( optarg ); } else if (strcmp(opt_name, "cursor-scale-height") == 0) { g_nCursorScaleHeight = parse_integer(optarg, opt_name); } else if (strcmp(opt_name, "mangoapp") == 0) { g_bLaunchMangoapp = true; } else if (strcmp(opt_name, "virtual-connector-strategy") == 0) { for ( uint32_t i = 0; i < gamescope::VirtualConnectorStrategies::Count; i++ ) { gamescope::VirtualConnectorStrategy eStrategy = static_cast( i ); if ( optarg == gamescope::VirtualConnectorStrategyToString( eStrategy ) ) { gamescope::cv_backend_virtual_connector_strategy = eStrategy; } } } break; case '?': fprintf( stderr, "See --help for a list of options.\n" ); return 1; } } if ( gamescope::Process::HasCapSysNice() ) { gamescope::Process::SetNice( -20 ); if ( g_bRt ) gamescope::Process::SetRealtime(); } else { fprintf( stderr, "No CAP_SYS_NICE, falling back to regular-priority compute and threads.\nPerformance will be affected.\n" ); } #if 0 while( !IsInDebugSession() ) { usleep( 100 ); } #endif gamescope::Process::RaiseFdLimit(); if ( gpuvis_trace_init() != -1 ) { fprintf( stderr, "Tracing is enabled\n"); } { gamescope::CScriptScopedLock script; script.Manager().RunDefaultScripts(); } XInitThreads(); g_mainThread = pthread_self(); g_pOriginalDisplay = getenv("DISPLAY"); g_pOriginalWaylandDisplay = getenv("WAYLAND_DISPLAY"); if ( eCurrentBackend == gamescope::GamescopeBackend::Auto ) { if ( g_pOriginalWaylandDisplay != NULL ) eCurrentBackend = gamescope::GamescopeBackend::Wayland; else if ( g_pOriginalDisplay != NULL ) eCurrentBackend = gamescope::GamescopeBackend::SDL; else eCurrentBackend = gamescope::GamescopeBackend::DRM; } if ( g_pOriginalWaylandDisplay != NULL ) { if (CheckWaylandPresentationTime()) { // Default to SDL_VIDEODRIVER wayland under Wayland and force enable vk_khr_present_wait // (not enabled by default in Mesa because instance does not know if Wayland // compositor supports wp_presentation, but we can check that ourselves.) setenv("vk_khr_present_wait", "true", 0); setenv("SDL_VIDEODRIVER", "wayland", 0); } else { fprintf(stderr, "Your Wayland compositor does NOT support wp_presentation/presentation-time which is required for VK_KHR_present_wait and VK_KHR_present_id.\n" "Please complain to your compositor vendor for support. Falling back to X11 window with less accurate present wait.\n"); setenv("SDL_VIDEODRIVER", "x11", 1); } } g_ForcedNV12ColorSpace = parse_colorspace_string( getenv( "GAMESCOPE_NV12_COLORSPACE" ) ); switch ( eCurrentBackend ) { #if HAVE_DRM case gamescope::GamescopeBackend::DRM: gamescope::IBackend::Set(); break; #endif #if HAVE_SDL2 case gamescope::GamescopeBackend::SDL: gamescope::IBackend::Set(); break; #endif #if HAVE_OPENVR case gamescope::GamescopeBackend::OpenVR: gamescope::IBackend::Set(); break; #endif case gamescope::GamescopeBackend::Headless: gamescope::IBackend::Set(); break; case gamescope::GamescopeBackend::Wayland: gamescope::IBackend::Set(); #if HAVE_SDL2 if ( !GetBackend() ) gamescope::IBackend::Set(); #endif break; default: abort(); } if ( !GetBackend() ) { fprintf( stderr, "Failed to create backend.\n" ); return 1; } UpdateCompatEnvVars(); if ( !vulkan_init_formats() ) { fprintf( stderr, "vulkan_init_formats failed\n" ); return 1; } if ( !vulkan_make_output() ) { fprintf( stderr, "vulkan_make_output failed\n" ); return 1; } // Prevent our clients from connecting to the parent compositor unsetenv("WAYLAND_DISPLAY"); // If DRM format modifiers aren't supported, prevent our clients from using // DCC, as this can cause tiling artifacts. if ( !vulkan_supports_modifiers() ) { const char *pchR600Debug = getenv( "R600_DEBUG" ); if ( pchR600Debug == nullptr ) { setenv( "R600_DEBUG", "nodcc", 1 ); } else if ( strstr( pchR600Debug, "nodcc" ) == nullptr ) { std::string strPreviousR600Debug = pchR600Debug; strPreviousR600Debug.append( ",nodcc" ); setenv( "R600_DEBUG", strPreviousR600Debug.c_str(), 1 ); } } if ( g_nNestedHeight == 0 ) { if ( g_nNestedWidth != 0 ) { fprintf( stderr, "Cannot specify -w without -h\n" ); return 1; } g_nNestedWidth = g_nOutputWidth; g_nNestedHeight = g_nOutputHeight; } if ( g_nNestedWidth == 0 ) g_nNestedWidth = g_nNestedHeight * 16 / 9; if ( !wlserver_init() ) { fprintf( stderr, "Failed to initialize wlserver\n" ); return 1; } gamescope_xwayland_server_t *base_server = wlserver_get_xwayland_server(0); setenv("DISPLAY", base_server->get_nested_display_name(), 1); if ( g_bExposeWayland ) setenv("XDG_SESSION_TYPE", "wayland", 1); else setenv("XDG_SESSION_TYPE", "x11", 1); setenv("XDG_CURRENT_DESKTOP", "gamescope", 1); if (g_nXWaylandCount > 1) { for (int i = 1; i < g_nXWaylandCount; i++) { char env_name[64]; snprintf(env_name, sizeof(env_name), "STEAM_GAME_DISPLAY_%d", i - 1); gamescope_xwayland_server_t *server = wlserver_get_xwayland_server(i); setenv(env_name, server->get_nested_display_name(), 1); } } else { setenv("STEAM_GAME_DISPLAY_0", base_server->get_nested_display_name(), 1); } setenv("GAMESCOPE_WAYLAND_DISPLAY", wlserver_get_wl_display_name(), 1); if ( g_bExposeWayland ) setenv("WAYLAND_DISPLAY", wlserver_get_wl_display_name(), 1); #if HAVE_PIPEWIRE if ( !init_pipewire() ) { fprintf( stderr, "Warning: failed to setup PipeWire, screen capture won't be available\n" ); } #endif std::thread steamCompMgrThread( steamCompMgrThreadRun, argc, argv ); handle_signal_action.sa_handler = handle_signal; sigaction(SIGHUP, &handle_signal_action, nullptr); sigaction(SIGINT, &handle_signal_action, nullptr); sigaction(SIGQUIT, &handle_signal_action, nullptr); sigaction(SIGTERM, &handle_signal_action, nullptr); sigaction(SIGUSR1, &handle_signal_action, nullptr); sigaction(SIGUSR2, &handle_signal_action, nullptr); wlserver_run(); steamCompMgrThread.join(); gamescope::Process::KillAllChildren( getpid(), SIGTERM ); gamescope::Process::WaitForAllChildren(); } static void steamCompMgrThreadRun(int argc, char **argv) { pthread_setname_np( pthread_self(), "gamescope-xwm" ); steamcompmgr_main( argc, argv ); pthread_kill( g_mainThread, SIGINT ); } ValveSoftware-gamescope-eb620ab/src/main.hpp000066400000000000000000000030611502457270500211610ustar00rootroot00000000000000#pragma once #include #include extern const char *gamescope_optstring; extern const struct option *gamescope_options; extern std::atomic< bool > g_bRun; extern int g_nNestedWidth; extern int g_nNestedHeight; extern int g_nNestedRefresh; // mHz extern int g_nNestedUnfocusedRefresh; // mHz extern int g_nNestedDisplayIndex; extern uint32_t g_nOutputWidth; extern uint32_t g_nOutputHeight; extern bool g_bForceRelativeMouse; extern int g_nOutputRefresh; // mHz extern bool g_bOutputHDREnabled; extern bool g_bForceInternal; extern bool g_bFullscreen; extern bool g_bGrabbed; extern float g_mouseSensitivity; extern const char *g_sOutputName; enum class GamescopeUpscaleFilter : uint32_t { LINEAR = 0, NEAREST, FSR, NIS, PIXEL, FROM_VIEW = 0xF, // internal }; static constexpr bool DoesHardwareSupportUpscaleFilter( GamescopeUpscaleFilter eFilter ) { // Could do nearest someday... AMDGPU DC supports custom tap placement to an extent. return eFilter == GamescopeUpscaleFilter::LINEAR; } enum class GamescopeUpscaleScaler : uint32_t { AUTO, INTEGER, FIT, FILL, STRETCH, }; extern GamescopeUpscaleFilter g_upscaleFilter; extern GamescopeUpscaleScaler g_upscaleScaler; extern GamescopeUpscaleFilter g_wantedUpscaleFilter; extern GamescopeUpscaleScaler g_wantedUpscaleScaler; extern int g_upscaleFilterSharpness; extern bool g_bBorderlessOutputWindow; extern bool g_bExposeWayland; extern bool g_bRt; extern int g_nXWaylandCount; extern uint32_t g_preferVendorID; extern uint32_t g_preferDeviceID; ValveSoftware-gamescope-eb620ab/src/mangoapp.cpp000066400000000000000000000062241502457270500220360ustar00rootroot00000000000000#include #include #include #include #include "steamcompmgr.hpp" #include "refresh_rate.h" #include "main.hpp" static bool inited = false; static int msgid = 0; extern bool g_bAppWantsHDRCached; extern uint32_t g_focusedBaseAppId; struct mangoapp_msg_header { long msg_type; // Message queue ID, never change uint32_t version; // for major changes in the way things work // } __attribute__((packed)); struct mangoapp_msg_v1 { struct mangoapp_msg_header hdr; uint32_t pid; uint64_t app_frametime_ns; uint8_t fsrUpscale; uint8_t fsrSharpness; uint64_t visible_frametime_ns; uint64_t latency_ns; uint32_t outputWidth; uint32_t outputHeight; uint16_t displayRefresh; bool bAppWantsHDR : 1; bool bSteamFocused : 1; char engineName[40]; // WARNING: Always ADD fields, never remove or repurpose fields } __attribute__((packed)) mangoapp_msg_v1; void init_mangoapp(){ int key = ftok("mangoapp", 65); msgid = msgget(key, 0666 | IPC_CREAT); mangoapp_msg_v1.hdr.msg_type = 1; mangoapp_msg_v1.hdr.version = 1; mangoapp_msg_v1.fsrUpscale = 0; mangoapp_msg_v1.fsrSharpness = 0; inited = true; } void mangoapp_update( uint64_t visible_frametime, uint64_t app_frametime_ns, uint64_t latency_ns ) { if (!inited) init_mangoapp(); mangoapp_msg_v1.visible_frametime_ns = visible_frametime; mangoapp_msg_v1.fsrUpscale = g_bFSRActive; mangoapp_msg_v1.fsrSharpness = g_upscaleFilterSharpness; mangoapp_msg_v1.app_frametime_ns = app_frametime_ns; mangoapp_msg_v1.latency_ns = latency_ns; mangoapp_msg_v1.pid = focusWindow_pid; mangoapp_msg_v1.outputWidth = g_nOutputWidth; mangoapp_msg_v1.outputHeight = g_nOutputHeight; mangoapp_msg_v1.displayRefresh = (uint16_t) gamescope::ConvertmHzToHz( g_nOutputRefresh ); mangoapp_msg_v1.bAppWantsHDR = g_bAppWantsHDRCached; mangoapp_msg_v1.bSteamFocused = g_focusedBaseAppId == 769; memset(mangoapp_msg_v1.engineName, 0, sizeof(mangoapp_msg_v1.engineName)); if (focusWindow_engine) focusWindow_engine->copy(mangoapp_msg_v1.engineName, sizeof(mangoapp_msg_v1.engineName) / sizeof(char)); else std::string("gamescope").copy(mangoapp_msg_v1.engineName, sizeof(mangoapp_msg_v1.engineName) / sizeof(char)); msgsnd(msgid, &mangoapp_msg_v1, sizeof(mangoapp_msg_v1) - sizeof(mangoapp_msg_v1.hdr.msg_type), IPC_NOWAIT); } extern uint64_t g_uCurrentBasePlaneCommitID; extern bool g_bCurrentBasePlaneIsFifo; void mangoapp_output_update( uint64_t vblanktime ) { if ( !g_bCurrentBasePlaneIsFifo ) { return; } static uint64_t s_uLastBasePlaneCommitID = 0; if ( s_uLastBasePlaneCommitID != g_uCurrentBasePlaneCommitID ) { static uint64_t s_uLastBasePlaneUpdateVBlankTime = vblanktime; uint64_t last_frametime = s_uLastBasePlaneUpdateVBlankTime; uint64_t frametime = vblanktime - last_frametime; s_uLastBasePlaneUpdateVBlankTime = vblanktime; s_uLastBasePlaneCommitID = g_uCurrentBasePlaneCommitID; if ( last_frametime > vblanktime ) return; mangoapp_update( frametime, uint64_t(~0ull), uint64_t(~0ull) ); } } ValveSoftware-gamescope-eb620ab/src/meson.build000066400000000000000000000167601502457270500217000ustar00rootroot00000000000000dep_xdamage = dependency('xdamage') dep_xcomposite = dependency('xcomposite') dep_xcursor = dependency('xcursor') dep_xrender = dependency('xrender') dep_xext = dependency('xext') dep_xfixes = dependency('xfixes') dep_xxf86vm = dependency('xxf86vm') dep_xtst = dependency('xtst') dep_xres = dependency('xres') dep_xmu = dependency('xmu') dep_xi = dependency('xi') drm_dep = dependency('libdrm', version: '>= 2.4.113', required: get_option('drm_backend')) eis_dep = dependency('libeis-1.0', required : get_option('input_emulation')) libsystemd_dep = dependency('libsystemd', required : false) wayland_server = dependency('wayland-server', version: '>=1.21') wayland_protos = dependency('wayland-protocols', version: '>=1.17') xkbcommon = dependency('xkbcommon') thread_dep = dependency('threads') cap_dep = dependency('libcap', required: get_option('rt_cap')) epoll_dep = dependency('epoll-shim', required: false) sdl2_dep = dependency('SDL2', required: get_option('sdl2_backend')) avif_dep = dependency('libavif', version: '>=1.0.0', required: get_option('avif_screenshots')) wlroots_dep = dependency( 'wlroots', version: ['>= 0.18.0', '< 0.19.0'], fallback: ['wlroots', 'wlroots'], default_options: ['default_library=static', 'examples=false', 'xwayland=enabled', 'backends=libinput', 'renderers=[]', 'allocators=[]', 'session=enabled'], ) displayinfo_dep = dependency( 'libdisplay-info', version: ['>= 0.0.0', '< 0.3.0'], fallback: ['libdisplay-info', 'di_dep'], default_options: ['default_library=static'], ) libdecor_dep = dependency('libdecor-0') glsl_compiler = find_program('glslang', 'glslangValidator', native: true) # Use --depfile to rebuild shaders when included files have changed. Sadly debian based # distros don't have up-to-date glslang so we need to check for support first. if run_command(glsl_compiler, ['--version', '--depfile', 'dummy.dep'], check: false).returncode() == 0 glsl_generator = generator( glsl_compiler, output : ['@BASENAME@.h'], arguments : ['-V', '@INPUT@', '--vn', '@BASENAME@', '-o', '@OUTPUT@', '--depfile', '@DEPFILE@', '--quiet'], depfile : '@BASENAME@.h.d', ) else glsl_generator = generator( glsl_compiler, output : ['@BASENAME@.h'], arguments : ['-V', '@INPUT@', '--vn', '@BASENAME@', '-o', '@OUTPUT@'], ) endif shader_src = [ 'shaders/cs_composite_blit.comp', 'shaders/cs_composite_blur.comp', 'shaders/cs_composite_blur_cond.comp', 'shaders/cs_composite_rcas.comp', 'shaders/cs_easu.comp', 'shaders/cs_easu_fp16.comp', 'shaders/cs_gaussian_blur_horizontal.comp', 'shaders/cs_nis.comp', 'shaders/cs_nis_fp16.comp', 'shaders/cs_rgb_to_nv12.comp', ] spirv_shaders = glsl_generator.process(shader_src) reshade_src = [ 'reshade/source/effect_codegen_spirv.cpp', 'reshade/source/effect_expression.cpp', 'reshade/source/effect_lexer.cpp', 'reshade/source/effect_parser_exp.cpp', 'reshade/source/effect_parser_stmt.cpp', 'reshade/source/effect_preprocessor.cpp', 'reshade/source/effect_symbol_table.cpp', ] reshade_include = include_directories([ 'reshade/source', 'reshade/include', '../thirdparty/SPIRV-Headers/include/spirv/unified1' ]) sol2_include = include_directories(['../thirdparty']) required_wlroots_features = ['xwayland'] src = [ 'Backends/HeadlessBackend.cpp', 'Backends/WaylandBackend.cpp', 'Utils/TempFiles.cpp', 'Utils/Version.cpp', 'Utils/Process.cpp', 'Script/Script.cpp', 'BufferMemo.cpp', 'steamcompmgr.cpp', 'convar.cpp', 'commit.cpp', 'color_helpers.cpp', 'main.cpp', 'edid.cpp', 'wlserver.cpp', 'vblankmanager.cpp', 'rendervulkan.cpp', 'log.cpp', 'ime.cpp', 'mangoapp.cpp', 'Timeline.cpp', 'reshade_effect_manager.cpp', 'backend.cpp', 'x11cursor.cpp', 'InputEmulation.cpp', 'LibInputHandler.cpp', ] luajit_dep = dependency( 'luajit' ) libinput_dep = dependency('libinput', required: true) gamescope_cpp_args = [] if drm_dep.found() src += 'Backends/DRMBackend.cpp' src += 'modegen.cpp' required_wlroots_features += 'libinput_backend' liftoff_dep = dependency( 'libliftoff', version: ['>= 0.5.0', '< 0.6.0'], fallback: ['libliftoff', 'liftoff'], default_options: ['default_library=static'], ) else liftoff_dep = dependency('', required: false) endif if sdl2_dep.found() src += 'Backends/SDLBackend.cpp' endif gamescope_cpp_args += '-DHAVE_DRM=@0@'.format(drm_dep.found().to_int()) gamescope_cpp_args += '-DHAVE_SDL2=@0@'.format(sdl2_dep.found().to_int()) gamescope_cpp_args += '-DHAVE_AVIF=@0@'.format(avif_dep.found().to_int()) gamescope_cpp_args += '-DHAVE_LIBCAP=@0@'.format(cap_dep.found().to_int()) gamescope_cpp_args += '-DHAVE_LIBEIS=@0@'.format(eis_dep.found().to_int()) gamescope_cpp_args += '-DHAVE_LIBSYSTEMD=@0@'.format(libsystemd_dep.found().to_int()) gamescope_cpp_args += '-DHAVE_SCRIPTING=1' src += spirv_shaders src += protocols_server_src src += protocols_client_src if pipewire_dep.found() src += 'pipewire.cpp' endif if openvr_dep.found() src += 'Backends/OpenVRBackend.cpp' endif foreach feat : required_wlroots_features if wlroots_dep.get_variable('have_' + feat) != 'true' error('Cannot use wlroots built without ' + feat + ' support') endif endforeach cc = meson.get_compiler('c') compiler_name = cc.get_id() compiler_version = cc.version() vcs_tag_cmd = ['git', 'describe', '--always', '--tags', '--dirty=+'] vcs_tag = run_command(vcs_tag_cmd, check: false).stdout().strip() version_tag = vcs_tag + ' (' + compiler_name + ' ' + compiler_version + ')' gamescope_version_conf = configuration_data() gamescope_version_conf.set('VCS_TAG', version_tag) gamescope_version = configure_file( input : 'GamescopeVersion.h.in', output : 'GamescopeVersion.h', configuration : gamescope_version_conf ) executable( 'gamescope', src, reshade_src, gamescope_version, include_directories : [reshade_include, sol2_include], dependencies: [ dep_wayland, dep_x11, dep_xdamage, dep_xcomposite, dep_xrender, dep_xext, dep_xfixes, dep_xxf86vm, dep_xres, glm_dep, drm_dep, wayland_server, xkbcommon, thread_dep, sdl2_dep, wlroots_dep, vulkan_dep, liftoff_dep, dep_xtst, dep_xmu, cap_dep, epoll_dep, pipewire_dep, librt_dep, stb_dep, displayinfo_dep, openvr_dep, dep_xcursor, avif_dep, dep_xi, libdecor_dep, eis_dep, luajit_dep, libinput_dep, libsystemd_dep, ], install: true, cpp_args: gamescope_cpp_args, ) gamescope_core_src = [ 'convar.cpp', 'log.cpp', 'Utils/Process.cpp', 'Utils/Version.cpp', ] if pipewire_dep.found() executable( 'gamescopestream', ['Apps/gamescopestream.cpp'], gamescope_core_src, gamescope_version, protocols_client_src, dependencies: [ pipewire_dep, dep_wayland, libdecor_dep ], install: true ) endif executable('gamescopereaper', ['Apps/gamescopereaper.cpp', gamescope_core_src], gamescope_version, install:true ) benchmark_dep = dependency('benchmark', required: get_option('benchmark'), disabler: true) executable('gamescope_color_microbench', ['color_bench.cpp', 'color_helpers.cpp'], gamescope_core_src, gamescope_version, dependencies:[benchmark_dep, glm_dep]) executable('gamescope_color_tests', ['color_tests.cpp', 'color_helpers.cpp'], gamescope_core_src, gamescope_version, dependencies:[glm_dep]) executable('gamescopectl', ['Apps/gamescopectl.cpp'], gamescope_core_src, gamescope_version, protocols_client_src, dependencies: [dep_wayland], install:true ) executable('gamescope_hotkey_example', ['Apps/gamescope_hotkey_example.cpp'], gamescope_core_src, gamescope_version, protocols_client_src, dependencies: [dep_wayland, xkbcommon], install: false ) ValveSoftware-gamescope-eb620ab/src/messagey.h000066400000000000000000000245651502457270500215260ustar00rootroot00000000000000// Code adapted from: // https://github.com/libsdl-org/SDL/blob/main/src/video/wayland/SDL_waylandmessagebox.c // which is licensed under Z-Lib // as follows: // // Simple DirectMedia Layer // Copyright (C) 1997-2022 Sam Lantinga // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. // Permission is granted to anyone to use this software for any purpose, // including commercial applications, and to alter it and redistribute it // freely, subject to the following restrictions: // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgment in the product documentation would be // appreciated but is not required. // 2. Altered source versions must be plainly marked as such, and must not be // misrepresented as being the original software. // 3. This notice may not be removed or altered from any source distribution. #pragma once #include #include #include #include #include #include #include namespace messagey { static constexpr int MaxButtons = 8; namespace MessageBoxFlag { static constexpr uint32_t Error = (1u << 0); static constexpr uint32_t Warning = (1u << 1); static constexpr uint32_t Information = (1u << 2); static constexpr uint32_t ButtonsLeftToRight = (1u << 3); static constexpr uint32_t ButtonsRightToLeft = (1u << 4); static constexpr uint32_t Simple_OK = (1u << 5); static constexpr uint32_t Simple_Cancel = (1u << 6); } using MessageBoxFlags = uint32_t; namespace MessageBoxButtonFlag { static constexpr uint32_t ReturnKeyDefault = (1u << 0); static constexpr uint32_t EscapeKeyDefault = (1u << 1); } using MessageBoxButtonFlags = uint32_t; struct MessageBoxButtonData { uint32_t flags; int buttonid; const char* text; }; struct MessageBoxData { uint32_t flags; const char* title; const char* message; int numbuttons; const MessageBoxButtonData *buttons; }; struct ErrorData { bool valid = false; char str[256]; }; inline ErrorData* GetErrBuf() { static thread_local ErrorData err; return &err; } inline ErrorData* GetError() { ErrorData *err = GetErrBuf(); if (!err->valid) return nullptr; return err; } inline int SetError(const char *fmt, ...) { if (fmt != nullptr) { va_list ap; ErrorData *error = GetErrBuf(); error->valid = true; va_start(ap, fmt); vsnprintf(error->str, sizeof(ErrorData::str), fmt, ap); va_end(ap); } return -1; } inline int Show(const MessageBoxData *messageboxdata, int *buttonid) { int fd_pipe[2]; /* fd_pipe[0]: read end of pipe, fd_pipe[1]: write end of pipe */ pid_t pid1; const char *zenityDisableEnv = getenv("GAMESCOPE_ZENITY_DISABLE"); if (zenityDisableEnv && *zenityDisableEnv && atoi(zenityDisableEnv) != 0) return 0; if (messageboxdata->numbuttons > MaxButtons) { return SetError("Too many buttons (%d max allowed)", MaxButtons); } if (pipe(fd_pipe) != 0) { /* create a pipe */ return SetError("pipe() failed: %s", strerror(errno)); } pid1 = fork(); if (pid1 == 0) { /* child process */ int argc = 5, i; const char* argv[5 + 2/* icon name */ + 2/* title */ + 2/* message */ + 2*MaxButtons + 1/* nullptr */] = { "zenity", "--question", "--switch", "--no-wrap", "--no-markup" }; close(fd_pipe[0]); /* no reading from pipe */ /* write stdout in pipe */ if (dup2(fd_pipe[1], STDOUT_FILENO) == -1) { _exit(128); } argv[argc++] = "--icon-name"; if (messageboxdata->flags & MessageBoxFlag::Error) argv[argc++] = "dialog-error"; else if (messageboxdata->flags & MessageBoxFlag::Warning) argv[argc++] = "dialog-warning"; else if (messageboxdata->flags & MessageBoxFlag::Information) argv[argc++] = "dialog-information"; if (messageboxdata->title && messageboxdata->title[0]) { argv[argc++] = "--title"; argv[argc++] = messageboxdata->title; } else { argv[argc++] = "--title=\"\""; } if (messageboxdata->message && messageboxdata->message[0]) { argv[argc++] = "--text"; argv[argc++] = messageboxdata->message; } else { argv[argc++] = "--text=\"\""; } for (i = 0; i < messageboxdata->numbuttons; ++i) { if (messageboxdata->buttons[i].text && messageboxdata->buttons[i].text[0]) { argv[argc++] = "--extra-button"; argv[argc++] = messageboxdata->buttons[i].text; } else { argv[argc++] = "--extra-button=\"\""; } } argv[argc] = nullptr; /* const casting argv is fine: * https://pubs.opengroup.org/onlinepubs/9699919799/functions/fexecve.html -> rational */ execvp("zenity", (char **)argv); _exit(129); } else if (pid1 < 0) { close(fd_pipe[0]); close(fd_pipe[1]); return SetError("fork() failed: %s", strerror(errno)); } else { int status; if (waitpid(pid1, &status, 0) == pid1) { if (WIFEXITED(status)) { if (WEXITSTATUS(status) < 128) { int i; size_t output_len = 1; char* output = nullptr; char* tmp = nullptr; FILE* stdout = nullptr; close(fd_pipe[1]); /* no writing to pipe */ /* At this point, if no button ID is needed, we can just bail as soon as the * process has completed. */ if (buttonid == NULL) { close(fd_pipe[0]); return 0; } *buttonid = -1; /* find button with longest text */ for (i = 0; i < messageboxdata->numbuttons; ++i) { if (messageboxdata->buttons[i].text != NULL) { const size_t button_len = strlen(messageboxdata->buttons[i].text); if (button_len > output_len) { output_len = button_len; } } } output = (char *)malloc(output_len + 1); if (!output) { close(fd_pipe[0]); return SetError("Out of memory"); } output[0] = '\0'; stdout = fdopen(fd_pipe[0], "r"); if (!stdout) { free(output); close(fd_pipe[0]); return SetError("Couldn't open pipe for reading: %s", strerror(errno)); } tmp = fgets(output, output_len + 1, stdout); fclose(stdout); if ((tmp == NULL) || (*tmp == '\0') || (*tmp == '\n')) { free(output); return 0; /* User simply closed the dialog */ } /* It likes to add a newline... */ tmp = strrchr(output, '\n'); if (tmp != NULL) { *tmp = '\0'; } /* Check which button got pressed */ for (i = 0; i < messageboxdata->numbuttons; i += 1) { if (messageboxdata->buttons[i].text != NULL) { if (strcmp(output, messageboxdata->buttons[i].text) == 0) { *buttonid = messageboxdata->buttons[i].buttonid; break; } } } free(output); return 0; /* success! */ } else { return SetError("zenity reported error or failed to launch: %d", WEXITSTATUS(status)); } } else { return SetError("zenity failed for some reason"); } } else { return SetError("Waiting on zenity failed: %s", strerror(errno)); } } } inline int ShowSimple(const char *message, const char *caption, MessageBoxFlags flags, int *buttonid) { int buttonCount = 0; MessageBoxButtonData buttons[2]; bool hasCancel = !!(flags & MessageBoxFlag::Simple_Cancel); bool hasOK = !!(flags & MessageBoxFlag::Simple_OK); if (hasCancel) { buttons[buttonCount++] = { .flags = MessageBoxButtonFlag::EscapeKeyDefault | (hasOK ? 0 : MessageBoxButtonFlag::ReturnKeyDefault), .buttonid = 0, .text = "Cancel", }; } if (hasOK) { buttons[buttonCount++] = { .flags = MessageBoxButtonFlag::ReturnKeyDefault, .buttonid = 1, .text = "OK", }; } MessageBoxData data = { .flags = flags, .title = caption, .message = message, .numbuttons = buttonCount, .buttons = buttons, }; return Show(&data, buttonid); } } ValveSoftware-gamescope-eb620ab/src/modegen.cpp000066400000000000000000000212151502457270500216470ustar00rootroot00000000000000/* * Copyright 2005-2006 Luc Verhaegen. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE COPYRIGHT HOLDER(S) OR AUTHOR(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ #include "modegen.hpp" /* top/bottom margin size (% of height) - default: 1.8 */ #define CVT_MARGIN_PERCENTAGE 1.8 /* character cell horizontal granularity (pixels) - default 8 */ #define CVT_H_GRANULARITY 8 /* Minimum vertical porch (lines) - default 3 */ #define CVT_MIN_V_PORCH 3 /* Minimum number of vertical back porch lines - default 6 */ #define CVT_MIN_V_BPORCH 6 /* Pixel clock step (kHz) */ #define CVT_CLOCK_STEP 250 /* Minimum time of vertical sync + back porch interval (µs) * default 550.0 */ #define CVT_MIN_VSYNC_BP 550.0 /* Nominal hsync width (% of line period) - default 8 */ #define CVT_HSYNC_PERCENTAGE 8 /* Definition of Horizontal blanking time limitation */ /* Gradient (%/kHz) - default 600 */ #define CVT_M_FACTOR 600 /* Offset (%) - default 40 */ #define CVT_C_FACTOR 40 /* Blanking time scaling factor - default 128 */ #define CVT_K_FACTOR 128 /* Scaling factor weighting - default 20 */ #define CVT_J_FACTOR 20 #define CVT_M_PRIME CVT_M_FACTOR * CVT_K_FACTOR / 256 #define CVT_C_PRIME (CVT_C_FACTOR - CVT_J_FACTOR) * CVT_K_FACTOR / 256 + \ CVT_J_FACTOR /* Minimum vertical blanking interval time (µs) - default 460 */ #define CVT_RB_MIN_VBLANK 460.0 /* Fixed number of clocks for horizontal sync */ #define CVT_RB_H_SYNC 32.0 /* Fixed number of clocks for horizontal blanking */ #define CVT_RB_H_BLANK 160.0 /* Fixed number of lines for vertical front porch - default 3 */ #define CVT_RB_VFPORCH 3 /* * Generate a CVT standard mode from hdisplay, vdisplay and vrefresh. * * These calculations are stolen from the CVT calculation spreadsheet written * by Graham Loveridge. He seems to be claiming no copyright and there seems to * be no license attached to this. He apparently just wants to see his name * mentioned. * * This file can be found at http://www.vesa.org/Public/CVT/CVTd6r1.xls * * Comments and structure corresponds to the comments and structure of the xls. * This should ease importing of future changes to the standard (not very * likely though). * * This function is borrowed from xorg-xserver's xf86CVTmode. */ void generate_cvt_mode(drmModeModeInfo *mode, int hdisplay, int vdisplay, float vrefresh, bool reduced, bool interlaced) { bool margins = false; float vfield_rate, hperiod; int hdisplay_rnd, hmargin; int vdisplay_rnd, vmargin, vsync; float interlace; /* Please rename this */ /* CVT default is 60.0Hz */ if (!vrefresh) { vrefresh = 60.0; } /* 1. Required field rate */ if (interlaced) { vfield_rate = vrefresh * 2; } else { vfield_rate = vrefresh; } /* 2. Horizontal pixels */ hdisplay_rnd = hdisplay - (hdisplay % CVT_H_GRANULARITY); /* 3. Determine left and right borders */ if (margins) { /* right margin is actually exactly the same as left */ hmargin = (((float) hdisplay_rnd) * CVT_MARGIN_PERCENTAGE / 100.0); hmargin -= hmargin % CVT_H_GRANULARITY; } else { hmargin = 0; } /* 4. Find total active pixels */ mode->hdisplay = hdisplay_rnd + 2 * hmargin; /* 5. Find number of lines per field */ if (interlaced) { vdisplay_rnd = vdisplay / 2; } else { vdisplay_rnd = vdisplay; } /* 6. Find top and bottom margins */ /* nope. */ if (margins) { /* top and bottom margins are equal again. */ vmargin = (((float) vdisplay_rnd) * CVT_MARGIN_PERCENTAGE / 100.0); } else { vmargin = 0; } mode->vdisplay = vdisplay + 2 * vmargin; /* 7. interlace */ if (interlaced) { interlace = 0.5; } else { interlace = 0.0; } /* Determine vsync Width from aspect ratio */ if (!(vdisplay % 3) && ((vdisplay * 4 / 3) == hdisplay)) { vsync = 4; } else if (!(vdisplay % 9) && ((vdisplay * 16 / 9) == hdisplay)) { vsync = 5; } else if (!(vdisplay % 10) && ((vdisplay * 16 / 10) == hdisplay)) { vsync = 6; } else if (!(vdisplay % 4) && ((vdisplay * 5 / 4) == hdisplay)) { vsync = 7; } else if (!(vdisplay % 9) && ((vdisplay * 15 / 9) == hdisplay)) { vsync = 7; } else { /* Custom */ vsync = 10; } if (!reduced) { /* simplified GTF calculation */ float hblank_percentage; int vsync_and_back_porch, vblank_porch; int hblank; /* 8. Estimated Horizontal period */ hperiod = ((float) (1000000.0 / vfield_rate - CVT_MIN_VSYNC_BP)) / (vdisplay_rnd + 2 * vmargin + CVT_MIN_V_PORCH + interlace); /* 9. Find number of lines in sync + backporch */ if (((int) (CVT_MIN_VSYNC_BP / hperiod) + 1) < (vsync + CVT_MIN_V_PORCH)) { vsync_and_back_porch = vsync + CVT_MIN_V_PORCH; } else { vsync_and_back_porch = (int) (CVT_MIN_VSYNC_BP / hperiod) + 1; } /* 10. Find number of lines in back porch */ vblank_porch = vsync_and_back_porch - vsync; (void) vblank_porch; /* 11. Find total number of lines in vertical field */ mode->vtotal = vdisplay_rnd + 2 * vmargin + vsync_and_back_porch + interlace + CVT_MIN_V_PORCH; /* 12. Find ideal blanking duty cycle from formula */ hblank_percentage = CVT_C_PRIME - CVT_M_PRIME * hperiod / 1000.0; /* 13. Blanking time */ if (hblank_percentage < 20) { hblank_percentage = 20; } hblank = mode->hdisplay * hblank_percentage / (100.0 - hblank_percentage); hblank -= hblank % (2 * CVT_H_GRANULARITY); /* 14. Find total number of pixels in a line. */ mode->htotal = mode->hdisplay + hblank; /* Fill in hsync values */ mode->hsync_end = mode->hdisplay + hblank / 2; mode->hsync_start = mode->hsync_end - (mode->htotal * CVT_HSYNC_PERCENTAGE) / 100; mode->hsync_start += CVT_H_GRANULARITY - mode->hsync_start % CVT_H_GRANULARITY; /* Fill in vsync values */ mode->vsync_start = mode->vdisplay + CVT_MIN_V_PORCH; mode->vsync_end = mode->vsync_start + vsync; } else { /* reduced blanking */ int vbi_lines; /* 8. Estimate Horizontal period. */ hperiod = ((float) (1000000.0 / vfield_rate - CVT_RB_MIN_VBLANK)) / (vdisplay_rnd + 2 * vmargin); /* 9. Find number of lines in vertical blanking */ vbi_lines = ((float) CVT_RB_MIN_VBLANK) / hperiod + 1; /* 10. Check if vertical blanking is sufficient */ if (vbi_lines < (CVT_RB_VFPORCH + vsync + CVT_MIN_V_BPORCH)) { vbi_lines = CVT_RB_VFPORCH + vsync + CVT_MIN_V_BPORCH; } /* 11. Find total number of lines in vertical field */ mode->vtotal = vdisplay_rnd + 2 * vmargin + interlace + vbi_lines; /* 12. Find total number of pixels in a line */ mode->htotal = mode->hdisplay + CVT_RB_H_BLANK; /* Fill in hsync values */ mode->hsync_end = mode->hdisplay + CVT_RB_H_BLANK / 2; mode->hsync_start = mode->hsync_end - CVT_RB_H_SYNC; /* Fill in vsync values */ mode->vsync_start = mode->vdisplay + CVT_RB_VFPORCH; mode->vsync_end = mode->vsync_start + vsync; } /* 15/13. Find pixel clock frequency (kHz for xf86) */ mode->clock = mode->htotal * 1000.0 / hperiod; mode->clock -= mode->clock % CVT_CLOCK_STEP; /* 17/15. Find actual Field rate */ mode->vrefresh = (1000.0 * ((float) mode->clock)) / ((float) (mode->htotal * mode->vtotal)); /* 18/16. Find actual vertical frame frequency */ /* ignore - just set the mode flag for interlaced */ if (interlaced) { mode->vtotal *= 2; mode->flags |= DRM_MODE_FLAG_INTERLACE; } snprintf(mode->name, sizeof(mode->name), "%dx%d", hdisplay, vdisplay); if (reduced) { mode->flags |= DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NVSYNC; } else { mode->flags |= DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_PVSYNC; } } void generate_fixed_mode(drmModeModeInfo *mode, const drmModeModeInfo *base, int vrefresh ) { *mode = *base; if (!vrefresh) vrefresh = 60; mode->clock = ( ( mode->htotal * mode->vtotal * vrefresh ) + 999 ) / 1000; mode->vrefresh = (1000 * mode->clock) / (mode->htotal * mode->vtotal); snprintf(mode->name, sizeof(mode->name), "%dx%d@%d.00", mode->hdisplay, mode->vdisplay, vrefresh); } ValveSoftware-gamescope-eb620ab/src/modegen.hpp000066400000000000000000000005571502457270500216620ustar00rootroot00000000000000#pragma once #include #include #include #include #include #include "gamescope_shared.h" void generate_cvt_mode(drmModeModeInfo *mode, int hdisplay, int vdisplay, float vrefresh, bool reduced, bool interlaced); void generate_fixed_mode(drmModeModeInfo *mode, const drmModeModeInfo *base, int vrefresh ); ValveSoftware-gamescope-eb620ab/src/pipewire.cpp000066400000000000000000000554401502457270500220640ustar00rootroot00000000000000 #include #include #include #include #include #include #include #include #include #include "main.hpp" #include "pipewire.hpp" #include "log.hpp" #include static LogScope pwr_log("pipewire"); static struct pipewire_state pipewire_state = { .stream_node_id = SPA_ID_INVALID }; static int nudgePipe[2] = { -1, -1 }; // Pending buffer for PipeWire → steamcompmgr static std::atomic out_buffer; // Pending buffer for steamcompmgr → PipeWire static std::atomic in_buffer; // Requested capture size static uint32_t s_nRequestedWidth; static uint32_t s_nRequestedHeight; static uint32_t s_nCaptureWidth; static uint32_t s_nCaptureHeight; static uint32_t s_nOutputWidth; static uint32_t s_nOutputHeight; static void destroy_buffer(struct pipewire_buffer *buffer) { assert(buffer->buffer == nullptr); switch (buffer->type) { case SPA_DATA_MemFd: { off_t size = buffer->shm.stride * buffer->video_info.size.height; if (buffer->video_info.format == SPA_VIDEO_FORMAT_NV12) { size += buffer->shm.stride * ((buffer->video_info.size.height + 1) / 2); } munmap(buffer->shm.data, size); close(buffer->shm.fd); break; } case SPA_DATA_DmaBuf: break; // nothing to do default: assert(false); // unreachable } // If out_buffer == buffer, then set it to nullptr. // We don't care about the result. struct pipewire_buffer *buffer1 = buffer; out_buffer.compare_exchange_strong(buffer1, nullptr); struct pipewire_buffer *buffer2 = buffer; in_buffer.compare_exchange_strong(buffer2, nullptr); delete buffer; } void pipewire_destroy_buffer(struct pipewire_buffer *buffer) { destroy_buffer(buffer); } static void calculate_capture_size() { s_nCaptureWidth = s_nOutputWidth; s_nCaptureHeight = s_nOutputHeight; if (s_nRequestedWidth > 0 && s_nRequestedHeight > 0 && (s_nOutputWidth > s_nRequestedWidth || s_nOutputHeight > s_nRequestedHeight)) { // Need to clamp to the smallest dimension float flRatioW = static_cast(s_nRequestedWidth) / s_nOutputWidth; float flRatioH = static_cast(s_nRequestedHeight) / s_nOutputHeight; if (flRatioW <= flRatioH) { s_nCaptureWidth = s_nRequestedWidth; s_nCaptureHeight = static_cast(ceilf(flRatioW * s_nOutputHeight)); } else { s_nCaptureWidth = static_cast(ceilf(flRatioH * s_nOutputWidth)); s_nCaptureHeight = s_nRequestedHeight; } } } static void build_format_params(struct spa_pod_builder *builder, spa_video_format format, std::vector ¶ms) { struct spa_rectangle size = SPA_RECTANGLE(s_nCaptureWidth, s_nCaptureHeight); struct spa_rectangle min_requested_size = { 0, 0 }; struct spa_rectangle max_requested_size = { UINT32_MAX, UINT32_MAX }; struct spa_fraction framerate = SPA_FRACTION(0, 1); uint64_t modifier = DRM_FORMAT_MOD_LINEAR; struct spa_pod_frame obj_frame, choice_frame; spa_pod_builder_push_object(builder, &obj_frame, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); spa_pod_builder_add(builder, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&size), SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&framerate), SPA_FORMAT_VIDEO_requested_size, SPA_POD_CHOICE_RANGE_Rectangle( &min_requested_size, &min_requested_size, &max_requested_size ), SPA_FORMAT_VIDEO_gamescope_focus_appid, SPA_POD_CHOICE_RANGE_Long( 0ll, INT64_MIN, INT64_MAX ), 0); if (format == SPA_VIDEO_FORMAT_NV12) { spa_pod_builder_add(builder, SPA_FORMAT_VIDEO_colorMatrix, SPA_POD_CHOICE_ENUM_Id(3, SPA_VIDEO_COLOR_MATRIX_BT601, SPA_VIDEO_COLOR_MATRIX_BT601, SPA_VIDEO_COLOR_MATRIX_BT709), SPA_FORMAT_VIDEO_colorRange, SPA_POD_CHOICE_ENUM_Id(3, SPA_VIDEO_COLOR_RANGE_16_235, SPA_VIDEO_COLOR_RANGE_16_235, SPA_VIDEO_COLOR_RANGE_0_255), 0); } spa_pod_builder_prop(builder, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY); spa_pod_builder_push_choice(builder, &choice_frame, SPA_CHOICE_Enum, 0); spa_pod_builder_long(builder, modifier); // default spa_pod_builder_long(builder, modifier); spa_pod_builder_pop(builder, &choice_frame); params.push_back((const struct spa_pod *) spa_pod_builder_pop(builder, &obj_frame)); spa_pod_builder_push_object(builder, &obj_frame, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); spa_pod_builder_add(builder, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&size), SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&framerate), SPA_FORMAT_VIDEO_requested_size, SPA_POD_CHOICE_RANGE_Rectangle( &min_requested_size, &min_requested_size, &max_requested_size ), SPA_FORMAT_VIDEO_gamescope_focus_appid, SPA_POD_CHOICE_RANGE_Long( 0ll, INT64_MIN, INT64_MAX ), 0); if (format == SPA_VIDEO_FORMAT_NV12) { spa_pod_builder_add(builder, SPA_FORMAT_VIDEO_colorMatrix, SPA_POD_CHOICE_ENUM_Id(3, SPA_VIDEO_COLOR_MATRIX_BT601, SPA_VIDEO_COLOR_MATRIX_BT601, SPA_VIDEO_COLOR_MATRIX_BT709), SPA_FORMAT_VIDEO_colorRange, SPA_POD_CHOICE_ENUM_Id(3, SPA_VIDEO_COLOR_RANGE_16_235, SPA_VIDEO_COLOR_RANGE_16_235, SPA_VIDEO_COLOR_RANGE_0_255), 0); } params.push_back((const struct spa_pod *) spa_pod_builder_pop(builder, &obj_frame)); // for (auto& param : params) // spa_debug_format(2, nullptr, param); } static std::vector build_format_params(struct spa_pod_builder *builder) { std::vector params; build_format_params(builder, SPA_VIDEO_FORMAT_BGRx, params); build_format_params(builder, SPA_VIDEO_FORMAT_NV12, params); return params; } static void request_buffer(struct pipewire_state *state) { struct pw_buffer *pw_buffer = pw_stream_dequeue_buffer(state->stream); if (!pw_buffer) { pwr_log.errorf("warning: out of buffers"); return; } struct pipewire_buffer *buffer = (struct pipewire_buffer *) pw_buffer->user_data; buffer->copying = true; // Past this exchange, the PipeWire thread shares the buffer with the // steamcompmgr thread struct pipewire_buffer *old = out_buffer.exchange(buffer); assert(old == nullptr); } static void copy_buffer(struct pipewire_state *state, struct pipewire_buffer *buffer) { gamescope::OwningRc &tex = buffer->texture; assert(tex != nullptr); struct pw_buffer *pw_buffer = buffer->buffer; struct spa_buffer *spa_buffer = pw_buffer->buffer; bool needs_reneg = buffer->video_info.size.width != tex->width() || buffer->video_info.size.height != tex->height(); struct spa_meta_header *header = (struct spa_meta_header *) spa_buffer_find_meta_data(spa_buffer, SPA_META_Header, sizeof(*header)); if (header != nullptr) { header->pts = -1; header->flags = needs_reneg ? SPA_META_HEADER_FLAG_CORRUPTED : 0; header->seq = state->seq++; header->dts_offset = 0; } float *requested_size_scale = (float *) spa_buffer_find_meta_data(spa_buffer, SPA_META_requested_size_scale, sizeof(*requested_size_scale)); if (requested_size_scale != nullptr) { *requested_size_scale = ((float)tex->width() / g_nOutputWidth); } struct spa_chunk *chunk = spa_buffer->datas[0].chunk; chunk->flags = needs_reneg ? SPA_CHUNK_FLAG_CORRUPTED : 0; struct wlr_dmabuf_attributes dmabuf; switch (buffer->type) { case SPA_DATA_MemFd: chunk->offset = 0; chunk->size = state->video_info.size.height * buffer->shm.stride; if (state->video_info.format == SPA_VIDEO_FORMAT_NV12) { chunk->size += ((state->video_info.size.height + 1)/2 * buffer->shm.stride); } chunk->stride = buffer->shm.stride; if (!needs_reneg) { uint8_t *pMappedData = tex->mappedData(); if (state->video_info.format == SPA_VIDEO_FORMAT_NV12) { for (uint32_t i = 0; i < tex->height(); i++) { const uint32_t lumaPwOffset = 0; memcpy( &buffer->shm.data[lumaPwOffset + i * buffer->shm.stride], &pMappedData [tex->lumaOffset() + i * tex->lumaRowPitch()], std::min(buffer->shm.stride, tex->lumaRowPitch())); } for (uint32_t i = 0; i < (tex->height() + 1) / 2; i++) { const uint32_t chromaPwOffset = tex->height() * buffer->shm.stride; memcpy( &buffer->shm.data[chromaPwOffset + i * buffer->shm.stride], &pMappedData [tex->chromaOffset() + i * tex->chromaRowPitch()], std::min(buffer->shm.stride, tex->chromaRowPitch())); } } else { for (uint32_t i = 0; i < tex->height(); i++) { memcpy( &buffer->shm.data[i * buffer->shm.stride], &pMappedData [i * tex->rowPitch()], std::min(buffer->shm.stride, tex->rowPitch())); } } } break; case SPA_DATA_DmaBuf: dmabuf = tex->dmabuf(); assert(dmabuf.n_planes == 1); chunk->offset = dmabuf.offset[0]; chunk->stride = dmabuf.stride[0]; chunk->size = dmabuf.height * chunk->stride; if (state->video_info.format == SPA_VIDEO_FORMAT_NV12) { chunk->size += ((dmabuf.height + 1)/2 * chunk->stride); } break; default: assert(false); // unreachable } } static void dispatch_nudge(struct pipewire_state *state, int fd) { while (true) { static char buf[1024]; if (read(fd, buf, sizeof(buf)) < 0) { if (errno != EAGAIN) pwr_log.errorf_errno("dispatch_nudge: read failed"); break; } } if (g_nOutputWidth != s_nOutputWidth || g_nOutputHeight != s_nOutputHeight) { s_nOutputWidth = g_nOutputWidth; s_nOutputHeight = g_nOutputHeight; calculate_capture_size(); } if (s_nCaptureWidth != state->video_info.size.width || s_nCaptureHeight != state->video_info.size.height) { pwr_log.debugf("renegotiating stream params (size: %dx%d)", s_nCaptureWidth, s_nCaptureHeight); uint8_t buf[4096]; struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(buf, sizeof(buf)); std::vector format_params = build_format_params(&builder); int ret = pw_stream_update_params(state->stream, format_params.data(), format_params.size()); if (ret < 0) { pwr_log.errorf("pw_stream_update_params failed"); } } struct pipewire_buffer *buffer = in_buffer.exchange(nullptr); if (buffer != nullptr) { // We now completely own the buffer, it's no longer shared with the // steamcompmgr thread. buffer->copying = false; if (buffer->buffer != nullptr) { copy_buffer(state, buffer); int ret = pw_stream_queue_buffer(state->stream, buffer->buffer); if (ret < 0) { pwr_log.errorf("pw_stream_queue_buffer failed"); } } else { destroy_buffer(buffer); } } } static void stream_handle_state_changed(void *data, enum pw_stream_state old_stream_state, enum pw_stream_state stream_state, const char *error) { struct pipewire_state *state = (struct pipewire_state *) data; pwr_log.infof("stream state changed: %s", pw_stream_state_as_string(stream_state)); switch (stream_state) { case PW_STREAM_STATE_PAUSED: if (state->stream_node_id == SPA_ID_INVALID) { state->stream_node_id = pw_stream_get_node_id(state->stream); } state->streaming = false; state->seq = 0; break; case PW_STREAM_STATE_STREAMING: state->streaming = true; break; case PW_STREAM_STATE_ERROR: case PW_STREAM_STATE_UNCONNECTED: state->running = false; break; default: break; } } static void stream_handle_param_changed(void *data, uint32_t id, const struct spa_pod *param) { struct pipewire_state *state = (struct pipewire_state *) data; if (param == nullptr || id != SPA_PARAM_Format) return; struct spa_gamescope gamescope_info{}; int ret = spa_format_video_raw_parse_with_gamescope(param, &state->video_info, &gamescope_info); if (ret < 0) { pwr_log.errorf("spa_format_video_raw_parse failed"); return; } s_nRequestedWidth = gamescope_info.requested_size.width; s_nRequestedHeight = gamescope_info.requested_size.height; calculate_capture_size(); state->gamescope_info = gamescope_info; int bpp = 4; if (state->video_info.format == SPA_VIDEO_FORMAT_NV12) { bpp = 1; } state->shm_stride = SPA_ROUND_UP_N(state->video_info.size.width * bpp, 4); const struct spa_pod_prop *modifier_prop = spa_pod_find_prop(param, nullptr, SPA_FORMAT_VIDEO_modifier); state->dmabuf = modifier_prop != nullptr; uint8_t buf[1024]; struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(buf, sizeof(buf)); int buffers = 4; int shm_size = state->shm_stride * state->video_info.size.height; if (state->video_info.format == SPA_VIDEO_FORMAT_NV12) { shm_size += ((state->video_info.size.height + 1) / 2) * state->shm_stride; } int data_type = state->dmabuf ? (1 << SPA_DATA_DmaBuf) : (1 << SPA_DATA_MemFd); const struct spa_pod *buffers_param = (const struct spa_pod *) spa_pod_builder_add_object(&builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(buffers, 1, 8), SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), SPA_PARAM_BUFFERS_size, SPA_POD_Int(shm_size), SPA_PARAM_BUFFERS_stride, SPA_POD_Int(state->shm_stride), SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(data_type)); const struct spa_pod *meta_param = (const struct spa_pod *) spa_pod_builder_add_object(&builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))); const struct spa_pod *scale_param = (const struct spa_pod *) spa_pod_builder_add_object(&builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_requested_size_scale), SPA_PARAM_META_size, SPA_POD_Int(sizeof(float))); const struct spa_pod *params[] = { buffers_param, meta_param, scale_param }; ret = pw_stream_update_params(state->stream, params, sizeof(params) / sizeof(params[0])); if (ret != 0) { pwr_log.errorf("pw_stream_update_params failed"); } pwr_log.debugf("format changed (size: %dx%d, requested %dx%d, format %d, stride %d, size: %d, dmabuf: %d)", state->video_info.size.width, state->video_info.size.height, s_nRequestedWidth, s_nRequestedHeight, state->video_info.format, state->shm_stride, shm_size, state->dmabuf); } static void randname(char *buf) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); long r = ts.tv_nsec; for (int i = 0; i < 6; ++i) { buf[i] = 'A'+(r&15)+(r&16)*2; r >>= 5; } } static int anonymous_shm_open(void) { char name[] = "/gamescope-pw-XXXXXX"; int retries = 100; do { randname(name + strlen(name) - 6); --retries; // shm_open guarantees that O_CLOEXEC is set int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); if (fd >= 0) { shm_unlink(name); return fd; } } while (retries > 0 && errno == EEXIST); return -1; } uint32_t spa_format_to_drm(uint32_t spa_format) { switch (spa_format) { case SPA_VIDEO_FORMAT_NV12: return DRM_FORMAT_NV12; default: case SPA_VIDEO_FORMAT_BGR: return DRM_FORMAT_XRGB8888; } } static void stream_handle_add_buffer(void *user_data, struct pw_buffer *pw_buffer) { struct pipewire_state *state = (struct pipewire_state *) user_data; struct spa_buffer *spa_buffer = pw_buffer->buffer; struct spa_data *spa_data = &spa_buffer->datas[0]; struct pipewire_buffer *buffer = new pipewire_buffer(); buffer->buffer = pw_buffer; buffer->video_info = state->video_info; buffer->gamescope_info = state->gamescope_info; bool is_dmabuf = (spa_data->type & (1 << SPA_DATA_DmaBuf)) != 0; bool is_memfd = (spa_data->type & (1 << SPA_DATA_MemFd)) != 0; EStreamColorspace colorspace = k_EStreamColorspace_Unknown; switch (state->video_info.color_matrix) { case SPA_VIDEO_COLOR_MATRIX_BT601: switch (state->video_info.color_range) { case SPA_VIDEO_COLOR_RANGE_16_235: colorspace = k_EStreamColorspace_BT601; break; case SPA_VIDEO_COLOR_RANGE_0_255: colorspace = k_EStreamColorspace_BT601_Full; break; default: break; } break; case SPA_VIDEO_COLOR_MATRIX_BT709: switch (state->video_info.color_range) { case SPA_VIDEO_COLOR_RANGE_16_235: colorspace = k_EStreamColorspace_BT709; break; case SPA_VIDEO_COLOR_RANGE_0_255: colorspace = k_EStreamColorspace_BT709_Full; break; default: break; } break; default: break; } uint32_t drmFormat = spa_format_to_drm(state->video_info.format); buffer->texture = new CVulkanTexture(); CVulkanTexture::createFlags screenshotImageFlags; screenshotImageFlags.bMappable = true; screenshotImageFlags.bTransferDst = true; screenshotImageFlags.bStorage = true; if (is_dmabuf || drmFormat == DRM_FORMAT_NV12) { screenshotImageFlags.bExportable = true; screenshotImageFlags.bLinear = true; // TODO: support multi-planar DMA-BUF export via PipeWire } bool bImageInitSuccess = buffer->texture->BInit( s_nCaptureWidth, s_nCaptureHeight, 1u, drmFormat, screenshotImageFlags ); if ( !bImageInitSuccess ) { pwr_log.errorf("Failed to initialize pipewire texture"); goto error; } buffer->texture->setStreamColorspace(colorspace); if (is_dmabuf) { const struct wlr_dmabuf_attributes dmabuf = buffer->texture->dmabuf(); if (dmabuf.n_planes != 1) { pwr_log.errorf("dmabuf.n_planes != 1"); goto error; } off_t size = lseek(dmabuf.fd[0], 0, SEEK_END); if (size < 0) { pwr_log.errorf_errno("lseek failed"); goto error; } buffer->type = SPA_DATA_DmaBuf; spa_data->type = SPA_DATA_DmaBuf; spa_data->flags = SPA_DATA_FLAG_READABLE; spa_data->fd = dmabuf.fd[0]; spa_data->mapoffset = dmabuf.offset[0]; spa_data->maxsize = size; spa_data->data = nullptr; } else if (is_memfd) { int fd = anonymous_shm_open(); if (fd < 0) { pwr_log.errorf("failed to create shm file"); goto error; } off_t size = state->shm_stride * state->video_info.size.height; if (state->video_info.format == SPA_VIDEO_FORMAT_NV12) { size += state->shm_stride * ((state->video_info.size.height + 1) / 2); } if (ftruncate(fd, size) != 0) { pwr_log.errorf_errno("ftruncate failed"); close(fd); goto error; } void *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (data == MAP_FAILED) { pwr_log.errorf_errno("mmap failed"); close(fd); goto error; } buffer->type = SPA_DATA_MemFd; buffer->shm.stride = state->shm_stride; buffer->shm.data = (uint8_t *) data; buffer->shm.fd = fd; spa_data->type = SPA_DATA_MemFd; spa_data->flags = SPA_DATA_FLAG_READABLE; spa_data->fd = fd; spa_data->mapoffset = 0; spa_data->maxsize = size; spa_data->data = data; } else { pwr_log.errorf("unsupported data type"); spa_data->type = SPA_DATA_Invalid; goto error; } pw_buffer->user_data = buffer; return; error: delete buffer; } static void stream_handle_remove_buffer(void *data, struct pw_buffer *pw_buffer) { struct pipewire_buffer *buffer = (struct pipewire_buffer *) pw_buffer->user_data; buffer->buffer = nullptr; if (!buffer->copying) { destroy_buffer(buffer); } } static const struct pw_stream_events stream_events = { .version = PW_VERSION_STREAM_EVENTS, .state_changed = stream_handle_state_changed, .param_changed = stream_handle_param_changed, .add_buffer = stream_handle_add_buffer, .remove_buffer = stream_handle_remove_buffer, .process = nullptr, }; enum pipewire_event_type { EVENT_PIPEWIRE, EVENT_NUDGE, EVENT_COUNT // keep last }; static void run_pipewire(struct pipewire_state *state) { pthread_setname_np( pthread_self(), "gamescope-pw" ); struct pollfd pollfds[] = { [EVENT_PIPEWIRE] = { .fd = pw_loop_get_fd(state->loop), .events = POLLIN, }, [EVENT_NUDGE] = { .fd = nudgePipe[0], .events = POLLIN, }, }; while (state->running) { int ret = poll(pollfds, EVENT_COUNT, -1); if (ret < 0) { pwr_log.errorf_errno("poll failed"); break; } if (pollfds[EVENT_PIPEWIRE].revents & POLLHUP) { pwr_log.errorf("lost connection to server"); break; } assert(!(pollfds[EVENT_NUDGE].revents & POLLHUP)); if (pollfds[EVENT_PIPEWIRE].revents & POLLIN) { ret = pw_loop_iterate(state->loop, -1); if (ret < 0) { pwr_log.errorf("pw_loop_iterate failed"); break; } } if (pollfds[EVENT_NUDGE].revents & POLLIN) { dispatch_nudge(state, nudgePipe[0]); } } pwr_log.infof("exiting"); pw_stream_destroy(state->stream); pw_core_disconnect(state->core); pw_context_destroy(state->context); pw_loop_destroy(state->loop); } bool init_pipewire(void) { struct pipewire_state *state = &pipewire_state; pw_init(nullptr, nullptr); if (pipe2(nudgePipe, O_CLOEXEC | O_NONBLOCK) != 0) { pwr_log.errorf_errno("pipe2 failed"); return false; } state->loop = pw_loop_new(nullptr); if (!state->loop) { pwr_log.errorf("pw_loop_new failed"); return false; } state->context = pw_context_new(state->loop, nullptr, 0); if (!state->context) { pwr_log.errorf("pw_context_new failed"); return false; } state->core = pw_context_connect(state->context, nullptr, 0); if (!state->core) { pwr_log.errorf("pw_context_connect failed"); return false; } state->stream = pw_stream_new(state->core, "gamescope", pw_properties_new( PW_KEY_MEDIA_CLASS, "Video/Source", nullptr)); if (!state->stream) { pwr_log.errorf("pw_stream_new failed"); return false; } static struct spa_hook stream_hook; pw_stream_add_listener(state->stream, &stream_hook, &stream_events, state); s_nRequestedWidth = 0; s_nRequestedHeight = 0; s_nOutputWidth = g_nOutputWidth; s_nOutputHeight = g_nOutputHeight; calculate_capture_size(); uint8_t buf[4096]; struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(buf, sizeof(buf)); std::vector format_params = build_format_params(&builder); enum pw_stream_flags flags = (enum pw_stream_flags)(PW_STREAM_FLAG_DRIVER | PW_STREAM_FLAG_ALLOC_BUFFERS); int ret = pw_stream_connect(state->stream, PW_DIRECTION_OUTPUT, PW_ID_ANY, flags, format_params.data(), format_params.size()); if (ret != 0) { pwr_log.errorf("pw_stream_connect failed"); return false; } state->running = true; while (state->stream_node_id == SPA_ID_INVALID) { int ret = pw_loop_iterate(state->loop, -1); if (ret < 0) { pwr_log.errorf("pw_loop_iterate failed"); return false; } } pwr_log.infof("stream available on node ID: %u", state->stream_node_id); std::thread thread(run_pipewire, state); thread.detach(); return true; } uint32_t get_pipewire_stream_node_id(void) { return pipewire_state.stream_node_id; } bool pipewire_is_streaming() { struct pipewire_state *state = &pipewire_state; return state->streaming; } struct pipewire_buffer *dequeue_pipewire_buffer(void) { struct pipewire_state *state = &pipewire_state; if (state->streaming) { request_buffer(state); } return out_buffer.exchange(nullptr); } void push_pipewire_buffer(struct pipewire_buffer *buffer) { struct pipewire_buffer *old = in_buffer.exchange(buffer); if ( old != nullptr ) { pwr_log.errorf_errno("push_pipewire_buffer: Already had a buffer?!"); } nudge_pipewire(); } void nudge_pipewire(void) { if (write(nudgePipe[1], "\n", 1) < 0) pwr_log.errorf_errno("nudge_pipewire: write failed"); } ValveSoftware-gamescope-eb620ab/src/pipewire.hpp000066400000000000000000000032341502457270500220630ustar00rootroot00000000000000#pragma once #include #include #include #include "rendervulkan.hpp" #include "pipewire_gamescope.hpp" struct pipewire_state { struct pw_loop *loop; struct pw_context *context; struct pw_core *core; bool running; struct pw_stream *stream; uint32_t stream_node_id; std::atomic streaming; struct spa_video_info_raw video_info; struct spa_gamescope gamescope_info; uint64_t focus_appid; bool dmabuf; int shm_stride; uint64_t seq; }; /** * PipeWire buffers are allocated by the PipeWire thread, and are temporarily * shared with the steamcompmgr thread (via dequeue_pipewire_buffer and * push_pipewire_buffer) for copying. */ struct pipewire_buffer { enum spa_data_type type; // SPA_DATA_MemFd or SPA_DATA_DmaBuf struct spa_video_info_raw video_info; struct spa_gamescope gamescope_info; gamescope::OwningRc texture; // Only used for SPA_DATA_MemFd struct { int stride; uint8_t *data; int fd; } shm; // The following fields are not thread-safe // The PipeWire buffer, or nullptr if it's been destroyed. std::atomic buffer; bool IsStale() const { return buffer == nullptr; } // We pass the buffer to the steamcompmgr thread for copying. This is set // to true if the buffer is currently owned by the steamcompmgr thread. bool copying; }; bool init_pipewire(void); uint32_t get_pipewire_stream_node_id(void); struct pipewire_buffer *dequeue_pipewire_buffer(void); bool pipewire_is_streaming(); void pipewire_destroy_buffer(struct pipewire_buffer *buffer); void push_pipewire_buffer(struct pipewire_buffer *buffer); void nudge_pipewire(void); ValveSoftware-gamescope-eb620ab/src/pipewire_gamescope.hpp000066400000000000000000000040031502457270500241010ustar00rootroot00000000000000#pragma once #include #include enum { SPA_FORMAT_VIDEO_requested_size = 0x70000, SPA_FORMAT_VIDEO_gamescope_focus_appid = 0x70001, }; enum { SPA_META_requested_size_scale = 0x70000 }; struct spa_gamescope { spa_rectangle requested_size; uint64_t focus_appid; }; static inline int spa_format_video_raw_parse_with_gamescope(const struct spa_pod *format, struct spa_video_info_raw *info, spa_gamescope *gamescope_info) { return spa_pod_parse_object(format, SPA_TYPE_OBJECT_Format, NULL, SPA_FORMAT_VIDEO_format, SPA_POD_Id(&info->format), SPA_FORMAT_VIDEO_modifier, SPA_POD_OPT_Long(&info->modifier), SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&info->size), SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&info->framerate), SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_OPT_Fraction(&info->max_framerate), SPA_FORMAT_VIDEO_views, SPA_POD_OPT_Int(&info->views), SPA_FORMAT_VIDEO_interlaceMode, SPA_POD_OPT_Id(&info->interlace_mode), SPA_FORMAT_VIDEO_pixelAspectRatio, SPA_POD_OPT_Fraction(&info->pixel_aspect_ratio), SPA_FORMAT_VIDEO_multiviewMode, SPA_POD_OPT_Id(&info->multiview_mode), SPA_FORMAT_VIDEO_multiviewFlags, SPA_POD_OPT_Id(&info->multiview_flags), SPA_FORMAT_VIDEO_chromaSite, SPA_POD_OPT_Id(&info->chroma_site), SPA_FORMAT_VIDEO_colorRange, SPA_POD_OPT_Id(&info->color_range), SPA_FORMAT_VIDEO_colorMatrix, SPA_POD_OPT_Id(&info->color_matrix), SPA_FORMAT_VIDEO_transferFunction, SPA_POD_OPT_Id(&info->transfer_function), SPA_FORMAT_VIDEO_colorPrimaries, SPA_POD_OPT_Id(&info->color_primaries), SPA_FORMAT_VIDEO_requested_size, SPA_POD_OPT_Rectangle(&gamescope_info->requested_size), SPA_FORMAT_VIDEO_gamescope_focus_appid, SPA_POD_OPT_Long(&gamescope_info->focus_appid)); } ValveSoftware-gamescope-eb620ab/src/rc.h000066400000000000000000000120631502457270500203030ustar00rootroot00000000000000#pragma once #include #include namespace gamescope { class RcObject { public: virtual ~RcObject() { } uint32_t IncRef() { uint32_t uRefCount = m_uRefCount++; if ( !uRefCount ) IncRefPrivate(); return uRefCount; } uint32_t DecRef() { uint32_t uRefCount = --m_uRefCount; if ( !uRefCount ) DecRefPrivate(); return uRefCount; } uint32_t IncRefPrivate() { return m_uRefPrivate++; } uint32_t DecRefPrivate() { uint32_t uRefPrivate = --m_uRefPrivate; if ( !uRefPrivate ) { m_uRefPrivate += 0x80000000; delete this; } return uRefPrivate; } uint32_t GetRefCount() const { return m_uRefCount; } uint32_t GetRefCountPrivate() const { return m_uRefPrivate; } bool HasLiveReferences() const { return bool( m_uRefCount.load() | ( m_uRefPrivate.load() & 0x7FFFFFFF ) ); } private: std::atomic m_uRefCount{ 0u }; std::atomic m_uRefPrivate{ 0u }; }; class IRcObject : public RcObject { public: virtual uint32_t IncRef() { return RcObject::IncRef(); } virtual uint32_t DecRef() { return RcObject::DecRef(); } }; template struct RcRef_ { static void IncRef( T* pObject ) { pObject->IncRef(); } static void DecRef( T* pObject ) { pObject->DecRef(); } }; template struct RcRef_ { static void IncRef( T* pObject ) { pObject->IncRefPrivate(); } static void DecRef( T* pObject ) { pObject->DecRefPrivate(); } }; template class Rc { template friend class Rc; using RcRef = RcRef_; public: Rc() { } Rc( std::nullptr_t ) { } Rc( T* pObject ) : m_pObject{ pObject } { this->IncRef(); } Rc( const Rc& other ) : m_pObject{ other.m_pObject } { this->IncRef(); } template Rc( const Rc& other ) : m_pObject{ other.m_pObject } { this->IncRef(); } Rc( Rc&& other ) : m_pObject{ other.m_pObject } { other.m_pObject = nullptr; } template Rc( Rc&& other ) : m_pObject{ other.m_pObject } { other.m_pObject = nullptr; } Rc& operator = ( std::nullptr_t ) { this->DecRef(); m_pObject = nullptr; return *this; } Rc& operator = ( const Rc& other ) { other.IncRef(); this->DecRef(); m_pObject = other.m_pObject; return *this; } template Rc& operator = ( const Rc& other ) { other.IncRef(); this->DecRef(); m_pObject = other.m_pObject; return *this; } Rc& operator = ( Rc&& other ) { this->DecRef(); this->m_pObject = other.m_pObject; other.m_pObject = nullptr; return *this; } template Rc& operator = ( Rc&& other ) { this->DecRef(); this->m_pObject = other.m_pObject; other.m_pObject = nullptr; return *this; } ~Rc() { this->DecRef(); } T& operator * () const { return *m_pObject; } T* operator -> () const { return m_pObject; } T* get() const { return m_pObject; } bool operator == ( const Rc& other ) const { return m_pObject == other.m_pObject; } bool operator != ( const Rc& other ) const { return m_pObject != other.m_pObject; } bool operator == ( T *pOther ) const { return m_pObject == pOther; } bool operator != ( T *pOther ) const { return m_pObject == pOther; } bool operator == ( std::nullptr_t ) const { return m_pObject == nullptr; } bool operator != ( std::nullptr_t ) const { return m_pObject != nullptr; } operator bool() const { return m_pObject != nullptr; } private: T* m_pObject = nullptr; inline void IncRef() const { if ( m_pObject != nullptr ) RcRef::IncRef( m_pObject ); } inline void DecRef() const { if ( m_pObject != nullptr ) RcRef::DecRef( m_pObject ); } }; template using OwningRc = Rc; }ValveSoftware-gamescope-eb620ab/src/refresh_rate.h000066400000000000000000000023331502457270500223470ustar00rootroot00000000000000#pragma once #include namespace gamescope { constexpr int32_t ConvertHztomHz( int32_t nRefreshHz ) { return nRefreshHz * 1'000; } constexpr int32_t ConvertmHzToHz( int32_t nRefreshmHz ) { // Round to nearest when going to mHz. // Ceil seems to be wrong when we have 60.001 or 90.004 etc. // Floor seems to be bad if we have 143.99 // So round to nearest. return ( nRefreshmHz + 499 ) / 1'000; } constexpr uint32_t ConvertHztomHz( uint32_t nRefreshHz ) { return nRefreshHz * 1'000; } constexpr uint32_t ConvertmHzToHz( uint32_t nRefreshmHz ) { return ( nRefreshmHz + 499 ) / 1'000; } constexpr float ConvertHztomHz( float flRefreshHz ) { return flRefreshHz * 1000.0f; } constexpr float ConvertmHzToHz( float nRefreshmHz ) { return ( nRefreshmHz ) / 1'000.0; } constexpr uint32_t RefreshCycleTomHz( int32_t nCycle ) { // Round cycle to nearest. return ( 1'000'000'000'000ul + ( nCycle / 2 ) - 1 ) / nCycle; } constexpr uint32_t mHzToRefreshCycle( int32_t nmHz ) { // Same thing. return RefreshCycleTomHz( nmHz ); } } ValveSoftware-gamescope-eb620ab/src/rendervulkan.cpp000066400000000000000000004052551502457270500227430ustar00rootroot00000000000000// Initialize Vulkan and composite stuff with a compute queue #include #include #include #include #include #include #include #include #include #include #include #include "vulkan_include.h" #include "Utils/Algorithm.h" #if defined(__linux__) #include #endif // Used to remove the config struct alignment specified by the NIS header #define NIS_ALIGNED(x) // NIS_Config needs to be included before the X11 headers because of conflicting defines introduced by X11 #include "shaders/NVIDIAImageScaling/NIS/NIS_Config.h" #include #include "hdmi.h" #if HAVE_DRM #include "drm_include.h" #endif #include "wlr_begin.hpp" #include #include "wlr_end.hpp" #include "rendervulkan.hpp" #include "main.hpp" #include "steamcompmgr.hpp" #include "log.hpp" #include "Utils/Process.h" #include "cs_composite_blit.h" #include "cs_composite_blur.h" #include "cs_composite_blur_cond.h" #include "cs_composite_rcas.h" #include "cs_easu.h" #include "cs_easu_fp16.h" #include "cs_gaussian_blur_horizontal.h" #include "cs_nis.h" #include "cs_nis_fp16.h" #include "cs_rgb_to_nv12.h" #define A_CPU #include "shaders/ffx_a.h" #include "shaders/ffx_fsr1.h" #include "reshade_effect_manager.hpp" extern bool g_bWasPartialComposite; static constexpr mat3x4 g_rgb2yuv_srgb_to_bt601_limited = {{ { 0.257f, 0.504f, 0.098f, 0.0625f }, { -0.148f, -0.291f, 0.439f, 0.5f }, { 0.439f, -0.368f, -0.071f, 0.5f }, }}; static constexpr mat3x4 g_rgb2yuv_srgb_to_bt601 = {{ { 0.299f, 0.587f, 0.114f, 0.0f }, { -0.169f, -0.331f, 0.500f, 0.5f }, { 0.500f, -0.419f, -0.081f, 0.5f }, }}; static constexpr mat3x4 g_rgb2yuv_srgb_to_bt709_limited = {{ { 0.1826f, 0.6142f, 0.0620f, 0.0625f }, { -0.1006f, -0.3386f, 0.4392f, 0.5f }, { 0.4392f, -0.3989f, -0.0403f, 0.5f }, }}; static constexpr mat3x4 g_rgb2yuv_srgb_to_bt709_full = {{ { 0.2126f, 0.7152f, 0.0722f, 0.0f }, { -0.1146f, -0.3854f, 0.5000f, 0.5f }, { 0.5000f, -0.4542f, -0.0458f, 0.5f }, }}; static const mat3x4& colorspace_to_conversion_from_srgb_matrix(EStreamColorspace colorspace) { switch (colorspace) { default: case k_EStreamColorspace_BT601: return g_rgb2yuv_srgb_to_bt601_limited; case k_EStreamColorspace_BT601_Full: return g_rgb2yuv_srgb_to_bt601; case k_EStreamColorspace_BT709: return g_rgb2yuv_srgb_to_bt709_limited; case k_EStreamColorspace_BT709_Full: return g_rgb2yuv_srgb_to_bt709_full; } } PFN_vkGetInstanceProcAddr g_pfn_vkGetInstanceProcAddr; PFN_vkCreateInstance g_pfn_vkCreateInstance; static VkResult vulkan_load_module() { static VkResult s_result = []() { void* pModule = dlopen( "libvulkan.so.1", RTLD_NOW | RTLD_LOCAL ); if ( !pModule ) pModule = dlopen( "libvulkan.so", RTLD_NOW | RTLD_LOCAL ); if ( !pModule ) return VK_ERROR_INITIALIZATION_FAILED; g_pfn_vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)dlsym( pModule, "vkGetInstanceProcAddr" ); if ( !g_pfn_vkGetInstanceProcAddr ) return VK_ERROR_INITIALIZATION_FAILED; g_pfn_vkCreateInstance = (PFN_vkCreateInstance) g_pfn_vkGetInstanceProcAddr( nullptr, "vkCreateInstance" ); if ( !g_pfn_vkCreateInstance ) return VK_ERROR_INITIALIZATION_FAILED; return VK_SUCCESS; }(); return s_result; } VulkanOutput_t g_output; uint32_t g_uCompositeDebug = 0u; gamescope::ConVar cv_composite_debug{ "composite_debug", 0, "Debug composition flags" }; static std::map< VkFormat, std::map< uint64_t, VkDrmFormatModifierPropertiesEXT > > DRMModifierProps = {}; static struct wlr_drm_format_set sampledShmFormats = {}; static struct wlr_drm_format_set sampledDRMFormats = {}; static LogScope vk_log("vulkan"); static void vk_errorf(VkResult result, const char *fmt, ...) { static char buf[1024]; va_list args; va_start(args, fmt); vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); vk_log.errorf("%s (VkResult: %d)", buf, result); } // For when device is up and it would be totally fatal to fail #define vk_check( x ) \ do \ { \ VkResult check_res = VK_SUCCESS; \ if ( ( check_res = ( x ) ) != VK_SUCCESS ) \ { \ vk_errorf( check_res, #x " failed!" ); \ abort(); \ } \ } while ( 0 ) template Target *pNextFind(const Base *base, VkStructureType sType) { for ( ; base; base = (const Base *)base->pNext ) { if (base->sType == sType) return (Target *) base; } return nullptr; } #define VK_STRUCTURE_TYPE_WSI_IMAGE_CREATE_INFO_MESA (VkStructureType)1000001002 #define VK_STRUCTURE_TYPE_WSI_MEMORY_ALLOCATE_INFO_MESA (VkStructureType)1000001003 struct wsi_image_create_info { VkStructureType sType; const void *pNext; bool scanout; uint32_t modifier_count; const uint64_t *modifiers; }; struct wsi_memory_allocate_info { VkStructureType sType; const void *pNext; bool implicit_sync; }; // DRM doesn't have 32bit floating point formats, so add our own #define DRM_FORMAT_ABGR32323232F fourcc_code('A', 'B', '8', 'F') #define DRM_FORMAT_R16F fourcc_code('R', '1', '6', 'F') #define DRM_FORMAT_R32F fourcc_code('R', '3', '2', 'F') struct { uint32_t DRMFormat; VkFormat vkFormat; VkFormat vkFormatSrgb; uint32_t bpp; bool bHasAlpha; bool internal; } s_DRMVKFormatTable[] = { { DRM_FORMAT_ARGB8888, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_SRGB, 4, true, false }, { DRM_FORMAT_XRGB8888, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_SRGB, 4, false, false }, { DRM_FORMAT_ABGR8888, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_SRGB, 4, true, false }, { DRM_FORMAT_XBGR8888, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_SRGB, 4, false, false }, { DRM_FORMAT_RGB565, VK_FORMAT_R5G6B5_UNORM_PACK16, VK_FORMAT_R5G6B5_UNORM_PACK16, 1, false, false }, { DRM_FORMAT_NV12, VK_FORMAT_G8_B8R8_2PLANE_420_UNORM, VK_FORMAT_G8_B8R8_2PLANE_420_UNORM, 0, false, false }, { DRM_FORMAT_ABGR16161616F, VK_FORMAT_R16G16B16A16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT, 8, true, false }, { DRM_FORMAT_XBGR16161616F, VK_FORMAT_R16G16B16A16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT, 8, false, false }, { DRM_FORMAT_ABGR16161616, VK_FORMAT_R16G16B16A16_UNORM, VK_FORMAT_R16G16B16A16_UNORM, 8, true, false }, { DRM_FORMAT_XBGR16161616, VK_FORMAT_R16G16B16A16_UNORM, VK_FORMAT_R16G16B16A16_UNORM, 8, false, false }, { DRM_FORMAT_ABGR2101010, VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4, true, false }, { DRM_FORMAT_XBGR2101010, VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4, false, false }, { DRM_FORMAT_ARGB2101010, VK_FORMAT_A2R10G10B10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32, 4, true, false }, { DRM_FORMAT_XRGB2101010, VK_FORMAT_A2R10G10B10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32, 4, false, false }, { DRM_FORMAT_R8, VK_FORMAT_R8_UNORM, VK_FORMAT_R8_UNORM, 1, false, true }, { DRM_FORMAT_R16, VK_FORMAT_R16_UNORM, VK_FORMAT_R16_UNORM, 2, false, true }, { DRM_FORMAT_GR88, VK_FORMAT_R8G8_UNORM, VK_FORMAT_R8G8_UNORM, 2, false, true }, { DRM_FORMAT_GR1616, VK_FORMAT_R16G16_UNORM, VK_FORMAT_R16G16_UNORM, 4, false, true }, { DRM_FORMAT_ABGR32323232F, VK_FORMAT_R32G32B32A32_SFLOAT, VK_FORMAT_R32G32B32A32_SFLOAT, 16,true, true }, { DRM_FORMAT_R16F, VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16_SFLOAT, 2, false, true }, { DRM_FORMAT_R32F, VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32_SFLOAT, 4, false, true }, { DRM_FORMAT_INVALID, VK_FORMAT_UNDEFINED, VK_FORMAT_UNDEFINED, false, true }, }; uint32_t VulkanFormatToDRM( VkFormat vkFormat, std::optional obHasAlphaOverride ) { for ( int i = 0; s_DRMVKFormatTable[i].vkFormat != VK_FORMAT_UNDEFINED; i++ ) { if ( ( s_DRMVKFormatTable[i].vkFormat == vkFormat || s_DRMVKFormatTable[i].vkFormatSrgb == vkFormat ) && ( !obHasAlphaOverride || s_DRMVKFormatTable[i].bHasAlpha == *obHasAlphaOverride ) ) { return s_DRMVKFormatTable[i].DRMFormat; } } return DRM_FORMAT_INVALID; } VkFormat DRMFormatToVulkan( uint32_t nDRMFormat, bool bSrgb ) { for ( int i = 0; s_DRMVKFormatTable[i].vkFormat != VK_FORMAT_UNDEFINED; i++ ) { if ( s_DRMVKFormatTable[i].DRMFormat == nDRMFormat ) { return bSrgb ? s_DRMVKFormatTable[i].vkFormatSrgb : s_DRMVKFormatTable[i].vkFormat; } } return VK_FORMAT_UNDEFINED; } bool DRMFormatHasAlpha( uint32_t nDRMFormat ) { for ( int i = 0; s_DRMVKFormatTable[i].vkFormat != VK_FORMAT_UNDEFINED; i++ ) { if ( s_DRMVKFormatTable[i].DRMFormat == nDRMFormat ) { return s_DRMVKFormatTable[i].bHasAlpha; } } return false; } uint32_t DRMFormatGetBPP( uint32_t nDRMFormat ) { for ( int i = 0; s_DRMVKFormatTable[i].vkFormat != VK_FORMAT_UNDEFINED; i++ ) { if ( s_DRMVKFormatTable[i].DRMFormat == nDRMFormat ) { return s_DRMVKFormatTable[i].bpp; } } return false; } bool CVulkanDevice::BInit(VkInstance instance, VkSurfaceKHR surface) { assert(instance); assert(!m_bInitialized); g_output.surface = surface; m_instance = instance; #define VK_FUNC(x) vk.x = (PFN_vk##x) g_pfn_vkGetInstanceProcAddr(instance, "vk"#x); VULKAN_INSTANCE_FUNCTIONS #undef VK_FUNC if (!selectPhysDev(surface)) return false; if (!createDevice()) return false; if (!createLayouts()) return false; if (!createPools()) return false; if (!createShaders()) return false; if (!createScratchResources()) return false; m_bInitialized = true; std::thread piplelineThread([this](){compileAllPipelines();}); piplelineThread.detach(); g_reshadeManager.init(this); return true; } extern bool env_to_bool(const char *env); bool CVulkanDevice::selectPhysDev(VkSurfaceKHR surface) { uint32_t deviceCount = 0; vk.EnumeratePhysicalDevices(instance(), &deviceCount, nullptr); std::vector physDevs(deviceCount); vk.EnumeratePhysicalDevices(instance(), &deviceCount, physDevs.data()); if (deviceCount < physDevs.size()) physDevs.resize(deviceCount); bool bTryComputeOnly = true; // In theory vkBasalt might want to filter out compute-only queue families to force our hand here const char *pchEnableVkBasalt = getenv( "ENABLE_VKBASALT" ); if ( pchEnableVkBasalt != nullptr && pchEnableVkBasalt[0] == '1' ) { bTryComputeOnly = false; } for (auto cphysDev : physDevs) { VkPhysicalDeviceProperties deviceProperties; vk.GetPhysicalDeviceProperties(cphysDev, &deviceProperties); if (deviceProperties.apiVersion < VK_API_VERSION_1_2) continue; uint32_t queueFamilyCount = 0; vk.GetPhysicalDeviceQueueFamilyProperties(cphysDev, &queueFamilyCount, nullptr); std::vector queueFamilyProperties(queueFamilyCount); vk.GetPhysicalDeviceQueueFamilyProperties(cphysDev, &queueFamilyCount, queueFamilyProperties.data()); uint32_t generalIndex = ~0u; uint32_t computeOnlyIndex = ~0u; for (uint32_t i = 0; i < queueFamilyCount; ++i) { const VkQueueFlags generalBits = VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT; if ((queueFamilyProperties[i].queueFlags & generalBits) == generalBits ) generalIndex = std::min(generalIndex, i); else if (bTryComputeOnly && queueFamilyProperties[i].queueFlags & VK_QUEUE_COMPUTE_BIT) computeOnlyIndex = std::min(computeOnlyIndex, i); } if (generalIndex != ~0u || computeOnlyIndex != ~0u) { // Select the device if it's the first one or the preferred one if (!m_physDev || (g_preferVendorID == deviceProperties.vendorID && g_preferDeviceID == deviceProperties.deviceID)) { // if we have a surface, check that the queue family can actually present on it if (surface) { VkBool32 canPresent = false; vk.GetPhysicalDeviceSurfaceSupportKHR( cphysDev, generalIndex, surface, &canPresent ); if ( !canPresent ) { vk_log.infof( "physical device %04x:%04x queue doesn't support presenting on our surface, testing next one..", deviceProperties.vendorID, deviceProperties.deviceID ); continue; } if (computeOnlyIndex != ~0u) { vk.GetPhysicalDeviceSurfaceSupportKHR( cphysDev, computeOnlyIndex, surface, &canPresent ); if ( !canPresent ) { vk_log.infof( "physical device %04x:%04x compute queue doesn't support presenting on our surface, using graphics queue", deviceProperties.vendorID, deviceProperties.deviceID ); computeOnlyIndex = ~0u; } } } m_queueFamily = computeOnlyIndex == ~0u ? generalIndex : computeOnlyIndex; m_generalQueueFamily = generalIndex; m_physDev = cphysDev; if ( env_to_bool( getenv( "GAMESCOPE_FORCE_GENERAL_QUEUE" ) ) ) m_queueFamily = generalIndex; } } } if (!m_physDev) { vk_log.errorf("failed to find physical device"); return false; } VkPhysicalDeviceProperties props; vk.GetPhysicalDeviceProperties( m_physDev, &props ); vk_log.infof( "selecting physical device '%s': queue family %x (general queue family %x)", props.deviceName, m_queueFamily, m_generalQueueFamily ); return true; } bool CVulkanDevice::createDevice() { vk.GetPhysicalDeviceMemoryProperties( physDev(), &m_memoryProperties ); uint32_t supportedExtensionCount; vk.EnumerateDeviceExtensionProperties( physDev(), NULL, &supportedExtensionCount, NULL ); std::vector supportedExts(supportedExtensionCount); vk.EnumerateDeviceExtensionProperties( physDev(), NULL, &supportedExtensionCount, supportedExts.data() ); bool hasDrmProps = false; bool supportsForeignQueue = false; bool supportsHDRMetadata = false; for ( uint32_t i = 0; i < supportedExtensionCount; ++i ) { if ( strcmp(supportedExts[i].extensionName, VK_EXT_IMAGE_DRM_FORMAT_MODIFIER_EXTENSION_NAME) == 0 ) m_bSupportsModifiers = true; if ( strcmp(supportedExts[i].extensionName, VK_EXT_PHYSICAL_DEVICE_DRM_EXTENSION_NAME) == 0 ) hasDrmProps = true; if ( strcmp(supportedExts[i].extensionName, VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME) == 0 ) supportsForeignQueue = true; if ( strcmp(supportedExts[i].extensionName, VK_EXT_HDR_METADATA_EXTENSION_NAME) == 0 ) supportsHDRMetadata = true; } vk_log.infof( "physical device %s DRM format modifiers", m_bSupportsModifiers ? "supports" : "does not support" ); if ( !GetBackend()->ValidPhysicalDevice( physDev() ) ) return false; #if HAVE_DRM // XXX(JoshA): Move this to ValidPhysicalDevice. // We need to refactor some Vulkan stuff to do that though. if ( hasDrmProps ) { VkPhysicalDeviceDrmPropertiesEXT drmProps = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRM_PROPERTIES_EXT, }; VkPhysicalDeviceProperties2 props2 = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &drmProps, }; vk.GetPhysicalDeviceProperties2( physDev(), &props2 ); if ( !GetBackend()->UsesVulkanSwapchain() && !drmProps.hasPrimary ) { vk_log.errorf( "physical device has no primary node" ); return false; } if ( !drmProps.hasRender ) { vk_log.errorf( "physical device has no render node" ); return false; } dev_t renderDevId = makedev( drmProps.renderMajor, drmProps.renderMinor ); drmDevice *drmDev = nullptr; if (drmGetDeviceFromDevId(renderDevId, 0, &drmDev) != 0) { vk_log.errorf( "drmGetDeviceFromDevId() failed" ); return false; } assert(drmDev->available_nodes & (1 << DRM_NODE_RENDER)); const char *drmRenderName = drmDev->nodes[DRM_NODE_RENDER]; m_drmRendererFd = open( drmRenderName, O_RDWR | O_CLOEXEC ); drmFreeDevice(&drmDev); if ( m_drmRendererFd < 0 ) { vk_log.errorf_errno( "failed to open DRM render node" ); return false; } if ( drmProps.hasPrimary ) { m_bHasDrmPrimaryDevId = true; m_drmPrimaryDevId = makedev( drmProps.primaryMajor, drmProps.primaryMinor ); } } else #endif { vk_log.errorf( "physical device doesn't support VK_EXT_physical_device_drm" ); return false; } if ( m_bSupportsModifiers && !supportsForeignQueue ) { vk_log.infof( "The vulkan driver does not support foreign queues," " disabling modifier support."); m_bSupportsModifiers = false; } { VkPhysicalDeviceVulkan12Features vulkan12Features = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, }; VkPhysicalDeviceFeatures2 features2 = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = &vulkan12Features, }; vk.GetPhysicalDeviceFeatures2( physDev(), &features2 ); m_bSupportsFp16 = vulkan12Features.shaderFloat16 && features2.features.shaderInt16; } float queuePriorities = 1.0f; VkDeviceQueueGlobalPriorityCreateInfoEXT queueCreateInfoEXT = { .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_GLOBAL_PRIORITY_CREATE_INFO_EXT, .pNext = nullptr, .globalPriority = VK_QUEUE_GLOBAL_PRIORITY_REALTIME_EXT }; VkDeviceQueueCreateInfo queueCreateInfos[2] = { { .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, .pNext = gamescope::Process::HasCapSysNice() ? &queueCreateInfoEXT : nullptr, .queueFamilyIndex = m_queueFamily, .queueCount = 1, .pQueuePriorities = &queuePriorities }, { .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, .pNext = gamescope::Process::HasCapSysNice() ? &queueCreateInfoEXT : nullptr, .queueFamilyIndex = m_generalQueueFamily, .queueCount = 1, .pQueuePriorities = &queuePriorities }, }; std::vector< const char * > enabledExtensions; if ( GetBackend()->UsesVulkanSwapchain() ) { enabledExtensions.push_back( VK_KHR_SWAPCHAIN_EXTENSION_NAME ); enabledExtensions.push_back( VK_KHR_SWAPCHAIN_MUTABLE_FORMAT_EXTENSION_NAME ); enabledExtensions.push_back( VK_KHR_PRESENT_ID_EXTENSION_NAME ); enabledExtensions.push_back( VK_KHR_PRESENT_WAIT_EXTENSION_NAME ); } if ( m_bSupportsModifiers ) { enabledExtensions.push_back( VK_EXT_IMAGE_DRM_FORMAT_MODIFIER_EXTENSION_NAME ); enabledExtensions.push_back( VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME ); } enabledExtensions.push_back( VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME ); enabledExtensions.push_back( VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME ); enabledExtensions.push_back( VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME ); enabledExtensions.push_back( VK_EXT_ROBUSTNESS_2_EXTENSION_NAME ); #if 0 enabledExtensions.push_back( VK_KHR_MAINTENANCE_5_EXTENSION_NAME ); #endif if ( supportsHDRMetadata ) enabledExtensions.push_back( VK_EXT_HDR_METADATA_EXTENSION_NAME ); for ( auto& extension : GetBackend()->GetDeviceExtensions( physDev() ) ) enabledExtensions.push_back( extension ); #if 0 VkPhysicalDeviceMaintenance5FeaturesKHR maintenance5 = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES_KHR, .maintenance5 = VK_TRUE, }; #endif VkPhysicalDeviceVulkan13Features features13 = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, #if 0 .pNext = &maintenance5, #endif .dynamicRendering = VK_TRUE, }; VkPhysicalDevicePresentWaitFeaturesKHR presentWaitFeatures = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_WAIT_FEATURES_KHR, .pNext = &features13, .presentWait = VK_TRUE, }; VkPhysicalDevicePresentIdFeaturesKHR presentIdFeatures = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR, .pNext = &presentWaitFeatures, .presentId = VK_TRUE, }; VkPhysicalDeviceFeatures2 features2 = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = &presentIdFeatures, .features = { .shaderInt16 = m_bSupportsFp16, }, }; VkDeviceCreateInfo deviceCreateInfo = { .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, .pNext = &features2, .queueCreateInfoCount = m_queueFamily == m_generalQueueFamily ? 1u : 2u, .pQueueCreateInfos = queueCreateInfos, .enabledExtensionCount = (uint32_t)enabledExtensions.size(), .ppEnabledExtensionNames = enabledExtensions.data(), }; VkPhysicalDeviceVulkan12Features vulkan12Features = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, .pNext = std::exchange(features2.pNext, &vulkan12Features), .shaderFloat16 = m_bSupportsFp16, .scalarBlockLayout = VK_TRUE, .timelineSemaphore = VK_TRUE, }; VkPhysicalDeviceSamplerYcbcrConversionFeatures ycbcrFeatures = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_YCBCR_CONVERSION_FEATURES, .pNext = std::exchange(features2.pNext, &ycbcrFeatures), .samplerYcbcrConversion = VK_TRUE, }; VkPhysicalDeviceRobustness2FeaturesEXT robustness2Features = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT, .pNext = std::exchange(features2.pNext, &robustness2Features), .nullDescriptor = VK_TRUE, }; VkResult res = vk.CreateDevice(physDev(), &deviceCreateInfo, nullptr, &m_device); if ( res == VK_ERROR_NOT_PERMITTED_KHR && gamescope::Process::HasCapSysNice() ) { fprintf(stderr, "vkCreateDevice failed with a high-priority queue (general + compute). Falling back to regular priority (general).\n"); queueCreateInfos[1].pNext = nullptr; res = vk.CreateDevice(physDev(), &deviceCreateInfo, nullptr, &m_device); if ( res == VK_ERROR_NOT_PERMITTED_KHR && gamescope::Process::HasCapSysNice() ) { fprintf(stderr, "vkCreateDevice failed with a high-priority queue (compute). Falling back to regular priority (all).\n"); queueCreateInfos[0].pNext = nullptr; res = vk.CreateDevice(physDev(), &deviceCreateInfo, nullptr, &m_device); } } if ( res != VK_SUCCESS ) { vk_errorf( res, "vkCreateDevice failed" ); return false; } #define VK_FUNC(x) vk.x = (PFN_vk##x) vk.GetDeviceProcAddr(device(), "vk"#x); VULKAN_DEVICE_FUNCTIONS #undef VK_FUNC vk.GetDeviceQueue(device(), m_queueFamily, 0, &m_queue); if ( m_queueFamily == m_generalQueueFamily ) m_generalQueue = m_queue; else vk.GetDeviceQueue(device(), m_generalQueueFamily, 0, &m_generalQueue); return true; } static VkSamplerYcbcrModelConversion colorspaceToYCBCRModel( EStreamColorspace colorspace ) { switch (colorspace) { default: case k_EStreamColorspace_Unknown: return VK_SAMPLER_YCBCR_MODEL_CONVERSION_YCBCR_709; case k_EStreamColorspace_BT601: case k_EStreamColorspace_BT601_Full: return VK_SAMPLER_YCBCR_MODEL_CONVERSION_YCBCR_601; case k_EStreamColorspace_BT709: case k_EStreamColorspace_BT709_Full: return VK_SAMPLER_YCBCR_MODEL_CONVERSION_YCBCR_709; } } static VkSamplerYcbcrRange colorspaceToYCBCRRange( EStreamColorspace colorspace ) { switch (colorspace) { default: case k_EStreamColorspace_Unknown: return VK_SAMPLER_YCBCR_RANGE_ITU_FULL; case k_EStreamColorspace_BT709: case k_EStreamColorspace_BT601: return VK_SAMPLER_YCBCR_RANGE_ITU_NARROW; case k_EStreamColorspace_BT601_Full: case k_EStreamColorspace_BT709_Full: return VK_SAMPLER_YCBCR_RANGE_ITU_FULL; } } bool CVulkanDevice::createLayouts() { VkFormatProperties nv12Properties; vk.GetPhysicalDeviceFormatProperties(physDev(), VK_FORMAT_G8_B8R8_2PLANE_420_UNORM, &nv12Properties); bool cosited = nv12Properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COSITED_CHROMA_SAMPLES_BIT; VkSamplerYcbcrConversionCreateInfo ycbcrSamplerConversionCreateInfo = { .sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO, .format = VK_FORMAT_G8_B8R8_2PLANE_420_UNORM, .ycbcrModel = colorspaceToYCBCRModel( g_ForcedNV12ColorSpace ), .ycbcrRange = colorspaceToYCBCRRange( g_ForcedNV12ColorSpace ), .xChromaOffset = cosited ? VK_CHROMA_LOCATION_COSITED_EVEN : VK_CHROMA_LOCATION_MIDPOINT, .yChromaOffset = cosited ? VK_CHROMA_LOCATION_COSITED_EVEN : VK_CHROMA_LOCATION_MIDPOINT, .chromaFilter = VK_FILTER_LINEAR, .forceExplicitReconstruction = VK_FALSE, }; vk.CreateSamplerYcbcrConversion( device(), &ycbcrSamplerConversionCreateInfo, nullptr, &m_ycbcrConversion ); VkSamplerYcbcrConversionInfo ycbcrSamplerConversionInfo = { .sType = VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO, .conversion = m_ycbcrConversion, }; VkSamplerCreateInfo ycbcrSamplerInfo = { .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, .pNext = &ycbcrSamplerConversionInfo, .magFilter = VK_FILTER_LINEAR, .minFilter = VK_FILTER_LINEAR, .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, .borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK, }; vk.CreateSampler( device(), &ycbcrSamplerInfo, nullptr, &m_ycbcrSampler ); // Create an array of our ycbcrSampler to fill up std::array ycbcrSamplers; for (auto& sampler : ycbcrSamplers) sampler = m_ycbcrSampler; std::array layoutBindings = { VkDescriptorSetLayoutBinding { .binding = 0, .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, .descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, }, VkDescriptorSetLayoutBinding { .binding = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, }, VkDescriptorSetLayoutBinding { .binding = 2, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, }, VkDescriptorSetLayoutBinding { .binding = 3, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = VKR_SAMPLER_SLOTS, .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, }, VkDescriptorSetLayoutBinding { .binding = 4, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = VKR_SAMPLER_SLOTS, .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, .pImmutableSamplers = ycbcrSamplers.data(), }, VkDescriptorSetLayoutBinding { .binding = 5, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = VKR_LUT3D_COUNT, .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, }, VkDescriptorSetLayoutBinding { .binding = 6, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = VKR_LUT3D_COUNT, .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, }, }; VkDescriptorSetLayoutCreateInfo descriptorSetLayoutCreateInfo = { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, .bindingCount = (uint32_t)layoutBindings.size(), .pBindings = layoutBindings.data() }; VkResult res = vk.CreateDescriptorSetLayout(device(), &descriptorSetLayoutCreateInfo, 0, &m_descriptorSetLayout); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkCreateDescriptorSetLayout failed" ); return false; } VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = { .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 1, .pSetLayouts = &m_descriptorSetLayout, }; res = vk.CreatePipelineLayout(device(), &pipelineLayoutCreateInfo, nullptr, &m_pipelineLayout); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkCreatePipelineLayout failed" ); return false; } return true; } bool CVulkanDevice::createPools() { VkCommandPoolCreateInfo commandPoolCreateInfo = { .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, .queueFamilyIndex = m_queueFamily, }; VkResult res = vk.CreateCommandPool(device(), &commandPoolCreateInfo, nullptr, &m_commandPool); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkCreateCommandPool failed" ); return false; } VkCommandPoolCreateInfo generalCommandPoolCreateInfo = { .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, .queueFamilyIndex = m_generalQueueFamily, }; res = vk.CreateCommandPool(device(), &generalCommandPoolCreateInfo, nullptr, &m_generalCommandPool); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkCreateCommandPool failed" ); return false; } VkDescriptorPoolSize poolSizes[3] { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, uint32_t(m_descriptorSets.size()), }, { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, uint32_t(m_descriptorSets.size()) * 2, }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, uint32_t(m_descriptorSets.size()) * ((2 * VKR_SAMPLER_SLOTS) + (2 * VKR_LUT3D_COUNT)), }, }; VkDescriptorPoolCreateInfo descriptorPoolCreateInfo = { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, .maxSets = uint32_t(m_descriptorSets.size()), .poolSizeCount = sizeof(poolSizes) / sizeof(poolSizes[0]), .pPoolSizes = poolSizes, }; res = vk.CreateDescriptorPool(device(), &descriptorPoolCreateInfo, nullptr, &m_descriptorPool); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkCreateDescriptorPool failed" ); return false; } return true; } bool CVulkanDevice::createShaders() { struct ShaderInfo_t { const uint32_t* spirv; uint32_t size; }; std::array shaderInfos; #define SHADER(type, array) shaderInfos[SHADER_TYPE_##type] = {array , sizeof(array)} SHADER(BLIT, cs_composite_blit); SHADER(BLUR, cs_composite_blur); SHADER(BLUR_COND, cs_composite_blur_cond); SHADER(BLUR_FIRST_PASS, cs_gaussian_blur_horizontal); SHADER(RCAS, cs_composite_rcas); if (m_bSupportsFp16) { SHADER(EASU, cs_easu_fp16); SHADER(NIS, cs_nis_fp16); } else { SHADER(EASU, cs_easu); SHADER(NIS, cs_nis); } SHADER(RGB_TO_NV12, cs_rgb_to_nv12); #undef SHADER for (uint32_t i = 0; i < shaderInfos.size(); i++) { VkShaderModuleCreateInfo shaderCreateInfo = { .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = shaderInfos[i].size, .pCode = shaderInfos[i].spirv, }; VkResult res = vk.CreateShaderModule(device(), &shaderCreateInfo, nullptr, &m_shaderModules[i]); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkCreateShaderModule failed" ); return false; } } return true; } bool CVulkanDevice::createScratchResources() { std::vector descriptorSetLayouts(m_descriptorSets.size(), m_descriptorSetLayout); VkDescriptorSetAllocateInfo descriptorSetAllocateInfo = { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, .descriptorPool = m_descriptorPool, .descriptorSetCount = (uint32_t)descriptorSetLayouts.size(), .pSetLayouts = descriptorSetLayouts.data(), }; VkResult res = vk.AllocateDescriptorSets(device(), &descriptorSetAllocateInfo, m_descriptorSets.data()); if ( res != VK_SUCCESS ) { vk_log.errorf( "vkAllocateDescriptorSets failed" ); return false; } // Make and map upload buffer VkBufferCreateInfo bufferCreateInfo = { .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = upload_buffer_size, .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, }; res = vk.CreateBuffer( device(), &bufferCreateInfo, nullptr, &m_uploadBuffer ); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkCreateBuffer failed" ); return false; } VkMemoryRequirements memRequirements; vk.GetBufferMemoryRequirements(device(), m_uploadBuffer, &memRequirements); uint32_t memTypeIndex = findMemoryType(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT|VK_MEMORY_PROPERTY_HOST_COHERENT_BIT|VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, memRequirements.memoryTypeBits ); if ( memTypeIndex == ~0u ) { vk_log.errorf( "findMemoryType failed" ); return false; } VkMemoryAllocateInfo allocInfo = { .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .allocationSize = memRequirements.size, .memoryTypeIndex = memTypeIndex, }; vk.AllocateMemory( device(), &allocInfo, nullptr, &m_uploadBufferMemory); vk.BindBufferMemory( device(), m_uploadBuffer, m_uploadBufferMemory, 0 ); res = vk.MapMemory( device(), m_uploadBufferMemory, 0, VK_WHOLE_SIZE, 0, (void**)&m_uploadBufferData ); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkMapMemory failed" ); return false; } VkSemaphoreTypeCreateInfo timelineCreateInfo = { .sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO, .semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE, }; VkSemaphoreCreateInfo semCreateInfo = { .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, .pNext = &timelineCreateInfo, }; res = vk.CreateSemaphore( device(), &semCreateInfo, NULL, &m_scratchTimelineSemaphore ); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkCreateSemaphore failed" ); return false; } return true; } VkSampler CVulkanDevice::sampler( SamplerState key ) { if ( m_samplerCache.count(key) != 0 ) return m_samplerCache[key]; VkSampler ret = VK_NULL_HANDLE; VkSamplerCreateInfo samplerCreateInfo = { .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, .magFilter = key.bNearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR, .minFilter = key.bNearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR, .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, .borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK, .unnormalizedCoordinates = key.bUnnormalized, }; vk.CreateSampler( device(), &samplerCreateInfo, nullptr, &ret ); m_samplerCache[key] = ret; return ret; } VkPipeline CVulkanDevice::compilePipeline(uint32_t layerCount, uint32_t ycbcrMask, ShaderType type, uint32_t blur_layer_count, uint32_t composite_debug, uint32_t colorspace_mask, uint32_t output_eotf, bool itm_enable) { const std::array specializationEntries = {{ { .constantID = 0, .offset = sizeof(uint32_t) * 0, .size = sizeof(uint32_t) }, { .constantID = 1, .offset = sizeof(uint32_t) * 1, .size = sizeof(uint32_t) }, { .constantID = 2, .offset = sizeof(uint32_t) * 2, .size = sizeof(uint32_t) }, { .constantID = 3, .offset = sizeof(uint32_t) * 3, .size = sizeof(uint32_t) }, { .constantID = 4, .offset = sizeof(uint32_t) * 4, .size = sizeof(uint32_t) }, { .constantID = 5, .offset = sizeof(uint32_t) * 5, .size = sizeof(uint32_t) }, { .constantID = 6, .offset = sizeof(uint32_t) * 6, .size = sizeof(uint32_t) }, }}; struct { uint32_t layerCount; uint32_t ycbcrMask; uint32_t debug; uint32_t blur_layer_count; uint32_t colorspace_mask; uint32_t output_eotf; uint32_t itm_enable; } specializationData = { .layerCount = layerCount, .ycbcrMask = ycbcrMask, .debug = composite_debug, .blur_layer_count = blur_layer_count, .colorspace_mask = colorspace_mask, .output_eotf = output_eotf, .itm_enable = itm_enable, }; VkSpecializationInfo specializationInfo = { .mapEntryCount = uint32_t(specializationEntries.size()), .pMapEntries = specializationEntries.data(), .dataSize = sizeof(specializationData), .pData = &specializationData, }; VkComputePipelineCreateInfo computePipelineCreateInfo = { .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, .stage = { .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = VK_SHADER_STAGE_COMPUTE_BIT, .module = m_shaderModules[type], .pName = "main", .pSpecializationInfo = &specializationInfo }, .layout = m_pipelineLayout, }; VkPipeline result; VkResult res = vk.CreateComputePipelines(device(), VK_NULL_HANDLE, 1, &computePipelineCreateInfo, nullptr, &result); if (res != VK_SUCCESS) { vk_errorf( res, "vkCreateComputePipelines failed" ); return VK_NULL_HANDLE; } return result; } void CVulkanDevice::compileAllPipelines() { pthread_setname_np( pthread_self(), "gamescope-shdr" ); std::array pipelineInfos; #define SHADER(type, layer_count, max_ycbcr, blur_layers) pipelineInfos[SHADER_TYPE_##type] = {SHADER_TYPE_##type, layer_count, max_ycbcr, blur_layers} SHADER(BLIT, k_nMaxLayers, k_nMaxYcbcrMask_ToPreCompile, 1); SHADER(BLUR, k_nMaxLayers, k_nMaxYcbcrMask_ToPreCompile, k_nMaxBlurLayers); SHADER(BLUR_COND, k_nMaxLayers, k_nMaxYcbcrMask_ToPreCompile, k_nMaxBlurLayers); SHADER(BLUR_FIRST_PASS, 1, 2, 1); SHADER(RCAS, k_nMaxLayers, k_nMaxYcbcrMask_ToPreCompile, 1); SHADER(EASU, 1, 1, 1); SHADER(NIS, 1, 1, 1); SHADER(RGB_TO_NV12, 1, 1, 1); #undef SHADER for (auto& info : pipelineInfos) { for (uint32_t layerCount = 1; layerCount <= info.layerCount; layerCount++) { for (uint32_t ycbcrMask = 0; ycbcrMask < info.ycbcrMask; ycbcrMask++) { for (uint32_t blur_layers = 1; blur_layers <= info.blurLayerCount; blur_layers++) { if (ycbcrMask >= (1u << (layerCount + 1))) continue; if (blur_layers > layerCount) continue; VkPipeline newPipeline = compilePipeline(layerCount, ycbcrMask, info.shaderType, blur_layers, info.compositeDebug, info.colorspaceMask, info.outputEOTF, info.itmEnable); { std::lock_guard lock(m_pipelineMutex); PipelineInfo_t key = {info.shaderType, layerCount, ycbcrMask, blur_layers, info.compositeDebug}; auto result = m_pipelineMap.emplace(std::make_pair(key, newPipeline)); if (!result.second) vk.DestroyPipeline(device(), newPipeline, nullptr); } } } } } } extern bool g_bSteamIsActiveWindow; VkPipeline CVulkanDevice::pipeline(ShaderType type, uint32_t layerCount, uint32_t ycbcrMask, uint32_t blur_layers, uint32_t colorspace_mask, uint32_t output_eotf, bool itm_enable) { uint32_t effective_debug = g_uCompositeDebug; if ( g_bSteamIsActiveWindow ) effective_debug &= ~(CompositeDebugFlag::Heatmap | CompositeDebugFlag::Heatmap_MSWCG | CompositeDebugFlag::Heatmap_Hard); std::lock_guard lock(m_pipelineMutex); PipelineInfo_t key = {type, layerCount, ycbcrMask, blur_layers, effective_debug, colorspace_mask, output_eotf, itm_enable}; auto search = m_pipelineMap.find(key); if (search == m_pipelineMap.end()) { VkPipeline result = compilePipeline(layerCount, ycbcrMask, type, blur_layers, effective_debug, colorspace_mask, output_eotf, itm_enable); m_pipelineMap[key] = result; return result; } else { return search->second; } } int32_t CVulkanDevice::findMemoryType( VkMemoryPropertyFlags properties, uint32_t requiredTypeBits ) { for ( uint32_t i = 0; i < m_memoryProperties.memoryTypeCount; i++ ) { if ( ( ( 1 << i ) & requiredTypeBits ) == 0 ) continue; if ( ( properties & m_memoryProperties.memoryTypes[ i ].propertyFlags ) != properties ) continue; return i; } return -1; } std::unique_ptr CVulkanDevice::commandBuffer() { std::unique_ptr cmdBuffer; if (m_unusedCmdBufs.empty()) { VkCommandBuffer rawCmdBuffer; VkCommandBufferAllocateInfo commandBufferAllocateInfo = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, .commandPool = m_commandPool, .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, .commandBufferCount = 1 }; VkResult res = vk.AllocateCommandBuffers( device(), &commandBufferAllocateInfo, &rawCmdBuffer ); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkAllocateCommandBuffers failed" ); return nullptr; } cmdBuffer = std::make_unique(this, rawCmdBuffer, queue(), queueFamily()); } else { cmdBuffer = std::move(m_unusedCmdBufs.back()); m_unusedCmdBufs.pop_back(); } cmdBuffer->begin(); return cmdBuffer; } uint64_t CVulkanDevice::submitInternal( CVulkanCmdBuffer* cmdBuffer ) { cmdBuffer->end(); // The seq no of the last submission. const uint64_t lastSubmissionSeqNo = m_submissionSeqNo++; // This is the seq no of the command buffer we are going to submit. const uint64_t nextSeqNo = lastSubmissionSeqNo + 1; std::vector pSignalSemaphores; std::vector ulSignalPoints; std::vector uWaitStageFlags; std::vector pWaitSemaphores; std::vector ulWaitPoints; pSignalSemaphores.push_back( m_scratchTimelineSemaphore ); ulSignalPoints.push_back( nextSeqNo ); for ( auto &dep : cmdBuffer->GetExternalSignals() ) { pSignalSemaphores.push_back( dep.pTimelineSemaphore->pVkSemaphore ); ulSignalPoints.push_back( dep.ulPoint ); } for ( auto &dep : cmdBuffer->GetExternalDependencies() ) { pWaitSemaphores.push_back( dep.pTimelineSemaphore->pVkSemaphore ); ulWaitPoints.push_back( dep.ulPoint ); uWaitStageFlags.push_back( VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT ); } VkTimelineSemaphoreSubmitInfo timelineInfo = { .sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO, // no need to ensure order of cmd buffer submission, we only have one queue .waitSemaphoreValueCount = static_cast( ulWaitPoints.size() ), .pWaitSemaphoreValues = ulWaitPoints.data(), .signalSemaphoreValueCount = static_cast( ulSignalPoints.size() ), .pSignalSemaphoreValues = ulSignalPoints.data(), }; VkCommandBuffer rawCmdBuffer = cmdBuffer->rawBuffer(); VkSubmitInfo submitInfo = { .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .pNext = &timelineInfo, .waitSemaphoreCount = static_cast( pWaitSemaphores.size() ), .pWaitSemaphores = pWaitSemaphores.data(), .pWaitDstStageMask = uWaitStageFlags.data(), .commandBufferCount = 1, .pCommandBuffers = &rawCmdBuffer, .signalSemaphoreCount = static_cast( pSignalSemaphores.size() ), .pSignalSemaphores = pSignalSemaphores.data(), }; vk_check( vk.QueueSubmit( cmdBuffer->queue(), 1, &submitInfo, VK_NULL_HANDLE ) ); return nextSeqNo; } uint64_t CVulkanDevice::submit( std::unique_ptr cmdBuffer) { uint64_t nextSeqNo = submitInternal(cmdBuffer.get()); m_pendingCmdBufs.emplace(nextSeqNo, std::move(cmdBuffer)); return nextSeqNo; } void CVulkanDevice::garbageCollect( void ) { uint64_t currentSeqNo; vk_check( vk.GetSemaphoreCounterValue(device(), m_scratchTimelineSemaphore, ¤tSeqNo) ); resetCmdBuffers(currentSeqNo); } VulkanTimelineSemaphore_t::~VulkanTimelineSemaphore_t() { if ( pVkSemaphore != VK_NULL_HANDLE ) { pDevice->vk.DestroySemaphore( pDevice->device(), pVkSemaphore, nullptr ); pVkSemaphore = VK_NULL_HANDLE; } } int VulkanTimelineSemaphore_t::GetFd() const { const VkSemaphoreGetFdInfoKHR semaphoreGetInfo = { .sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR, .semaphore = pVkSemaphore, .handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT, }; int32_t nFd = -1; VkResult res = VK_SUCCESS; if ( ( res = pDevice->vk.GetSemaphoreFdKHR( pDevice->device(), &semaphoreGetInfo, &nFd ) ) != VK_SUCCESS ) { vk_errorf( res, "vkGetSemaphoreFdKHR failed" ); return -1; } return nFd; } std::shared_ptr CVulkanDevice::CreateTimelineSemaphore( uint64_t ulStartPoint, bool bShared ) { std::shared_ptr pSemaphore = std::make_unique(); pSemaphore->pDevice = this; VkSemaphoreCreateInfo createInfo = { .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, }; VkSemaphoreTypeCreateInfo typeInfo = { .sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO, .pNext = std::exchange( createInfo.pNext, &typeInfo ), .semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE, .initialValue = ulStartPoint, }; VkExportSemaphoreCreateInfo exportInfo = { .sType = VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO, .pNext = bShared ? std::exchange( createInfo.pNext, &exportInfo ) : nullptr, // This is a syncobj fd for any drivers using syncobj. .handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT, }; VkResult res; if ( ( res = vk.CreateSemaphore( m_device, &createInfo, nullptr, &pSemaphore->pVkSemaphore ) ) != VK_SUCCESS ) { vk_errorf( res, "vkCreateSemaphore failed" ); return nullptr; } return pSemaphore; } std::shared_ptr CVulkanDevice::ImportTimelineSemaphore( gamescope::CTimeline *pTimeline ) { std::shared_ptr pSemaphore = std::make_unique(); pSemaphore->pDevice = this; const VkSemaphoreTypeCreateInfo typeInfo = { .sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO, .semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE, }; const VkSemaphoreCreateInfo createInfo = { .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, .pNext = &typeInfo, }; VkResult res; if ( ( res = vk.CreateSemaphore( m_device, &createInfo, nullptr, &pSemaphore->pVkSemaphore ) ) != VK_SUCCESS ) { vk_errorf( res, "vkCreateSemaphore failed" ); return nullptr; } // "Importing a semaphore payload from a file descriptor transfers // ownership of the file descriptor from the application to the Vulkan // implementation. The application must not perform any operations on // the file descriptor after a successful import." // // Thus, we must dup. VkImportSemaphoreFdInfoKHR importFdInfo = { .sType = VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_FD_INFO_KHR, .pNext = nullptr, .semaphore = pSemaphore->pVkSemaphore, .flags = 0, // not temporary .handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT, .fd = dup( pTimeline->GetSyncobjFd() ), }; if ( ( res = vk.ImportSemaphoreFdKHR( m_device, &importFdInfo ) ) != VK_SUCCESS ) { vk_errorf( res, "vkImportSemaphoreFdKHR failed" ); return nullptr; } return pSemaphore; } void CVulkanCmdBuffer::AddDependency( std::shared_ptr pTimelineSemaphore, uint64_t ulPoint ) { m_ExternalDependencies.emplace_back( std::move( pTimelineSemaphore ), ulPoint ); } void CVulkanCmdBuffer::AddSignal( std::shared_ptr pTimelineSemaphore, uint64_t ulPoint ) { m_ExternalSignals.emplace_back( std::move( pTimelineSemaphore ), ulPoint ); } void CVulkanDevice::wait(uint64_t sequence, bool reset) { if (m_submissionSeqNo == sequence) m_uploadBufferOffset = 0; VkSemaphoreWaitInfo waitInfo = { .sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO, .semaphoreCount = 1, .pSemaphores = &m_scratchTimelineSemaphore, .pValues = &sequence, } ; vk_check( vk.WaitSemaphores( device(), &waitInfo, ~0ull ) ); if (reset) resetCmdBuffers(sequence); } void CVulkanDevice::waitIdle(bool reset) { wait(m_submissionSeqNo, reset); } void CVulkanDevice::resetCmdBuffers(uint64_t sequence) { auto last = m_pendingCmdBufs.find(sequence); if (last == m_pendingCmdBufs.end()) return; for (auto it = m_pendingCmdBufs.begin(); ; it++) { it->second->reset(); m_unusedCmdBufs.push_back(std::move(it->second)); if (it == last) break; } m_pendingCmdBufs.erase(m_pendingCmdBufs.begin(), ++last); } CVulkanCmdBuffer::CVulkanCmdBuffer(CVulkanDevice *parent, VkCommandBuffer cmdBuffer, VkQueue queue, uint32_t queueFamily) : m_cmdBuffer(cmdBuffer), m_device(parent), m_queue(queue), m_queueFamily(queueFamily) { } CVulkanCmdBuffer::~CVulkanCmdBuffer() { m_device->vk.FreeCommandBuffers(m_device->device(), m_device->commandPool(), 1, &m_cmdBuffer); } void CVulkanCmdBuffer::reset() { vk_check( m_device->vk.ResetCommandBuffer(m_cmdBuffer, 0) ); m_textureRefs.clear(); m_textureState.clear(); m_ExternalDependencies.clear(); m_ExternalSignals.clear(); } void CVulkanCmdBuffer::begin() { VkCommandBufferBeginInfo commandBufferBeginInfo = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT }; vk_check( m_device->vk.BeginCommandBuffer(m_cmdBuffer, &commandBufferBeginInfo) ); clearState(); } void CVulkanCmdBuffer::end() { insertBarrier(true); vk_check( m_device->vk.EndCommandBuffer(m_cmdBuffer) ); } void CVulkanCmdBuffer::bindTexture(uint32_t slot, gamescope::Rc texture) { m_boundTextures[slot] = texture.get(); if (texture) m_textureRefs.emplace_back(std::move(texture)); } void CVulkanCmdBuffer::bindColorMgmtLuts(uint32_t slot, gamescope::Rc lut1d, gamescope::Rc lut3d) { m_shaperLut[slot] = lut1d.get(); m_lut3D[slot] = lut3d.get(); if (lut1d != nullptr) m_textureRefs.emplace_back(std::move(lut1d)); if (lut3d != nullptr) m_textureRefs.emplace_back(std::move(lut3d)); } void CVulkanCmdBuffer::setTextureSrgb(uint32_t slot, bool srgb) { m_useSrgb[slot] = srgb; } void CVulkanCmdBuffer::setSamplerNearest(uint32_t slot, bool nearest) { m_samplerState[slot].bNearest = nearest; } void CVulkanCmdBuffer::setSamplerUnnormalized(uint32_t slot, bool unnormalized) { m_samplerState[slot].bUnnormalized = unnormalized; } void CVulkanCmdBuffer::bindTarget(gamescope::Rc target) { m_target = target.get(); if (target) m_textureRefs.emplace_back(std::move(target)); } void CVulkanCmdBuffer::clearState() { for (auto& texture : m_boundTextures) texture = nullptr; for (auto& sampler : m_samplerState) sampler = {}; m_target = nullptr; m_useSrgb.reset(); } template void CVulkanCmdBuffer::uploadConstants(Args&&... args) { PushData data(std::forward(args)...); auto [ptr, offset] = m_device->uploadBufferData(sizeof(data)); m_renderBufferOffset = offset; memcpy(ptr, &data, sizeof(data)); } void CVulkanCmdBuffer::bindPipeline(VkPipeline pipeline) { m_device->vk.CmdBindPipeline(m_cmdBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); } void CVulkanCmdBuffer::dispatch(uint32_t x, uint32_t y, uint32_t z) { for (auto src : m_boundTextures) { if (src) prepareSrcImage(src); } assert(m_target != nullptr); prepareDestImage(m_target); insertBarrier(); VkDescriptorSet descriptorSet = m_device->descriptorSet(); std::array writeDescriptorSets; std::array imageDescriptors = {}; std::array ycbcrImageDescriptors = {}; std::array targetDescriptors = {}; std::array shaperLutDescriptor = {}; std::array lut3DDescriptor = {}; VkDescriptorBufferInfo scratchDescriptor = {}; writeDescriptorSets[0] = { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = descriptorSet, .dstBinding = 0, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, .pBufferInfo = &scratchDescriptor, }; writeDescriptorSets[1] = { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = descriptorSet, .dstBinding = 1, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .pImageInfo = &targetDescriptors[0], }; writeDescriptorSets[2] = { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = descriptorSet, .dstBinding = 2, .dstArrayElement = 0, .descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .pImageInfo = &targetDescriptors[1], }; writeDescriptorSets[3] = { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = descriptorSet, .dstBinding = 3, .dstArrayElement = 0, .descriptorCount = imageDescriptors.size(), .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .pImageInfo = imageDescriptors.data(), }; writeDescriptorSets[4] = { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = descriptorSet, .dstBinding = 4, .dstArrayElement = 0, .descriptorCount = ycbcrImageDescriptors.size(), .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .pImageInfo = ycbcrImageDescriptors.data(), }; writeDescriptorSets[5] = { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = descriptorSet, .dstBinding = 5, .dstArrayElement = 0, .descriptorCount = shaperLutDescriptor.size(), .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .pImageInfo = shaperLutDescriptor.data(), }; writeDescriptorSets[6] = { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = descriptorSet, .dstBinding = 6, .dstArrayElement = 0, .descriptorCount = lut3DDescriptor.size(), .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .pImageInfo = lut3DDescriptor.data(), }; scratchDescriptor.buffer = m_device->m_uploadBuffer; scratchDescriptor.offset = m_renderBufferOffset; scratchDescriptor.range = VK_WHOLE_SIZE; for (uint32_t i = 0; i < VKR_SAMPLER_SLOTS; i++) { imageDescriptors[i].sampler = m_device->sampler(m_samplerState[i]); imageDescriptors[i].imageLayout = VK_IMAGE_LAYOUT_GENERAL; ycbcrImageDescriptors[i].imageLayout = VK_IMAGE_LAYOUT_GENERAL; if (m_boundTextures[i] == nullptr) continue; VkImageView view = m_useSrgb[i] ? m_boundTextures[i]->srgbView() : m_boundTextures[i]->linearView(); if (m_boundTextures[i]->format() == VK_FORMAT_G8_B8R8_2PLANE_420_UNORM) ycbcrImageDescriptors[i].imageView = view; else imageDescriptors[i].imageView = view; } for (uint32_t i = 0; i < VKR_LUT3D_COUNT; i++) { SamplerState linearState; linearState.bNearest = false; linearState.bUnnormalized = false; SamplerState nearestState; // TODO(Josh): Probably want to do this when I bring in tetrahedral interpolation. nearestState.bNearest = true; nearestState.bUnnormalized = false; shaperLutDescriptor[i].sampler = m_device->sampler(linearState); shaperLutDescriptor[i].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; // TODO(Josh): I hate the fact that srgbView = view *as* raw srgb and treat as linear. // I need to change this, it's so utterly stupid and confusing. shaperLutDescriptor[i].imageView = m_shaperLut[i] ? m_shaperLut[i]->srgbView() : VK_NULL_HANDLE; lut3DDescriptor[i].sampler = m_device->sampler(nearestState); lut3DDescriptor[i].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; lut3DDescriptor[i].imageView = m_lut3D[i] ? m_lut3D[i]->srgbView() : VK_NULL_HANDLE; } if (!m_target->isYcbcr()) { targetDescriptors[0].imageView = m_target->srgbView(); targetDescriptors[0].imageLayout = VK_IMAGE_LAYOUT_GENERAL; } else { targetDescriptors[0].imageView = m_target->lumaView(); targetDescriptors[0].imageLayout = VK_IMAGE_LAYOUT_GENERAL; targetDescriptors[1].imageView = m_target->chromaView(); targetDescriptors[1].imageLayout = VK_IMAGE_LAYOUT_GENERAL; } m_device->vk.UpdateDescriptorSets(m_device->device(), writeDescriptorSets.size(), writeDescriptorSets.data(), 0, nullptr); m_device->vk.CmdBindDescriptorSets(m_cmdBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, m_device->pipelineLayout(), 0, 1, &descriptorSet, 0, nullptr); m_device->vk.CmdDispatch(m_cmdBuffer, x, y, z); markDirty(m_target); } void CVulkanCmdBuffer::copyImage(gamescope::Rc src, gamescope::Rc dst) { assert(src->width() == dst->width()); assert(src->height() == dst->height()); prepareSrcImage(src.get()); prepareDestImage(dst.get()); insertBarrier(); VkImageCopy region = { .srcSubresource = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .layerCount = 1 }, .dstSubresource = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .layerCount = 1 }, .extent = { .width = src->width(), .height = src->height(), .depth = 1 }, }; m_device->vk.CmdCopyImage(m_cmdBuffer, src->vkImage(), VK_IMAGE_LAYOUT_GENERAL, dst->vkImage(), VK_IMAGE_LAYOUT_GENERAL, 1, ®ion); markDirty(dst.get()); m_textureRefs.emplace_back(std::move(src)); m_textureRefs.emplace_back(std::move(dst)); } void CVulkanCmdBuffer::copyBufferToImage(VkBuffer buffer, VkDeviceSize offset, uint32_t stride, gamescope::Rc dst) { prepareDestImage(dst.get()); insertBarrier(); VkBufferImageCopy region = { .bufferOffset = offset, .bufferRowLength = stride, .imageSubresource = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .layerCount = 1, }, .imageExtent = { .width = dst->width(), .height = dst->height(), .depth = dst->depth(), }, }; m_device->vk.CmdCopyBufferToImage(m_cmdBuffer, buffer, dst->vkImage(), VK_IMAGE_LAYOUT_GENERAL, 1, ®ion); markDirty(dst.get()); m_textureRefs.emplace_back(std::move(dst)); } void CVulkanCmdBuffer::prepareSrcImage(CVulkanTexture *image) { auto result = m_textureState.emplace(image, TextureState()); // no need to reimport if the image didn't change if (!result.second) return; // using the swapchain image as a source without writing to it doesn't make any sense assert(image->outputImage() == false); result.first->second.needsImport = image->externalImage(); result.first->second.needsExport = image->externalImage(); } void CVulkanCmdBuffer::prepareDestImage(CVulkanTexture *image) { auto result = m_textureState.emplace(image, TextureState()); // no need to discard if the image is already image/in the correct layout if (!result.second) return; result.first->second.discarded = true; result.first->second.needsExport = image->externalImage(); result.first->second.needsPresentLayout = image->outputImage(); } void CVulkanCmdBuffer::discardImage(CVulkanTexture *image) { auto result = m_textureState.emplace(image, TextureState()); if (!result.second) return; result.first->second.discarded = true; } void CVulkanCmdBuffer::markDirty(CVulkanTexture *image) { auto result = m_textureState.find(image); // image should have been prepared already assert(result != m_textureState.end()); result->second.dirty = true; } void CVulkanCmdBuffer::insertBarrier(bool flush) { std::vector barriers; uint32_t externalQueue = m_device->supportsModifiers() ? VK_QUEUE_FAMILY_FOREIGN_EXT : VK_QUEUE_FAMILY_EXTERNAL_KHR; VkImageSubresourceRange subResRange = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .levelCount = 1, .layerCount = 1 }; for (auto& pair : m_textureState) { CVulkanTexture *image = pair.first; TextureState& state = pair.second; assert(!flush || !state.needsImport); bool isExport = flush && state.needsExport; bool isPresent = flush && state.needsPresentLayout; if (!state.discarded && !state.dirty && !state.needsImport && !isExport && !isPresent) continue; const VkAccessFlags write_bits = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; const VkAccessFlags read_bits = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT; if (image->queueFamily == VK_QUEUE_FAMILY_IGNORED) image->queueFamily = m_queueFamily; VkImageMemoryBarrier memoryBarrier = { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .srcAccessMask = state.dirty ? write_bits : 0u, .dstAccessMask = flush ? 0u : read_bits | write_bits, .oldLayout = state.discarded ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_GENERAL, .newLayout = isPresent ? GetBackend()->GetPresentLayout() : VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = isExport ? image->queueFamily : state.needsImport ? externalQueue : image->queueFamily, .dstQueueFamilyIndex = isExport ? externalQueue : state.needsImport ? m_queueFamily : m_queueFamily, .image = image->vkImage(), .subresourceRange = subResRange }; barriers.push_back(memoryBarrier); state.discarded = false; state.dirty = false; state.needsImport = false; } // TODO replace VK_PIPELINE_STAGE_ALL_COMMANDS_BIT m_device->vk.CmdPipelineBarrier(m_cmdBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, barriers.size(), barriers.data()); } CVulkanDevice g_device; static bool allDMABUFsEqual( wlr_dmabuf_attributes *pDMA ) { if ( pDMA->n_planes == 1 ) return true; struct stat first_stat; if ( fstat( pDMA->fd[0], &first_stat ) != 0 ) { vk_log.errorf_errno( "fstat failed" ); return false; } for ( int i = 1; i < pDMA->n_planes; ++i ) { struct stat plane_stat; if ( fstat( pDMA->fd[i], &plane_stat ) != 0 ) { vk_log.errorf_errno( "fstat failed" ); return false; } if ( plane_stat.st_ino != first_stat.st_ino ) return false; } return true; } static VkResult getModifierProps( const VkImageCreateInfo *imageInfo, uint64_t modifier, VkExternalImageFormatProperties *externalFormatProps) { VkPhysicalDeviceImageDrmFormatModifierInfoEXT modifierFormatInfo = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_DRM_FORMAT_MODIFIER_INFO_EXT, .drmFormatModifier = modifier, .sharingMode = imageInfo->sharingMode, }; VkPhysicalDeviceExternalImageFormatInfo externalImageFormatInfo = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO, .pNext = &modifierFormatInfo, .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, }; VkPhysicalDeviceImageFormatInfo2 imageFormatInfo = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2, .pNext = &externalImageFormatInfo, .format = imageInfo->format, .type = imageInfo->imageType, .tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT, .usage = imageInfo->usage, .flags = imageInfo->flags, }; const VkImageFormatListCreateInfo *readonlyList = pNextFind(imageInfo, VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO); VkImageFormatListCreateInfo formatList = {}; if ( readonlyList != nullptr ) { formatList = *readonlyList; formatList.pNext = std::exchange(imageFormatInfo.pNext, &formatList); } VkImageFormatProperties2 imageProps = { .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2, .pNext = externalFormatProps, }; return g_device.vk.GetPhysicalDeviceImageFormatProperties2(g_device.physDev(), &imageFormatInfo, &imageProps); } static VkImageViewType VulkanImageTypeToViewType(VkImageType type) { switch (type) { case VK_IMAGE_TYPE_1D: return VK_IMAGE_VIEW_TYPE_1D; case VK_IMAGE_TYPE_2D: return VK_IMAGE_VIEW_TYPE_2D; case VK_IMAGE_TYPE_3D: return VK_IMAGE_VIEW_TYPE_3D; default: abort(); } } bool CVulkanTexture::BInit( uint32_t width, uint32_t height, uint32_t depth, uint32_t drmFormat, createFlags flags, wlr_dmabuf_attributes *pDMA /* = nullptr */, uint32_t contentWidth /* = 0 */, uint32_t contentHeight /* = 0 */, CVulkanTexture *pExistingImageToReuseMemory, gamescope::OwningRc pBackendFb ) { m_pBackendFb = std::move( pBackendFb ); m_drmFormat = drmFormat; VkResult res = VK_ERROR_INITIALIZATION_FAILED; VkImageTiling tiling = (flags.bMappable || flags.bLinear) ? VK_IMAGE_TILING_LINEAR : VK_IMAGE_TILING_OPTIMAL; VkImageUsageFlags usage = 0; VkMemoryPropertyFlags properties; if ( flags.bSampled == true ) { usage |= VK_IMAGE_USAGE_SAMPLED_BIT; } if ( flags.bStorage == true ) { usage |= VK_IMAGE_USAGE_STORAGE_BIT; } if ( flags.bColorAttachment == true ) { usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; } if ( flags.bFlippable == true ) { flags.bExportable = true; } if ( flags.bTransferSrc == true ) { usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; } if ( flags.bTransferDst == true ) { usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; } if ( flags.bMappable == true ) { properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT; } else { properties = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; } if ( flags.bOutputImage == true ) { m_bOutputImage = true; } m_bExternal = pDMA || flags.bExportable == true; // Possible extensions for below wsi_image_create_info wsiImageCreateInfo = {}; VkExternalMemoryImageCreateInfo externalImageCreateInfo = {}; VkImageDrmFormatModifierExplicitCreateInfoEXT modifierInfo = {}; VkSubresourceLayout modifierPlaneLayouts[4] = {}; VkImageDrmFormatModifierListCreateInfoEXT modifierListInfo = {}; VkImageCreateInfo imageInfo = { .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = flags.imageType, .format = DRMFormatToVulkan(drmFormat, false), .extent = { .width = width, .height = height, .depth = depth, }, .mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, .tiling = tiling, .usage = usage, .sharingMode = VK_SHARING_MODE_EXCLUSIVE, }; assert( imageInfo.format != VK_FORMAT_UNDEFINED ); std::array formats = { DRMFormatToVulkan(drmFormat, false), DRMFormatToVulkan(drmFormat, true), }; VkImageFormatListCreateInfo formatList = { .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO, .viewFormatCount = (uint32_t)formats.size(), .pViewFormats = formats.data(), }; if ( formats[0] != formats[1] ) { formatList.pNext = std::exchange(imageInfo.pNext, &formatList); imageInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; } if ( pDMA != nullptr ) { assert( drmFormat == pDMA->format ); } if ( g_device.supportsModifiers() && pDMA && pDMA->modifier != DRM_FORMAT_MOD_INVALID ) { VkExternalImageFormatProperties externalImageProperties = { .sType = VK_STRUCTURE_TYPE_EXTERNAL_IMAGE_FORMAT_PROPERTIES, }; res = getModifierProps( &imageInfo, pDMA->modifier, &externalImageProperties ); if ( res != VK_SUCCESS && res != VK_ERROR_FORMAT_NOT_SUPPORTED ) { vk_errorf( res, "getModifierProps failed" ); return false; } if ( res == VK_SUCCESS && ( externalImageProperties.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT ) ) { modifierInfo = { .sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT, .pNext = std::exchange(imageInfo.pNext, &modifierInfo), .drmFormatModifier = pDMA->modifier, .drmFormatModifierPlaneCount = uint32_t(pDMA->n_planes), .pPlaneLayouts = modifierPlaneLayouts, }; for ( int i = 0; i < pDMA->n_planes; ++i ) { modifierPlaneLayouts[i].offset = pDMA->offset[i]; modifierPlaneLayouts[i].rowPitch = pDMA->stride[i]; } imageInfo.tiling = tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT; } } std::vector modifiers = {}; // TODO(JoshA): Move this code to backend for making flippable image. if ( GetBackend()->UsesModifiers() && flags.bFlippable && g_device.supportsModifiers() && !pDMA ) { assert( drmFormat != DRM_FORMAT_INVALID ); uint64_t linear = DRM_FORMAT_MOD_LINEAR; const uint64_t *possibleModifiers; size_t numPossibleModifiers; if ( flags.bLinear ) { possibleModifiers = &linear; numPossibleModifiers = 1; } else { std::span modifiers = GetBackend()->GetSupportedModifiers( drmFormat ); assert( !modifiers.empty() ); possibleModifiers = modifiers.data(); numPossibleModifiers = modifiers.size(); } for ( size_t i = 0; i < numPossibleModifiers; i++ ) { uint64_t modifier = possibleModifiers[i]; VkExternalImageFormatProperties externalFormatProps = { .sType = VK_STRUCTURE_TYPE_EXTERNAL_IMAGE_FORMAT_PROPERTIES, }; res = getModifierProps( &imageInfo, modifier, &externalFormatProps ); if ( res == VK_ERROR_FORMAT_NOT_SUPPORTED ) continue; else if ( res != VK_SUCCESS ) { vk_errorf( res, "getModifierProps failed" ); return false; } if ( !( externalFormatProps.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT ) ) continue; modifiers.push_back( modifier ); } assert( modifiers.size() > 0 ); modifierListInfo = { .sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_LIST_CREATE_INFO_EXT, .pNext = std::exchange(imageInfo.pNext, &modifierListInfo), .drmFormatModifierCount = uint32_t(modifiers.size()), .pDrmFormatModifiers = modifiers.data(), }; externalImageCreateInfo = { .sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO, .pNext = std::exchange(imageInfo.pNext, &externalImageCreateInfo), .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, }; imageInfo.tiling = tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT; } if ( flags.bFlippable == true && tiling != VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT ) { // We want to scan-out the image wsiImageCreateInfo = { .sType = VK_STRUCTURE_TYPE_WSI_IMAGE_CREATE_INFO_MESA, .pNext = std::exchange(imageInfo.pNext, &wsiImageCreateInfo), .scanout = VK_TRUE, }; } if ( pDMA != nullptr ) { externalImageCreateInfo = { .sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO, .pNext = std::exchange(imageInfo.pNext, &externalImageCreateInfo), .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, }; } m_width = width; m_height = height; m_depth = depth; if (contentWidth && contentHeight) { m_contentWidth = contentWidth; m_contentHeight = contentHeight; } else { m_contentWidth = width; m_contentHeight = height; } m_format = imageInfo.format; res = g_device.vk.CreateImage(g_device.device(), &imageInfo, nullptr, &m_vkImage); if (res != VK_SUCCESS) { vk_errorf( res, "vkCreateImage failed" ); return false; } VkMemoryRequirements memRequirements; g_device.vk.GetImageMemoryRequirements(g_device.device(), m_vkImage, &memRequirements); VkMemoryAllocateInfo allocInfo = { .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .allocationSize = memRequirements.size, .memoryTypeIndex = uint32_t(g_device.findMemoryType(properties, memRequirements.memoryTypeBits)), }; m_size = allocInfo.allocationSize; VkDeviceMemory memoryHandle = VK_NULL_HANDLE; if ( pExistingImageToReuseMemory == nullptr ) { // Possible pNexts VkImportMemoryFdInfoKHR importMemoryInfo = {}; VkExportMemoryAllocateInfo memory_export_info = {}; VkMemoryDedicatedAllocateInfo memory_dedicated_info = {}; struct wsi_memory_allocate_info memory_wsi_info = {}; if ( flags.bFlippable == true ) { memory_wsi_info = { .sType = VK_STRUCTURE_TYPE_WSI_MEMORY_ALLOCATE_INFO_MESA, .pNext = std::exchange(allocInfo.pNext, &memory_wsi_info), }; } if ( flags.bExportable == true || pDMA != nullptr ) { memory_dedicated_info = { .sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO, .pNext = std::exchange(allocInfo.pNext, &memory_dedicated_info), .image = m_vkImage, }; } if ( flags.bExportable == true && pDMA == nullptr ) { // We'll export it to DRM memory_export_info = { .sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO, .pNext = std::exchange(allocInfo.pNext, &memory_export_info), .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, }; } if ( pDMA != nullptr ) { // TODO: multi-planar DISTINCT DMA-BUFs support (see vkBindImageMemory2 // and VkBindImagePlaneMemoryInfo) assert( allDMABUFsEqual( pDMA ) ); // Importing memory from a FD transfers ownership of the FD int fd = dup( pDMA->fd[0] ); if ( fd < 0 ) { vk_log.errorf_errno( "dup failed" ); return false; } // Memory already provided by pDMA importMemoryInfo = { .sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR, .pNext = std::exchange(allocInfo.pNext, &importMemoryInfo), .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, .fd = fd, }; } res = g_device.vk.AllocateMemory( g_device.device(), &allocInfo, nullptr, &memoryHandle ); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkAllocateMemory failed" ); return false; } m_vkImageMemory = memoryHandle; } else { vk_log.infof("%d vs %d!", (int)pExistingImageToReuseMemory->m_size, (int)m_size); assert(pExistingImageToReuseMemory->m_size >= m_size); memoryHandle = pExistingImageToReuseMemory->m_vkImageMemory; m_vkImageMemory = VK_NULL_HANDLE; } res = g_device.vk.BindImageMemory( g_device.device(), m_vkImage, memoryHandle, 0 ); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkBindImageMemory failed" ); return false; } if ( flags.bMappable == true ) { assert( tiling != VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT ); const VkImageSubresource image_subresource = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, }; VkSubresourceLayout image_layout; g_device.vk.GetImageSubresourceLayout(g_device.device(), m_vkImage, &image_subresource, &image_layout); m_unRowPitch = image_layout.rowPitch; if (isYcbcr()) { const VkImageSubresource lumaSubresource = { .aspectMask = VK_IMAGE_ASPECT_PLANE_0_BIT, }; VkSubresourceLayout lumaLayout; g_device.vk.GetImageSubresourceLayout(g_device.device(), m_vkImage, &lumaSubresource, &lumaLayout); m_lumaOffset = lumaLayout.offset; m_lumaPitch = lumaLayout.rowPitch; const VkImageSubresource chromaSubresource = { .aspectMask = VK_IMAGE_ASPECT_PLANE_1_BIT, }; VkSubresourceLayout chromaLayout; g_device.vk.GetImageSubresourceLayout(g_device.device(), m_vkImage, &chromaSubresource, &chromaLayout); m_chromaOffset = chromaLayout.offset; m_chromaPitch = chromaLayout.rowPitch; } } if ( flags.bExportable == true ) { // We assume we own the memory when doing this right now. // We could support the import scenario as well if needed (but we // already have a DMA-BUF in that case). assert( pDMA == nullptr ); struct wlr_dmabuf_attributes dmabuf = { .width = int(width), .height = int(height), .format = drmFormat, }; assert( dmabuf.format != DRM_FORMAT_INVALID ); // TODO: disjoint planes support const VkMemoryGetFdInfoKHR memory_get_fd_info = { .sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR, .memory = memoryHandle, .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, }; res = g_device.vk.GetMemoryFdKHR(g_device.device(), &memory_get_fd_info, &dmabuf.fd[0]); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkGetMemoryFdKHR failed" ); return false; } if ( tiling == VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT ) { assert( g_device.vk.GetImageDrmFormatModifierPropertiesEXT != nullptr ); VkImageDrmFormatModifierPropertiesEXT imgModifierProps = { .sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_PROPERTIES_EXT, }; res = g_device.vk.GetImageDrmFormatModifierPropertiesEXT( g_device.device(), m_vkImage, &imgModifierProps ); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkGetImageDrmFormatModifierPropertiesEXT failed" ); return false; } dmabuf.modifier = imgModifierProps.drmFormatModifier; assert( DRMModifierProps.count( m_format ) > 0); assert( DRMModifierProps[ m_format ].count( dmabuf.modifier ) > 0); dmabuf.n_planes = DRMModifierProps[ m_format ][ dmabuf.modifier ].drmFormatModifierPlaneCount; const VkImageAspectFlagBits planeAspects[] = { VK_IMAGE_ASPECT_MEMORY_PLANE_0_BIT_EXT, VK_IMAGE_ASPECT_MEMORY_PLANE_1_BIT_EXT, VK_IMAGE_ASPECT_MEMORY_PLANE_2_BIT_EXT, VK_IMAGE_ASPECT_MEMORY_PLANE_3_BIT_EXT, }; assert( dmabuf.n_planes <= 4 ); for ( int i = 0; i < dmabuf.n_planes; i++ ) { const VkImageSubresource subresource = { .aspectMask = planeAspects[i], }; VkSubresourceLayout subresourceLayout = {}; g_device.vk.GetImageSubresourceLayout( g_device.device(), m_vkImage, &subresource, &subresourceLayout ); dmabuf.offset[i] = subresourceLayout.offset; dmabuf.stride[i] = subresourceLayout.rowPitch; } // Copy the first FD to all other planes for ( int i = 1; i < dmabuf.n_planes; i++ ) { dmabuf.fd[i] = dup( dmabuf.fd[0] ); if ( dmabuf.fd[i] < 0 ) { vk_log.errorf_errno( "dup failed" ); return false; } } } else { const VkImageSubresource subresource = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, }; VkSubresourceLayout subresourceLayout = {}; g_device.vk.GetImageSubresourceLayout( g_device.device(), m_vkImage, &subresource, &subresourceLayout ); dmabuf.n_planes = 1; dmabuf.modifier = DRM_FORMAT_MOD_INVALID; dmabuf.offset[0] = 0; dmabuf.stride[0] = subresourceLayout.rowPitch; } m_dmabuf = dmabuf; } if ( flags.bFlippable == true ) { m_pBackendFb = GetBackend()->ImportDmabufToBackend( nullptr, &m_dmabuf ); } bool bHasAlpha = pDMA ? DRMFormatHasAlpha( pDMA->format ) : true; if (!bHasAlpha ) { // not compatible with with swizzles assert ( flags.bStorage == false ); } if ( flags.bStorage || flags.bSampled || flags.bColorAttachment ) { VkImageViewCreateInfo createInfo = { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = m_vkImage, .viewType = VulkanImageTypeToViewType(flags.imageType), .format = DRMFormatToVulkan(drmFormat, false), .components = { .r = VK_COMPONENT_SWIZZLE_IDENTITY, .g = VK_COMPONENT_SWIZZLE_IDENTITY, .b = VK_COMPONENT_SWIZZLE_IDENTITY, .a = bHasAlpha ? VK_COMPONENT_SWIZZLE_IDENTITY : VK_COMPONENT_SWIZZLE_ONE, }, .subresourceRange = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .levelCount = 1, .layerCount = 1, }, }; res = g_device.vk.CreateImageView(g_device.device(), &createInfo, nullptr, &m_srgbView); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkCreateImageView failed" ); return false; } if ( flags.bSampled ) { VkImageViewUsageCreateInfo viewUsageInfo = { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO, .usage = usage & ~VK_IMAGE_USAGE_STORAGE_BIT, }; createInfo.pNext = &viewUsageInfo; createInfo.format = DRMFormatToVulkan(drmFormat, true); res = g_device.vk.CreateImageView(g_device.device(), &createInfo, nullptr, &m_linearView); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkCreateImageView failed" ); return false; } } if ( isYcbcr() ) { createInfo.pNext = NULL; createInfo.format = VK_FORMAT_R8_UNORM; createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_PLANE_0_BIT; res = g_device.vk.CreateImageView(g_device.device(), &createInfo, nullptr, &m_lumaView); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkCreateImageView failed" ); return false; } createInfo.pNext = NULL; createInfo.format = VK_FORMAT_R8G8_UNORM; createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_PLANE_1_BIT; res = g_device.vk.CreateImageView(g_device.device(), &createInfo, nullptr, &m_chromaView); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkCreateImageView failed" ); return false; } createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; } } if ( flags.bMappable ) { if (pExistingImageToReuseMemory) { m_pMappedData = pExistingImageToReuseMemory->m_pMappedData; } else { void *pData = nullptr; res = g_device.vk.MapMemory( g_device.device(), memoryHandle, 0, VK_WHOLE_SIZE, 0, &pData ); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkMapMemory failed" ); return false; } m_pMappedData = (uint8_t*)pData; } } m_bInitialized = true; return true; } bool CVulkanTexture::BInitFromSwapchain( VkImage image, uint32_t width, uint32_t height, VkFormat format ) { m_drmFormat = VulkanFormatToDRM( format ); m_vkImage = image; m_vkImageMemory = VK_NULL_HANDLE; m_width = width; m_height = height; m_depth = 1; m_format = format; m_contentWidth = width; m_contentHeight = height; m_bOutputImage = true; VkImageViewCreateInfo createInfo = { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = image, .viewType = VK_IMAGE_VIEW_TYPE_2D, .format = ToLinearVulkanFormat( format ), .components = { .r = VK_COMPONENT_SWIZZLE_IDENTITY, .g = VK_COMPONENT_SWIZZLE_IDENTITY, .b = VK_COMPONENT_SWIZZLE_IDENTITY, .a = VK_COMPONENT_SWIZZLE_IDENTITY, }, .subresourceRange = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .levelCount = 1, .layerCount = 1, }, }; VkResult res = g_device.vk.CreateImageView(g_device.device(), &createInfo, nullptr, &m_srgbView); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkCreateImageView failed" ); return false; } VkImageViewUsageCreateInfo viewUsageInfo = { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO, .usage = VK_IMAGE_USAGE_SAMPLED_BIT, }; createInfo.pNext = &viewUsageInfo; createInfo.format = ToSrgbVulkanFormat( format ); res = g_device.vk.CreateImageView(g_device.device(), &createInfo, nullptr, &m_linearView); if ( res != VK_SUCCESS ) { vk_errorf( res, "vkCreateImageView failed" ); return false; } m_bInitialized = true; return true; } uint32_t CVulkanTexture::IncRef() { uint32_t uRefCount = gamescope::RcObject::IncRef(); if ( m_pBackendFb && !uRefCount ) { m_pBackendFb->IncRef(); } return uRefCount; } uint32_t CVulkanTexture::DecRef() { // Need to pull it out as we could be destroyed in DecRef. gamescope::IBackendFb *pBackendFb = m_pBackendFb.get(); uint32_t uRefCount = gamescope::RcObject::DecRef(); if ( pBackendFb && !uRefCount ) { pBackendFb->DecRef(); } return uRefCount; } bool CVulkanTexture::IsInUse() { if ( m_pBackendFb && m_pBackendFb->GetRefCount() != 0 ) return true; return GetRefCount() != 0; } CVulkanTexture::CVulkanTexture( void ) { } CVulkanTexture::~CVulkanTexture( void ) { wlr_dmabuf_attributes_finish( &m_dmabuf ); if ( m_pMappedData != nullptr && m_vkImageMemory ) { g_device.vk.UnmapMemory( g_device.device(), m_vkImageMemory ); m_pMappedData = nullptr; } if ( m_srgbView != VK_NULL_HANDLE ) { g_device.vk.DestroyImageView( g_device.device(), m_srgbView, nullptr ); m_srgbView = VK_NULL_HANDLE; } if ( m_linearView != VK_NULL_HANDLE ) { g_device.vk.DestroyImageView( g_device.device(), m_linearView, nullptr ); m_linearView = VK_NULL_HANDLE; } if ( m_pBackendFb != nullptr ) m_pBackendFb = nullptr; if ( m_vkImageMemory != VK_NULL_HANDLE ) { if ( m_vkImage != VK_NULL_HANDLE ) { g_device.vk.DestroyImage( g_device.device(), m_vkImage, nullptr ); m_vkImage = VK_NULL_HANDLE; } g_device.vk.FreeMemory( g_device.device(), m_vkImageMemory, nullptr ); m_vkImageMemory = VK_NULL_HANDLE; } m_bInitialized = false; } int CVulkanTexture::memoryFence() { const VkMemoryGetFdInfoKHR memory_get_fd_info = { .sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR, .memory = m_vkImageMemory, .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, }; int fence = -1; VkResult res = g_device.vk.GetMemoryFdKHR(g_device.device(), &memory_get_fd_info, &fence); if ( res != VK_SUCCESS ) { fprintf( stderr, "vkGetMemoryFdKHR failed\n" ); } return fence; } static bool is_image_format_modifier_supported(VkFormat format, uint32_t drmFormat, uint64_t modifier) { VkPhysicalDeviceImageFormatInfo2 imageFormatInfo = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2, .format = format, .type = VK_IMAGE_TYPE_2D, .tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT, .usage = VK_IMAGE_USAGE_SAMPLED_BIT, }; std::array formats = { DRMFormatToVulkan(drmFormat, false), DRMFormatToVulkan(drmFormat, true), }; VkImageFormatListCreateInfo formatList = { .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO, .viewFormatCount = (uint32_t)formats.size(), .pViewFormats = formats.data(), }; if ( formats[0] != formats[1] ) { formatList.pNext = std::exchange(imageFormatInfo.pNext, &formatList); imageFormatInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; } VkPhysicalDeviceImageDrmFormatModifierInfoEXT modifierInfo = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_DRM_FORMAT_MODIFIER_INFO_EXT, .pNext = nullptr, .drmFormatModifier = modifier, }; modifierInfo.pNext = std::exchange(imageFormatInfo.pNext, &modifierInfo); VkImageFormatProperties2 imageFormatProps = { .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2, }; VkResult res = g_device.vk.GetPhysicalDeviceImageFormatProperties2( g_device.physDev(), &imageFormatInfo, &imageFormatProps ); return res == VK_SUCCESS; } bool vulkan_init_format(VkFormat format, uint32_t drmFormat) { // First, check whether the Vulkan format is supported VkPhysicalDeviceImageFormatInfo2 imageFormatInfo = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2, .format = format, .type = VK_IMAGE_TYPE_2D, .tiling = VK_IMAGE_TILING_OPTIMAL, .usage = VK_IMAGE_USAGE_SAMPLED_BIT, }; std::array formats = { DRMFormatToVulkan(drmFormat, false), DRMFormatToVulkan(drmFormat, true), }; VkImageFormatListCreateInfo formatList = { .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO, .viewFormatCount = (uint32_t)formats.size(), .pViewFormats = formats.data(), }; if ( formats[0] != formats[1] ) { formatList.pNext = std::exchange(imageFormatInfo.pNext, &formatList); imageFormatInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; } VkImageFormatProperties2 imageFormatProps = { .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2, }; VkResult res = g_device.vk.GetPhysicalDeviceImageFormatProperties2( g_device.physDev(), &imageFormatInfo, &imageFormatProps ); if ( res == VK_ERROR_FORMAT_NOT_SUPPORTED ) { return false; } else if ( res != VK_SUCCESS ) { vk_errorf( res, "vkGetPhysicalDeviceImageFormatProperties2 failed for DRM format 0x%" PRIX32, drmFormat ); return false; } wlr_drm_format_set_add( &sampledShmFormats, drmFormat, DRM_FORMAT_MOD_LINEAR ); if ( g_device.supportsModifiers() ) { // Then, collect the list of modifiers supported for sampled usage VkDrmFormatModifierPropertiesListEXT modifierPropList = { .sType = VK_STRUCTURE_TYPE_DRM_FORMAT_MODIFIER_PROPERTIES_LIST_EXT, }; VkFormatProperties2 formatProps = { .sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2, .pNext = &modifierPropList, }; g_device.vk.GetPhysicalDeviceFormatProperties2( g_device.physDev(), format, &formatProps ); if ( modifierPropList.drmFormatModifierCount == 0 ) { vk_errorf( res, "vkGetPhysicalDeviceFormatProperties2 returned zero modifiers for DRM format 0x%" PRIX32, drmFormat ); return false; } std::vector modifierProps(modifierPropList.drmFormatModifierCount); modifierPropList.pDrmFormatModifierProperties = modifierProps.data(); g_device.vk.GetPhysicalDeviceFormatProperties2( g_device.physDev(), format, &formatProps ); std::map< uint64_t, VkDrmFormatModifierPropertiesEXT > map = {}; for ( size_t j = 0; j < modifierProps.size(); j++ ) { map[ modifierProps[j].drmFormatModifier ] = modifierProps[j]; uint64_t modifier = modifierProps[j].drmFormatModifier; if ( !is_image_format_modifier_supported( format, drmFormat, modifier ) ) continue; if ( ( modifierProps[j].drmFormatModifierTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT ) == 0 ) { continue; } if ( GetBackend()->UsesModifiers() && !gamescope::Algorithm::Contains( GetBackend()->GetSupportedModifiers( drmFormat ), modifier ) ) continue; wlr_drm_format_set_add( &sampledDRMFormats, drmFormat, modifier ); } DRMModifierProps[ format ] = map; return true; } else { if ( GetBackend()->UsesModifiers() && !GetBackend()->SupportsFormat( drmFormat ) ) return false; wlr_drm_format_set_add( &sampledDRMFormats, drmFormat, DRM_FORMAT_MOD_INVALID ); return false; } } bool vulkan_init_formats() { for ( size_t i = 0; s_DRMVKFormatTable[i].DRMFormat != DRM_FORMAT_INVALID; i++ ) { if (s_DRMVKFormatTable[i].internal) continue; VkFormat format = s_DRMVKFormatTable[i].vkFormat; VkFormat srgbFormat = s_DRMVKFormatTable[i].vkFormatSrgb; uint32_t drmFormat = s_DRMVKFormatTable[i].DRMFormat; vulkan_init_format(format, drmFormat); if (format != srgbFormat) vulkan_init_format(srgbFormat, drmFormat); } vk_log.infof( "supported DRM formats for sampling usage:" ); for ( size_t i = 0; i < sampledDRMFormats.len; i++ ) { uint32_t fmt = sampledDRMFormats.formats[ i ].format; #if HAVE_DRM char *name = drmGetFormatName(fmt); vk_log.infof( " %s (0x%" PRIX32 ")", name, fmt ); free(name); #endif } return true; } bool acquire_next_image( void ) { VkResult res = g_device.vk.AcquireNextImageKHR( g_device.device(), g_output.swapChain, UINT64_MAX, VK_NULL_HANDLE, g_output.acquireFence, &g_output.nOutImage ); if ( res != VK_SUCCESS && res != VK_SUBOPTIMAL_KHR ) return false; if ( g_device.vk.WaitForFences( g_device.device(), 1, &g_output.acquireFence, false, UINT64_MAX ) != VK_SUCCESS ) return false; return g_device.vk.ResetFences( g_device.device(), 1, &g_output.acquireFence ) == VK_SUCCESS; } static std::atomic g_currentPresentWaitId = {0u}; static std::mutex present_wait_lock; extern void mangoapp_output_update( uint64_t vblanktime ); static void present_wait_thread_func( void ) { uint64_t present_wait_id = 0; while (true) { g_currentPresentWaitId.wait(present_wait_id); // Lock to make sure swapchain destruction is waited on and that // it's for this swapchain. { std::unique_lock lock(present_wait_lock); present_wait_id = g_currentPresentWaitId.load(); if (present_wait_id != 0) { g_device.vk.WaitForPresentKHR( g_device.device(), g_output.swapChain, present_wait_id, 1'000'000'000lu ); uint64_t vblanktime = get_time_in_nanos(); GetVBlankTimer().MarkVBlank( vblanktime, true ); mangoapp_output_update( vblanktime ); } } } } void vulkan_update_swapchain_hdr_metadata( VulkanOutput_t *pOutput ) { if (!g_output.swapchainHDRMetadata) return; if ( !g_device.vk.SetHdrMetadataEXT ) { static bool s_bWarned = false; if (!s_bWarned) { vk_log.errorf("Unable to forward HDR metadata with Vulkan as vkSetMetadataEXT is not supported."); s_bWarned = true; } return; } const hdr_metadata_infoframe &infoframe = g_output.swapchainHDRMetadata->View().hdmi_metadata_type1; VkHdrMetadataEXT metadata = { .sType = VK_STRUCTURE_TYPE_HDR_METADATA_EXT, .displayPrimaryRed = VkXYColorEXT { color_xy_from_u16(infoframe.display_primaries[0].x), color_xy_from_u16(infoframe.display_primaries[0].y) }, .displayPrimaryGreen = VkXYColorEXT { color_xy_from_u16(infoframe.display_primaries[1].x), color_xy_from_u16(infoframe.display_primaries[1].y), }, .displayPrimaryBlue = VkXYColorEXT { color_xy_from_u16(infoframe.display_primaries[2].x), color_xy_from_u16(infoframe.display_primaries[2].y), }, .whitePoint = VkXYColorEXT { color_xy_from_u16(infoframe.white_point.x), color_xy_from_u16(infoframe.white_point.y), }, .maxLuminance = nits_from_u16(infoframe.max_display_mastering_luminance), .minLuminance = nits_from_u16_dark(infoframe.min_display_mastering_luminance), .maxContentLightLevel = nits_from_u16(infoframe.max_cll), .maxFrameAverageLightLevel = nits_from_u16(infoframe.max_fall), }; g_device.vk.SetHdrMetadataEXT(g_device.device(), 1, &g_output.swapChain, &metadata); } void vulkan_present_to_window( void ) { static uint64_t s_lastPresentId = 0; uint64_t presentId = ++s_lastPresentId; auto feedback = steamcompmgr_get_base_layer_swapchain_feedback(); if (feedback && feedback->hdr_metadata_blob) { if ( feedback->hdr_metadata_blob != g_output.swapchainHDRMetadata ) { g_output.swapchainHDRMetadata = feedback->hdr_metadata_blob; vulkan_update_swapchain_hdr_metadata( &g_output ); } } else if ( g_output.swapchainHDRMetadata != nullptr ) { // Only way to clear hdr metadata for a swapchain in Vulkan // is to recreate the swapchain. g_output.swapchainHDRMetadata = nullptr; vulkan_remake_swapchain(); } VkPresentIdKHR presentIdInfo = { .sType = VK_STRUCTURE_TYPE_PRESENT_ID_KHR, .swapchainCount = 1, .pPresentIds = &presentId, }; VkPresentInfoKHR presentInfo = { .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, .pNext = &presentIdInfo, .swapchainCount = 1, .pSwapchains = &g_output.swapChain, .pImageIndices = &g_output.nOutImage, }; if ( g_device.vk.QueuePresentKHR( g_device.queue(), &presentInfo ) == VK_SUCCESS ) { g_currentPresentWaitId = presentId; g_currentPresentWaitId.notify_all(); } else vulkan_remake_swapchain(); while ( !acquire_next_image() ) vulkan_remake_swapchain(); } gamescope::Rc vulkan_create_1d_lut(uint32_t size) { CVulkanTexture::createFlags flags; flags.bSampled = true; flags.bTransferDst = true; flags.imageType = VK_IMAGE_TYPE_1D; auto texture = new CVulkanTexture(); auto drmFormat = VulkanFormatToDRM( VK_FORMAT_R16G16B16A16_UNORM ); bool bRes = texture->BInit( size, 1u, 1u, drmFormat, flags ); assert( bRes ); return texture; } gamescope::Rc vulkan_create_3d_lut(uint32_t width, uint32_t height, uint32_t depth) { CVulkanTexture::createFlags flags; flags.bSampled = true; flags.bTransferDst = true; flags.imageType = VK_IMAGE_TYPE_3D; auto texture = new CVulkanTexture(); auto drmFormat = VulkanFormatToDRM( VK_FORMAT_R16G16B16A16_UNORM ); bool bRes = texture->BInit( width, height, depth, drmFormat, flags ); assert( bRes ); return texture; } void vulkan_update_luts(const gamescope::Rc& lut1d, const gamescope::Rc& lut3d, void* lut1d_data, void* lut3d_data) { size_t lut1d_size = lut1d->width() * sizeof(uint16_t) * 4; size_t lut3d_size = lut3d->width() * lut3d->height() * lut3d->depth() * sizeof(uint16_t) * 4; auto [base_dst, base_offset] = g_device.uploadBufferData(lut1d_size + lut3d_size); void* lut1d_dst = base_dst; void *lut3d_dst = ((uint8_t*)base_dst) + lut1d_size; memcpy(lut1d_dst, lut1d_data, lut1d_size); memcpy(lut3d_dst, lut3d_data, lut3d_size); auto cmdBuffer = g_device.commandBuffer(); cmdBuffer->copyBufferToImage(g_device.uploadBuffer(), base_offset, 0, lut1d); cmdBuffer->copyBufferToImage(g_device.uploadBuffer(), base_offset + lut1d_size, 0, lut3d); g_device.submit(std::move(cmdBuffer)); g_device.waitIdle(); // TODO: Sync this better } gamescope::Rc vulkan_get_hacky_blank_texture() { return g_output.temporaryHackyBlankImage.get(); } gamescope::OwningRc vulkan_create_flat_texture( uint32_t width, uint32_t height, uint8_t r, uint8_t g, uint8_t b, uint8_t a ) { CVulkanTexture::createFlags flags; flags.bFlippable = true; flags.bSampled = true; flags.bTransferDst = true; gamescope::OwningRc texture = new CVulkanTexture(); bool bRes = texture->BInit( width, height, 1u, VulkanFormatToDRM( VK_FORMAT_B8G8R8A8_UNORM ), flags ); assert( bRes ); auto [_dst, offset] = g_device.uploadBufferData( width * height * 4 ); uint8_t *dst = (uint8_t *)_dst; for ( uint32_t i = 0; i < width * height * 4; i += 4 ) { dst[i + 0] = b; dst[i + 1] = g; dst[i + 2] = r; dst[i + 3] = a; } auto cmdBuffer = g_device.commandBuffer(); cmdBuffer->copyBufferToImage(g_device.uploadBuffer(), offset, 0, texture.get()); g_device.submit(std::move(cmdBuffer)); g_device.waitIdle(); return texture; } gamescope::OwningRc vulkan_create_debug_blank_texture() { // To match Steam's scaling, which is capped at 1080p int width = std::min( g_nOutputWidth, 1920 ); int height = std::min( g_nOutputHeight, 1080 ); return vulkan_create_flat_texture( width, height, 0, 0, 0, 0 ); } bool vulkan_supports_hdr10() { for ( auto& format : g_output.surfaceFormats ) { if ( format.colorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT ) return true; } return false; } extern bool g_bOutputHDREnabled; bool vulkan_make_swapchain( VulkanOutput_t *pOutput ) { uint32_t imageCount = pOutput->surfaceCaps.minImageCount + 1; uint32_t formatCount = pOutput->surfaceFormats.size(); uint32_t surfaceFormat = formatCount; VkColorSpaceKHR preferredColorSpace = g_bOutputHDREnabled ? VK_COLOR_SPACE_HDR10_ST2084_EXT : VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; if ( surfaceFormat == formatCount ) { for ( surfaceFormat = 0; surfaceFormat < formatCount; surfaceFormat++ ) { if ( pOutput->surfaceFormats[ surfaceFormat ].format == VK_FORMAT_A2B10G10R10_UNORM_PACK32 && pOutput->surfaceFormats[ surfaceFormat ].colorSpace == preferredColorSpace ) break; } } if ( surfaceFormat == formatCount ) { for ( surfaceFormat = 0; surfaceFormat < formatCount; surfaceFormat++ ) { if ( pOutput->surfaceFormats[ surfaceFormat ].format == VK_FORMAT_A2R10G10B10_UNORM_PACK32 && pOutput->surfaceFormats[ surfaceFormat ].colorSpace == preferredColorSpace ) break; } } if ( surfaceFormat == formatCount ) { for ( surfaceFormat = 0; surfaceFormat < formatCount; surfaceFormat++ ) { if ( pOutput->surfaceFormats[ surfaceFormat ].format == VK_FORMAT_B8G8R8A8_UNORM && pOutput->surfaceFormats[ surfaceFormat ].colorSpace == preferredColorSpace ) break; } } if ( surfaceFormat == formatCount ) return false; VkFormat eVkFormat = pOutput->surfaceFormats[ surfaceFormat ].format; pOutput->uOutputFormat = VulkanFormatToDRM( pOutput->surfaceFormats[ surfaceFormat ].format ); VkFormat formats[2] = { ToSrgbVulkanFormat( eVkFormat ), ToLinearVulkanFormat( eVkFormat ), }; VkImageFormatListCreateInfo usageListInfo = { .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO, .viewFormatCount = 2, .pViewFormats = formats, }; vk_log.infof("Creating Gamescope nested swapchain with format %u and colorspace %u", eVkFormat, pOutput->surfaceFormats[surfaceFormat].colorSpace); VkSwapchainCreateInfoKHR createInfo = { .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, .pNext = formats[0] != formats[1] ? &usageListInfo : nullptr, .flags = formats[0] != formats[1] ? VK_SWAPCHAIN_CREATE_MUTABLE_FORMAT_BIT_KHR : (VkSwapchainCreateFlagBitsKHR )0, .surface = pOutput->surface, .minImageCount = imageCount, .imageFormat = eVkFormat, .imageColorSpace = pOutput->surfaceFormats[surfaceFormat].colorSpace, .imageExtent = { .width = g_nOutputWidth, .height = g_nOutputHeight, }, .imageArrayLayers = 1, .imageUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE, .preTransform = pOutput->surfaceCaps.currentTransform, .compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, .presentMode = VK_PRESENT_MODE_FIFO_KHR, .clipped = VK_TRUE, }; if (g_device.vk.CreateSwapchainKHR( g_device.device(), &createInfo, nullptr, &pOutput->swapChain) != VK_SUCCESS ) { return false; } g_device.vk.GetSwapchainImagesKHR( g_device.device(), pOutput->swapChain, &imageCount, nullptr ); std::vector swapchainImages( imageCount ); g_device.vk.GetSwapchainImagesKHR( g_device.device(), pOutput->swapChain, &imageCount, swapchainImages.data() ); pOutput->outputImages.resize(imageCount); for ( uint32_t i = 0; i < pOutput->outputImages.size(); i++ ) { pOutput->outputImages[i] = new CVulkanTexture(); if ( !pOutput->outputImages[i]->BInitFromSwapchain(swapchainImages[i], g_nOutputWidth, g_nOutputHeight, eVkFormat)) return false; } VkFenceCreateInfo fenceInfo = { .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, }; g_device.vk.CreateFence( g_device.device(), &fenceInfo, nullptr, &pOutput->acquireFence ); vulkan_update_swapchain_hdr_metadata(pOutput); return true; } bool vulkan_remake_swapchain( void ) { std::unique_lock lock(present_wait_lock); g_currentPresentWaitId = 0; g_currentPresentWaitId.notify_all(); VulkanOutput_t *pOutput = &g_output; g_device.waitIdle(); g_device.vk.QueueWaitIdle( g_device.queue() ); pOutput->outputImages.clear(); g_device.vk.DestroySwapchainKHR( g_device.device(), pOutput->swapChain, nullptr ); // Delete screenshot image to be remade if needed for (auto& pScreenshotImage : pOutput->pScreenshotImages) pScreenshotImage = nullptr; bool bRet = vulkan_make_swapchain( pOutput ); assert( bRet ); // Something has gone horribly wrong! return bRet; } static bool vulkan_make_output_images( VulkanOutput_t *pOutput ) { CVulkanTexture::createFlags outputImageflags; outputImageflags.bFlippable = true; outputImageflags.bStorage = true; outputImageflags.bTransferSrc = true; // for screenshots outputImageflags.bSampled = true; // for pipewire blits outputImageflags.bOutputImage = true; pOutput->outputImages.resize(3); // extra image for partial composition. pOutput->outputImagesPartialOverlay.resize(3); pOutput->outputImages[0] = nullptr; pOutput->outputImages[1] = nullptr; pOutput->outputImages[2] = nullptr; pOutput->outputImagesPartialOverlay[0] = nullptr; pOutput->outputImagesPartialOverlay[1] = nullptr; pOutput->outputImagesPartialOverlay[2] = nullptr; uint32_t uDRMFormat = pOutput->uOutputFormat; pOutput->outputImages[0] = new CVulkanTexture(); bool bSuccess = pOutput->outputImages[0]->BInit( g_nOutputWidth, g_nOutputHeight, 1u, uDRMFormat, outputImageflags ); if ( bSuccess != true ) { vk_log.errorf( "failed to allocate buffer for KMS" ); return false; } pOutput->outputImages[1] = new CVulkanTexture(); bSuccess = pOutput->outputImages[1]->BInit( g_nOutputWidth, g_nOutputHeight, 1u, uDRMFormat, outputImageflags ); if ( bSuccess != true ) { vk_log.errorf( "failed to allocate buffer for KMS" ); return false; } pOutput->outputImages[2] = new CVulkanTexture(); bSuccess = pOutput->outputImages[2]->BInit( g_nOutputWidth, g_nOutputHeight, 1u, uDRMFormat, outputImageflags ); if ( bSuccess != true ) { vk_log.errorf( "failed to allocate buffer for KMS" ); return false; } // Oh no. pOutput->temporaryHackyBlankImage = vulkan_create_debug_blank_texture(); if ( pOutput->uOutputFormatOverlay != VK_FORMAT_UNDEFINED && !kDisablePartialComposition ) { uint32_t uPartialDRMFormat = pOutput->uOutputFormatOverlay; pOutput->outputImagesPartialOverlay[0] = new CVulkanTexture(); bool bSuccess = pOutput->outputImagesPartialOverlay[0]->BInit( g_nOutputWidth, g_nOutputHeight, 1u, uPartialDRMFormat, outputImageflags, nullptr, 0, 0, pOutput->outputImages[0].get() ); if ( bSuccess != true ) { vk_log.errorf( "failed to allocate buffer for KMS" ); return false; } pOutput->outputImagesPartialOverlay[1] = new CVulkanTexture(); bSuccess = pOutput->outputImagesPartialOverlay[1]->BInit( g_nOutputWidth, g_nOutputHeight, 1u, uPartialDRMFormat, outputImageflags, nullptr, 0, 0, pOutput->outputImages[1].get() ); if ( bSuccess != true ) { vk_log.errorf( "failed to allocate buffer for KMS" ); return false; } pOutput->outputImagesPartialOverlay[2] = new CVulkanTexture(); bSuccess = pOutput->outputImagesPartialOverlay[2]->BInit( g_nOutputWidth, g_nOutputHeight, 1u, uPartialDRMFormat, outputImageflags, nullptr, 0, 0, pOutput->outputImages[2].get() ); if ( bSuccess != true ) { vk_log.errorf( "failed to allocate buffer for KMS" ); return false; } } return true; } bool vulkan_remake_output_images() { VulkanOutput_t *pOutput = &g_output; g_device.waitIdle(); pOutput->nOutImage = 0; // Delete screenshot image to be remade if needed for (auto& pScreenshotImage : pOutput->pScreenshotImages) pScreenshotImage = nullptr; bool bRet = vulkan_make_output_images( pOutput ); assert( bRet ); return bRet; } bool vulkan_make_output() { VulkanOutput_t *pOutput = &g_output; VkResult result; if ( GetBackend()->UsesVulkanSwapchain() ) { result = g_device.vk.GetPhysicalDeviceSurfaceCapabilitiesKHR( g_device.physDev(), pOutput->surface, &pOutput->surfaceCaps ); if ( result != VK_SUCCESS ) { vk_errorf( result, "vkGetPhysicalDeviceSurfaceCapabilitiesKHR failed" ); return false; } uint32_t formatCount = 0; result = g_device.vk.GetPhysicalDeviceSurfaceFormatsKHR( g_device.physDev(), pOutput->surface, &formatCount, nullptr ); if ( result != VK_SUCCESS ) { vk_errorf( result, "vkGetPhysicalDeviceSurfaceFormatsKHR failed" ); return false; } if ( formatCount != 0 ) { pOutput->surfaceFormats.resize( formatCount ); g_device.vk.GetPhysicalDeviceSurfaceFormatsKHR( g_device.physDev(), pOutput->surface, &formatCount, pOutput->surfaceFormats.data() ); if ( result != VK_SUCCESS ) { vk_errorf( result, "vkGetPhysicalDeviceSurfaceFormatsKHR failed" ); return false; } } uint32_t presentModeCount = false; result = g_device.vk.GetPhysicalDeviceSurfacePresentModesKHR(g_device.physDev(), pOutput->surface, &presentModeCount, nullptr ); if ( result != VK_SUCCESS ) { vk_errorf( result, "vkGetPhysicalDeviceSurfacePresentModesKHR failed" ); return false; } if ( presentModeCount != 0 ) { pOutput->presentModes.resize(presentModeCount); result = g_device.vk.GetPhysicalDeviceSurfacePresentModesKHR( g_device.physDev(), pOutput->surface, &presentModeCount, pOutput->presentModes.data() ); if ( result != VK_SUCCESS ) { vk_errorf( result, "vkGetPhysicalDeviceSurfacePresentModesKHR failed" ); return false; } } if ( !vulkan_make_swapchain( pOutput ) ) return false; while ( !acquire_next_image() ) vulkan_remake_swapchain(); } else { GetBackend()->GetPreferredOutputFormat( &pOutput->uOutputFormat, &pOutput->uOutputFormatOverlay ); if ( pOutput->uOutputFormat == DRM_FORMAT_INVALID ) { vk_log.errorf( "failed to find Vulkan format suitable for KMS" ); return false; } if ( pOutput->uOutputFormatOverlay == DRM_FORMAT_INVALID ) { vk_log.errorf( "failed to find Vulkan format suitable for KMS partial overlays" ); return false; } if ( !vulkan_make_output_images( pOutput ) ) return false; } return true; } static void update_tmp_images( uint32_t width, uint32_t height ) { if ( g_output.tmpOutput != nullptr && width == g_output.tmpOutput->width() && height == g_output.tmpOutput->height() ) { return; } CVulkanTexture::createFlags createFlags; createFlags.bSampled = true; createFlags.bStorage = true; g_output.tmpOutput = new CVulkanTexture(); bool bSuccess = g_output.tmpOutput->BInit( width, height, 1u, DRM_FORMAT_ARGB8888, createFlags, nullptr ); if ( !bSuccess ) { vk_log.errorf( "failed to create fsr output" ); return; } } static bool init_nis_data() { // Create the NIS images // Select between the FP16 or FP32 coefficients void* coefScaleData = g_device.supportsFp16() ? (void*) coef_scale_fp16 : (void*) coef_scale; void* coefUsmData = g_device.supportsFp16() ? (void*) coef_usm_fp16 : (void*) coef_usm; uint32_t nisFormat = g_device.supportsFp16() ? DRM_FORMAT_ABGR16161616F : DRM_FORMAT_ABGR32323232F; uint32_t width = kFilterSize / 4; uint32_t height = kPhaseCount; g_output.nisScalerImage = vulkan_create_texture_from_bits( width, height, width, height, nisFormat, {}, coefScaleData ); g_output.nisUsmImage = vulkan_create_texture_from_bits( width, height, width, height, nisFormat, {}, coefUsmData ); return true; } VkInstance vulkan_get_instance( void ) { static VkInstance s_pVkInstance = []() -> VkInstance { VkResult result = VK_ERROR_INITIALIZATION_FAILED; if ( ( result = vulkan_load_module() ) != VK_SUCCESS ) { vk_errorf( result, "Failed to load vulkan module." ); return nullptr; } auto instanceExtensions = GetBackend()->GetInstanceExtensions(); const VkApplicationInfo appInfo = { .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, .pApplicationName = "gamescope", .applicationVersion = VK_MAKE_VERSION(1, 0, 0), .pEngineName = "hopefully not just some code", .engineVersion = VK_MAKE_VERSION(1, 0, 0), .apiVersion = VK_API_VERSION_1_3, }; const VkInstanceCreateInfo createInfo = { .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, .pApplicationInfo = &appInfo, .enabledExtensionCount = (uint32_t)instanceExtensions.size(), .ppEnabledExtensionNames = instanceExtensions.data(), }; VkInstance instance = nullptr; result = g_pfn_vkCreateInstance(&createInfo, 0, &instance); if ( result != VK_SUCCESS ) { vk_errorf( result, "vkCreateInstance failed" ); } return instance; }(); return s_pVkInstance; } bool vulkan_init( VkInstance instance, VkSurfaceKHR surface ) { if (!g_device.BInit(instance, surface)) return false; if (!init_nis_data()) return false; if ( GetBackend()->UsesVulkanSwapchain() ) { std::thread present_wait_thread( present_wait_thread_func ); present_wait_thread.detach(); } return true; } gamescope::OwningRc vulkan_create_texture_from_dmabuf( struct wlr_dmabuf_attributes *pDMA, gamescope::OwningRc pBackendFb ) { gamescope::OwningRc pTex = new CVulkanTexture(); CVulkanTexture::createFlags texCreateFlags; texCreateFlags.bSampled = true; //fprintf(stderr, "pDMA->width: %d pDMA->height: %d pDMA->format: 0x%x pDMA->modifier: 0x%lx pDMA->n_planes: %d\n", // pDMA->width, pDMA->height, pDMA->format, pDMA->modifier, pDMA->n_planes); if ( pTex->BInit( pDMA->width, pDMA->height, 1u, pDMA->format, texCreateFlags, pDMA, 0, 0, nullptr, pBackendFb ) == false ) return nullptr; return pTex; } gamescope::OwningRc vulkan_create_texture_from_bits( uint32_t width, uint32_t height, uint32_t contentWidth, uint32_t contentHeight, uint32_t drmFormat, CVulkanTexture::createFlags texCreateFlags, void *bits ) { gamescope::OwningRc pTex = new CVulkanTexture(); texCreateFlags.bSampled = true; texCreateFlags.bTransferDst = true; if ( pTex->BInit( width, height, 1u, drmFormat, texCreateFlags, nullptr, contentWidth, contentHeight) == false ) return nullptr; size_t size = width * height * DRMFormatGetBPP(drmFormat); auto [ dst, offset ] = g_device.uploadBufferData(size); memcpy( dst, bits, size ); auto cmdBuffer = g_device.commandBuffer(); cmdBuffer->copyBufferToImage(g_device.uploadBuffer(), offset, 0, pTex.get()); // TODO: Sync this copyBufferToImage. g_device.submit(std::move(cmdBuffer)); g_device.waitIdle(); return pTex; } static uint32_t s_frameId = 0; void vulkan_garbage_collect( void ) { g_device.garbageCollect(); } gamescope::Rc vulkan_acquire_screenshot_texture(uint32_t width, uint32_t height, bool exportable, uint32_t drmFormat, EStreamColorspace colorspace) { for (auto& pScreenshotImage : g_output.pScreenshotImages) { if (pScreenshotImage == nullptr) { pScreenshotImage = new CVulkanTexture(); CVulkanTexture::createFlags screenshotImageFlags; screenshotImageFlags.bMappable = true; screenshotImageFlags.bTransferDst = true; screenshotImageFlags.bStorage = true; if (exportable || drmFormat == DRM_FORMAT_NV12) { screenshotImageFlags.bExportable = true; screenshotImageFlags.bLinear = true; // TODO: support multi-planar DMA-BUF export via PipeWire } bool bSuccess = pScreenshotImage->BInit( width, height, 1u, drmFormat, screenshotImageFlags ); pScreenshotImage->setStreamColorspace(colorspace); assert( bSuccess ); } if (pScreenshotImage->GetRefCount() != 0 || width != pScreenshotImage->width() || height != pScreenshotImage->height() || drmFormat != pScreenshotImage->drmFormat()) continue; return pScreenshotImage.get(); } vk_log.errorf("Unable to acquire screenshot texture. Out of textures."); return nullptr; } // Internal display's native brightness. float g_flInternalDisplayBrightnessNits = 500.0f; float g_flHDRItmSdrNits = 100.f; float g_flHDRItmTargetNits = 1000.f; #pragma pack(push, 1) struct BlitPushData_t { vec2_t scale[k_nMaxLayers]; vec2_t offset[k_nMaxLayers]; float opacity[k_nMaxLayers]; glm::mat3x4 ctm[k_nMaxLayers]; uint32_t borderMask; uint32_t frameId; uint32_t blurRadius; uint32_t u_shaderFilter; uint32_t u_alphaMode; float u_linearToNits; // unset float u_nitsToLinear; // unset float u_itmSdrNits; // unset float u_itmTargetNits; // unset explicit BlitPushData_t(const struct FrameInfo_t *frameInfo) { u_shaderFilter = 0; u_alphaMode = 0; for (int i = 0; i < frameInfo->layerCount; i++) { const FrameInfo_t::Layer_t *layer = &frameInfo->layers[i]; scale[i] = layer->scale; offset[i] = layer->offsetPixelCenter(); opacity[i] = layer->opacity; if (layer->isScreenSize() || (layer->filter == GamescopeUpscaleFilter::LINEAR && layer->viewConvertsToLinearAutomatically())) u_shaderFilter |= ((uint32_t)GamescopeUpscaleFilter::FROM_VIEW) << (i * 4); else u_shaderFilter |= ((uint32_t)layer->filter) << (i * 4); u_alphaMode |= ((uint32_t)layer->eAlphaBlendingMode) << ( i * 4 ); if (layer->ctm) { ctm[i] = layer->ctm->View(); } else { ctm[i] = glm::mat3x4 { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0 }; } } borderMask = frameInfo->borderMask(); frameId = s_frameId++; blurRadius = frameInfo->blurRadius ? ( frameInfo->blurRadius * 2 ) - 1 : 0; u_linearToNits = g_flInternalDisplayBrightnessNits; u_nitsToLinear = 1.0f / g_flInternalDisplayBrightnessNits; u_itmSdrNits = g_flHDRItmSdrNits; u_itmTargetNits = g_flHDRItmTargetNits; } explicit BlitPushData_t(float blit_scale) { scale[0] = { blit_scale, blit_scale }; offset[0] = { 0.5f, 0.5f }; opacity[0] = 1.0f; u_shaderFilter = (uint32_t)GamescopeUpscaleFilter::LINEAR; u_alphaMode = 0; ctm[0] = glm::mat3x4 { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0 }; borderMask = 0; frameId = s_frameId; u_linearToNits = g_flInternalDisplayBrightnessNits; u_nitsToLinear = 1.0f / g_flInternalDisplayBrightnessNits; u_itmSdrNits = g_flHDRItmSdrNits; u_itmTargetNits = g_flHDRItmTargetNits; } }; struct CaptureConvertBlitData_t { vec2_t scale[1]; vec2_t offset[1]; float opacity[1]; glm::mat3x4 ctm[1]; mat3x4 outputCTM; uint32_t borderMask; uint32_t halfExtent[2]; explicit CaptureConvertBlitData_t(float blit_scale, const mat3x4 &color_matrix) { scale[0] = { blit_scale, blit_scale }; offset[0] = { 0.0f, 0.0f }; opacity[0] = 1.0f; borderMask = 0; ctm[0] = glm::mat3x4 { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0 }; outputCTM = color_matrix; } }; struct uvec4_t { uint32_t x; uint32_t y; uint32_t z; uint32_t w; }; struct uvec2_t { uint32_t x; uint32_t y; }; struct EasuPushData_t { uvec4_t Const0; uvec4_t Const1; uvec4_t Const2; uvec4_t Const3; EasuPushData_t(uint32_t inputX, uint32_t inputY, uint32_t tempX, uint32_t tempY) { FsrEasuCon(&Const0.x, &Const1.x, &Const2.x, &Const3.x, inputX, inputY, inputX, inputY, tempX, tempY); } }; struct RcasPushData_t { uvec2_t u_layer0Offset; vec2_t u_scale[k_nMaxLayers - 1]; vec2_t u_offset[k_nMaxLayers - 1]; float u_opacity[k_nMaxLayers]; glm::mat3x4 ctm[k_nMaxLayers]; uint32_t u_borderMask; uint32_t u_frameId; uint32_t u_c1; uint32_t u_shaderFilter; uint32_t u_alphaMode; float u_linearToNits; // unset float u_nitsToLinear; // unset float u_itmSdrNits; // unset float u_itmTargetNits; // unset RcasPushData_t(const struct FrameInfo_t *frameInfo, float sharpness) { uvec4_t tmp; FsrRcasCon(&tmp.x, sharpness); u_layer0Offset.x = uint32_t(int32_t(frameInfo->layers[0].offset.x)); u_layer0Offset.y = uint32_t(int32_t(frameInfo->layers[0].offset.y)); u_borderMask = frameInfo->borderMask() >> 1u; u_frameId = s_frameId++; u_c1 = tmp.x; u_shaderFilter = 0; u_alphaMode = 0; for (int i = 0; i < frameInfo->layerCount; i++) { const FrameInfo_t::Layer_t *layer = &frameInfo->layers[i]; if (i == 0 || layer->isScreenSize() || (layer->filter == GamescopeUpscaleFilter::LINEAR && layer->viewConvertsToLinearAutomatically())) u_shaderFilter |= ((uint32_t)GamescopeUpscaleFilter::FROM_VIEW) << (i * 4); else u_shaderFilter |= ((uint32_t)layer->filter) << (i * 4); u_alphaMode |= ((uint32_t)layer->eAlphaBlendingMode) << ( i * 4 ); if (layer->ctm) { ctm[i] = layer->ctm->View(); } else { ctm[i] = glm::mat3x4 { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0 }; } u_opacity[i] = frameInfo->layers[i].opacity; } u_linearToNits = g_flInternalDisplayBrightnessNits; u_nitsToLinear = 1.0f / g_flInternalDisplayBrightnessNits; u_itmSdrNits = g_flHDRItmSdrNits; u_itmTargetNits = g_flHDRItmTargetNits; for (uint32_t i = 1; i < k_nMaxLayers; i++) { u_scale[i - 1] = frameInfo->layers[i].scale; u_offset[i - 1] = frameInfo->layers[i].offsetPixelCenter(); } } }; struct NisPushData_t { NISConfig nisConfig; NisPushData_t(uint32_t inputX, uint32_t inputY, uint32_t tempX, uint32_t tempY, float sharpness) { NVScalerUpdateConfig( nisConfig, sharpness, 0, 0, inputX, inputY, inputX, inputY, 0, 0, tempX, tempY, tempX, tempY); } }; #pragma pack(pop) void bind_all_layers(CVulkanCmdBuffer* cmdBuffer, const struct FrameInfo_t *frameInfo) { for ( int i = 0; i < frameInfo->layerCount; i++ ) { const FrameInfo_t::Layer_t *layer = &frameInfo->layers[i]; bool nearest = layer->isScreenSize() || layer->filter == GamescopeUpscaleFilter::NEAREST || (layer->filter == GamescopeUpscaleFilter::LINEAR && !layer->viewConvertsToLinearAutomatically()); cmdBuffer->bindTexture(i, layer->tex); cmdBuffer->setTextureSrgb(i, layer->colorspace != GAMESCOPE_APP_TEXTURE_COLORSPACE_LINEAR); cmdBuffer->setSamplerNearest(i, nearest); cmdBuffer->setSamplerUnnormalized(i, true); } for (uint32_t i = frameInfo->layerCount; i < VKR_SAMPLER_SLOTS; i++) { cmdBuffer->bindTexture(i, nullptr); } } std::optional vulkan_screenshot( const struct FrameInfo_t *frameInfo, gamescope::Rc pScreenshotTexture, gamescope::Rc pYUVOutTexture ) { EOTF outputTF = frameInfo->outputEncodingEOTF; if (!frameInfo->applyOutputColorMgmt) outputTF = EOTF_Count; //Disable blending stuff. auto cmdBuffer = g_device.commandBuffer(); for (uint32_t i = 0; i < EOTF_Count; i++) cmdBuffer->bindColorMgmtLuts(i, frameInfo->shaperLut[i], frameInfo->lut3D[i]); cmdBuffer->bindPipeline( g_device.pipeline(SHADER_TYPE_BLIT, frameInfo->layerCount, frameInfo->ycbcrMask(), 0u, frameInfo->colorspaceMask(), outputTF )); bind_all_layers(cmdBuffer.get(), frameInfo); cmdBuffer->bindTarget(pScreenshotTexture); cmdBuffer->uploadConstants(frameInfo); const int pixelsPerGroup = 8; cmdBuffer->dispatch(div_roundup(currentOutputWidth, pixelsPerGroup), div_roundup(currentOutputHeight, pixelsPerGroup)); if ( pYUVOutTexture != nullptr ) { float scale = (float)pScreenshotTexture->width() / pYUVOutTexture->width(); CaptureConvertBlitData_t constants( scale, colorspace_to_conversion_from_srgb_matrix( pYUVOutTexture->streamColorspace() ) ); constants.halfExtent[0] = pYUVOutTexture->width() / 2.0f; constants.halfExtent[1] = pYUVOutTexture->height() / 2.0f; cmdBuffer->uploadConstants(constants); for (uint32_t i = 0; i < EOTF_Count; i++) cmdBuffer->bindColorMgmtLuts(i, nullptr, nullptr); cmdBuffer->bindPipeline(g_device.pipeline( SHADER_TYPE_RGB_TO_NV12, 1, 0, 0, GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB, EOTF_Count )); cmdBuffer->bindTexture(0, pScreenshotTexture); cmdBuffer->setTextureSrgb(0, true); cmdBuffer->setSamplerNearest(0, false); cmdBuffer->setSamplerUnnormalized(0, true); for (uint32_t i = 1; i < VKR_SAMPLER_SLOTS; i++) { cmdBuffer->bindTexture(i, nullptr); } cmdBuffer->bindTarget(pYUVOutTexture); const int pixelsPerGroup = 8; // For ycbcr, we operate on 2 pixels at a time, so use the half-extent. const int dispatchSize = pixelsPerGroup * 2; cmdBuffer->dispatch(div_roundup(pYUVOutTexture->width(), dispatchSize), div_roundup(pYUVOutTexture->height(), dispatchSize)); } uint64_t sequence = g_device.submit(std::move(cmdBuffer)); return sequence; } extern std::string g_reshade_effect; extern uint32_t g_reshade_technique_idx; ReshadeEffectPipeline *g_pLastReshadeEffect = nullptr; std::optional vulkan_composite( struct FrameInfo_t *frameInfo, gamescope::Rc pPipewireTexture, bool partial, gamescope::Rc pOutputOverride, bool increment, std::unique_ptr pInCommandBuffer ) { EOTF outputTF = frameInfo->outputEncodingEOTF; if (!frameInfo->applyOutputColorMgmt) outputTF = EOTF_Count; //Disable blending stuff. g_pLastReshadeEffect = nullptr; if (!g_reshade_effect.empty()) { if (frameInfo->layers[0].tex) { ReshadeEffectKey key { .path = g_reshade_effect, .bufferWidth = frameInfo->layers[0].tex->width(), .bufferHeight = frameInfo->layers[0].tex->height(), .bufferColorSpace = frameInfo->layers[0].colorspace, .bufferFormat = frameInfo->layers[0].tex->format(), .techniqueIdx = g_reshade_technique_idx, }; ReshadeEffectPipeline* pipeline = g_reshadeManager.pipeline(key); g_pLastReshadeEffect = pipeline; if (pipeline != nullptr) { uint64_t seq = pipeline->execute(frameInfo->layers[0].tex, &frameInfo->layers[0].tex); g_device.wait(seq); } } } else { g_reshadeManager.clear(); } gamescope::Rc compositeImage; if ( pOutputOverride ) compositeImage = pOutputOverride; else compositeImage = partial ? g_output.outputImagesPartialOverlay[ g_output.nOutImage ] : g_output.outputImages[ g_output.nOutImage ]; auto cmdBuffer = pInCommandBuffer ? std::move( pInCommandBuffer ) : g_device.commandBuffer(); for (uint32_t i = 0; i < EOTF_Count; i++) cmdBuffer->bindColorMgmtLuts(i, frameInfo->shaperLut[i], frameInfo->lut3D[i]); if ( frameInfo->useFSRLayer0 ) { uint32_t inputX = frameInfo->layers[0].tex->width(); uint32_t inputY = frameInfo->layers[0].tex->height(); uint32_t tempX = frameInfo->layers[0].integerWidth(); uint32_t tempY = frameInfo->layers[0].integerHeight(); update_tmp_images(tempX, tempY); cmdBuffer->bindPipeline(g_device.pipeline(SHADER_TYPE_EASU)); cmdBuffer->bindTarget(g_output.tmpOutput); cmdBuffer->bindTexture(0, frameInfo->layers[0].tex); cmdBuffer->setTextureSrgb(0, true); cmdBuffer->setSamplerUnnormalized(0, false); cmdBuffer->setSamplerNearest(0, false); cmdBuffer->uploadConstants(inputX, inputY, tempX, tempY); int pixelsPerGroup = 16; cmdBuffer->dispatch(div_roundup(tempX, pixelsPerGroup), div_roundup(tempY, pixelsPerGroup)); cmdBuffer->bindPipeline(g_device.pipeline(SHADER_TYPE_RCAS, frameInfo->layerCount, frameInfo->ycbcrMask() & ~1, 0u, frameInfo->colorspaceMask(), outputTF )); bind_all_layers(cmdBuffer.get(), frameInfo); cmdBuffer->bindTexture(0, g_output.tmpOutput); cmdBuffer->setTextureSrgb(0, true); cmdBuffer->setSamplerUnnormalized(0, false); cmdBuffer->setSamplerNearest(0, false); cmdBuffer->bindTarget(compositeImage); cmdBuffer->uploadConstants(frameInfo, g_upscaleFilterSharpness / 10.0f); cmdBuffer->dispatch(div_roundup(currentOutputWidth, pixelsPerGroup), div_roundup(currentOutputHeight, pixelsPerGroup)); } else if ( frameInfo->useNISLayer0 ) { uint32_t inputX = frameInfo->layers[0].tex->width(); uint32_t inputY = frameInfo->layers[0].tex->height(); uint32_t tempX = frameInfo->layers[0].integerWidth(); uint32_t tempY = frameInfo->layers[0].integerHeight(); update_tmp_images(tempX, tempY); float nisSharpness = (20 - g_upscaleFilterSharpness) / 20.0f; cmdBuffer->bindPipeline(g_device.pipeline(SHADER_TYPE_NIS)); cmdBuffer->bindTarget(g_output.tmpOutput); cmdBuffer->bindTexture(0, frameInfo->layers[0].tex); cmdBuffer->setTextureSrgb(0, true); cmdBuffer->setSamplerUnnormalized(0, false); cmdBuffer->setSamplerNearest(0, false); cmdBuffer->bindTexture(VKR_NIS_COEF_SCALER_SLOT, g_output.nisScalerImage); cmdBuffer->setSamplerUnnormalized(VKR_NIS_COEF_SCALER_SLOT, false); cmdBuffer->setSamplerNearest(VKR_NIS_COEF_SCALER_SLOT, false); cmdBuffer->bindTexture(VKR_NIS_COEF_USM_SLOT, g_output.nisUsmImage); cmdBuffer->setSamplerUnnormalized(VKR_NIS_COEF_USM_SLOT, false); cmdBuffer->setSamplerNearest(VKR_NIS_COEF_USM_SLOT, false); cmdBuffer->uploadConstants(inputX, inputY, tempX, tempY, nisSharpness); int pixelsPerGroupX = 32; int pixelsPerGroupY = 24; cmdBuffer->dispatch(div_roundup(tempX, pixelsPerGroupX), div_roundup(tempY, pixelsPerGroupY)); struct FrameInfo_t nisFrameInfo = *frameInfo; nisFrameInfo.layers[0].tex = g_output.tmpOutput; nisFrameInfo.layers[0].scale.x = 1.0f; nisFrameInfo.layers[0].scale.y = 1.0f; cmdBuffer->bindPipeline( g_device.pipeline(SHADER_TYPE_BLIT, nisFrameInfo.layerCount, nisFrameInfo.ycbcrMask(), 0u, nisFrameInfo.colorspaceMask(), outputTF )); bind_all_layers(cmdBuffer.get(), &nisFrameInfo); cmdBuffer->bindTarget(compositeImage); cmdBuffer->uploadConstants(&nisFrameInfo); int pixelsPerGroup = 8; cmdBuffer->dispatch(div_roundup(currentOutputWidth, pixelsPerGroup), div_roundup(currentOutputHeight, pixelsPerGroup)); } else if ( frameInfo->blurLayer0 ) { update_tmp_images(currentOutputWidth, currentOutputHeight); ShaderType type = SHADER_TYPE_BLUR_FIRST_PASS; uint32_t blur_layer_count = 1; // Also blur the override on top if we have one. if (frameInfo->layerCount >= 2 && frameInfo->layers[1].zpos == g_zposOverride) blur_layer_count++; cmdBuffer->bindPipeline(g_device.pipeline(type, blur_layer_count, frameInfo->ycbcrMask() & 0x3u, 0, frameInfo->colorspaceMask(), outputTF )); cmdBuffer->bindTarget(g_output.tmpOutput); for (uint32_t i = 0; i < blur_layer_count; i++) { cmdBuffer->bindTexture(i, frameInfo->layers[i].tex); cmdBuffer->setTextureSrgb(i, false); cmdBuffer->setSamplerUnnormalized(i, true); cmdBuffer->setSamplerNearest(i, false); } cmdBuffer->uploadConstants(frameInfo); int pixelsPerGroup = 8; cmdBuffer->dispatch(div_roundup(currentOutputWidth, pixelsPerGroup), div_roundup(currentOutputHeight, pixelsPerGroup)); bool useSrgbView = frameInfo->layers[0].colorspace == GAMESCOPE_APP_TEXTURE_COLORSPACE_LINEAR; type = frameInfo->blurLayer0 == BLUR_MODE_COND ? SHADER_TYPE_BLUR_COND : SHADER_TYPE_BLUR; cmdBuffer->bindPipeline(g_device.pipeline(type, frameInfo->layerCount, frameInfo->ycbcrMask(), blur_layer_count, frameInfo->colorspaceMask(), outputTF )); bind_all_layers(cmdBuffer.get(), frameInfo); cmdBuffer->bindTarget(compositeImage); cmdBuffer->bindTexture(VKR_BLUR_EXTRA_SLOT, g_output.tmpOutput); cmdBuffer->setTextureSrgb(VKR_BLUR_EXTRA_SLOT, !useSrgbView); // Inverted because it chooses whether to view as linear (sRGB view) or sRGB (raw view). It's horrible. I need to change it. cmdBuffer->setSamplerUnnormalized(VKR_BLUR_EXTRA_SLOT, true); cmdBuffer->setSamplerNearest(VKR_BLUR_EXTRA_SLOT, false); cmdBuffer->dispatch(div_roundup(currentOutputWidth, pixelsPerGroup), div_roundup(currentOutputHeight, pixelsPerGroup)); } else { cmdBuffer->bindPipeline( g_device.pipeline(SHADER_TYPE_BLIT, frameInfo->layerCount, frameInfo->ycbcrMask(), 0u, frameInfo->colorspaceMask(), outputTF )); bind_all_layers(cmdBuffer.get(), frameInfo); cmdBuffer->bindTarget(compositeImage); cmdBuffer->uploadConstants(frameInfo); const int pixelsPerGroup = 8; cmdBuffer->dispatch(div_roundup(currentOutputWidth, pixelsPerGroup), div_roundup(currentOutputHeight, pixelsPerGroup)); } if ( pPipewireTexture != nullptr ) { if (compositeImage->format() == pPipewireTexture->format() && compositeImage->width() == pPipewireTexture->width() && compositeImage->height() == pPipewireTexture->height()) { cmdBuffer->copyImage(compositeImage, pPipewireTexture); } else { const bool ycbcr = pPipewireTexture->isYcbcr(); float scale = (float)compositeImage->width() / pPipewireTexture->width(); if ( ycbcr ) { CaptureConvertBlitData_t constants( scale, colorspace_to_conversion_from_srgb_matrix( pPipewireTexture->streamColorspace() ) ); constants.halfExtent[0] = pPipewireTexture->width() / 2.0f; constants.halfExtent[1] = pPipewireTexture->height() / 2.0f; cmdBuffer->uploadConstants(constants); } else { BlitPushData_t constants( scale ); cmdBuffer->uploadConstants(constants); } for (uint32_t i = 0; i < EOTF_Count; i++) cmdBuffer->bindColorMgmtLuts(i, nullptr, nullptr); cmdBuffer->bindPipeline(g_device.pipeline( ycbcr ? SHADER_TYPE_RGB_TO_NV12 : SHADER_TYPE_BLIT, 1, 0, 0, GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB, EOTF_Count )); cmdBuffer->bindTexture(0, compositeImage); cmdBuffer->setTextureSrgb(0, true); cmdBuffer->setSamplerNearest(0, false); cmdBuffer->setSamplerUnnormalized(0, true); for (uint32_t i = 1; i < VKR_SAMPLER_SLOTS; i++) { cmdBuffer->bindTexture(i, nullptr); } cmdBuffer->bindTarget(pPipewireTexture); const int pixelsPerGroup = 8; // For ycbcr, we operate on 2 pixels at a time, so use the half-extent. const int dispatchSize = ycbcr ? pixelsPerGroup * 2 : pixelsPerGroup; cmdBuffer->dispatch(div_roundup(pPipewireTexture->width(), dispatchSize), div_roundup(pPipewireTexture->height(), dispatchSize)); } } uint64_t sequence = g_device.submit(std::move(cmdBuffer)); if ( !GetBackend()->UsesVulkanSwapchain() && pOutputOverride == nullptr && increment ) { g_output.nOutImage = ( g_output.nOutImage + 1 ) % 3; } return sequence; } void vulkan_wait( uint64_t ulSeqNo, bool bReset ) { return g_device.wait( ulSeqNo, bReset ); } gamescope::Rc vulkan_get_last_output_image( bool partial, bool defer ) { // Get previous image ( +2 ) // 1 2 3 // | // | uint32_t nRegularImage = ( g_output.nOutImage + 2 ) % 3; // Get previous previous image ( +1 ) // 1 2 3 // | // | uint32_t nDeferredImage = ( g_output.nOutImage + 1 ) % 3; uint32_t nOutImage = defer ? nDeferredImage : nRegularImage; if ( partial ) { //vk_log.infof( "Partial overlay frame: %d", nDeferredImage ); return g_output.outputImagesPartialOverlay[ nOutImage ]; } return g_output.outputImages[ nOutImage ]; } bool vulkan_primary_dev_id(dev_t *id) { *id = g_device.primaryDevId(); return g_device.hasDrmPrimaryDevId(); } bool vulkan_supports_modifiers(void) { return g_device.supportsModifiers(); } static void texture_destroy( struct wlr_texture *wlr_texture ) { VulkanWlrTexture_t *tex = (VulkanWlrTexture_t *)wlr_texture; wlr_buffer_unlock( tex->buf ); delete tex; } static const struct wlr_texture_impl texture_impl = { .destroy = texture_destroy, }; static const struct wlr_drm_format_set *renderer_get_texture_formats( struct wlr_renderer *wlr_renderer, uint32_t buffer_caps ) { if (buffer_caps & WLR_BUFFER_CAP_DMABUF) { return &sampledDRMFormats; } else if (buffer_caps & WLR_BUFFER_CAP_DATA_PTR) { return &sampledShmFormats; } else { return nullptr; } } static int renderer_get_drm_fd( struct wlr_renderer *wlr_renderer ) { return g_device.drmRenderFd(); } static struct wlr_texture *renderer_texture_from_buffer( struct wlr_renderer *wlr_renderer, struct wlr_buffer *buf ) { VulkanWlrTexture_t *tex = new VulkanWlrTexture_t(); wlr_texture_init( &tex->base, wlr_renderer, &texture_impl, buf->width, buf->height ); tex->buf = wlr_buffer_lock( buf ); // TODO: check format/modifier // TODO: if DMA-BUF, try importing it into Vulkan return &tex->base; } static struct wlr_render_pass *renderer_begin_buffer_pass( struct wlr_renderer *renderer, struct wlr_buffer *buffer, const struct wlr_buffer_pass_options *options ) { abort(); // unreachable } static const struct wlr_renderer_impl renderer_impl = { .get_texture_formats = renderer_get_texture_formats, .get_drm_fd = renderer_get_drm_fd, .texture_from_buffer = renderer_texture_from_buffer, .begin_buffer_pass = renderer_begin_buffer_pass, }; struct wlr_renderer *vulkan_renderer_create( void ) { VulkanRenderer_t *renderer = new VulkanRenderer_t(); wlr_renderer_init(&renderer->base, &renderer_impl, WLR_BUFFER_CAP_DMABUF | WLR_BUFFER_CAP_DATA_PTR); return &renderer->base; } gamescope::OwningRc vulkan_create_texture_from_wlr_buffer( struct wlr_buffer *buf, gamescope::OwningRc pBackendFb ) { struct wlr_dmabuf_attributes dmabuf = {0}; if ( wlr_buffer_get_dmabuf( buf, &dmabuf ) ) { return vulkan_create_texture_from_dmabuf( &dmabuf, pBackendFb ); } VkResult result; void *src; uint32_t drmFormat; size_t stride; if ( !wlr_buffer_begin_data_ptr_access( buf, WLR_BUFFER_DATA_PTR_ACCESS_READ, &src, &drmFormat, &stride ) ) { return nullptr; } uint32_t width = buf->width; uint32_t height = buf->height; VkBufferCreateInfo bufferCreateInfo = { .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = stride * height, .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, }; VkBuffer buffer; result = g_device.vk.CreateBuffer( g_device.device(), &bufferCreateInfo, nullptr, &buffer ); if ( result != VK_SUCCESS ) { wlr_buffer_end_data_ptr_access( buf ); return nullptr; } VkMemoryRequirements memRequirements; g_device.vk.GetBufferMemoryRequirements(g_device.device(), buffer, &memRequirements); uint32_t memTypeIndex = g_device.findMemoryType(VK_MEMORY_PROPERTY_HOST_COHERENT_BIT|VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, memRequirements.memoryTypeBits ); if ( memTypeIndex == ~0u ) { wlr_buffer_end_data_ptr_access( buf ); return nullptr; } VkMemoryAllocateInfo allocInfo = { .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .allocationSize = memRequirements.size, .memoryTypeIndex = memTypeIndex, }; VkDeviceMemory bufferMemory; result = g_device.vk.AllocateMemory( g_device.device(), &allocInfo, nullptr, &bufferMemory); if ( result != VK_SUCCESS ) { wlr_buffer_end_data_ptr_access( buf ); return nullptr; } result = g_device.vk.BindBufferMemory( g_device.device(), buffer, bufferMemory, 0 ); if ( result != VK_SUCCESS ) { wlr_buffer_end_data_ptr_access( buf ); return nullptr; } void *dst; result = g_device.vk.MapMemory( g_device.device(), bufferMemory, 0, VK_WHOLE_SIZE, 0, &dst ); if ( result != VK_SUCCESS ) { wlr_buffer_end_data_ptr_access( buf ); return nullptr; } memcpy( dst, src, stride * height ); g_device.vk.UnmapMemory( g_device.device(), bufferMemory ); wlr_buffer_end_data_ptr_access( buf ); gamescope::OwningRc pTex = new CVulkanTexture(); CVulkanTexture::createFlags texCreateFlags; texCreateFlags.bSampled = true; texCreateFlags.bTransferDst = true; texCreateFlags.bFlippable = true; if ( pTex->BInit( width, height, 1u, drmFormat, texCreateFlags, nullptr, 0, 0, nullptr, pBackendFb ) == false ) return nullptr; auto cmdBuffer = g_device.commandBuffer(); cmdBuffer->copyBufferToImage( buffer, 0, stride / DRMFormatGetBPP(drmFormat), pTex); // TODO: Sync this copyBufferToImage uint64_t sequence = g_device.submit(std::move(cmdBuffer)); g_device.wait(sequence); g_device.vk.DestroyBuffer(g_device.device(), buffer, nullptr); g_device.vk.FreeMemory(g_device.device(), bufferMemory, nullptr); return pTex; } ValveSoftware-gamescope-eb620ab/src/rendervulkan.hpp000066400000000000000000000714431502457270500227460ustar00rootroot00000000000000// Initialize Vulkan and composite stuff with a compute queue #pragma once #include #include #include #include #include #include #include #include #include #include "main.hpp" #include "gamescope_shared.h" #include "backend.h" #include "shaders/descriptor_set_constants.h" class CVulkanCmdBuffer; // 1: Fade Plane (Fade outs between switching focus) // 2: Video Underlay (The actual video) // 3: Video Streaming UI (Game, App) // 4: External Overlay (Mangoapp, etc) // 5: Primary Overlay (Steam Overlay) // 6: Cursor // or // 1: Fade Plane (Fade outs between switching focus) // 2: Base Plane (Game, App) // 3: Override Plane (Dropdowns, etc) // 4: External Overlay (Mangoapp, etc) // 5: Primary Overlay (Steam Overlay) // 6: Cursor #define k_nMaxLayers 6 #define k_nMaxYcbcrMask 16 #define k_nMaxYcbcrMask_ToPreCompile 3 #define k_nMaxBlurLayers 2 #define kMaxBlurRadius (37u / 2 + 1) enum BlurMode { BLUR_MODE_OFF = 0, BLUR_MODE_COND = 1, BLUR_MODE_ALWAYS = 2, }; enum EStreamColorspace : int { k_EStreamColorspace_Unknown = 0, k_EStreamColorspace_BT601 = 1, k_EStreamColorspace_BT601_Full = 2, k_EStreamColorspace_BT709 = 3, k_EStreamColorspace_BT709_Full = 4 }; #include #include #include #include #include #include "wlr_begin.hpp" #include #include #include "wlr_end.hpp" #define VK_NO_PROTOTYPES #include #include struct VulkanRenderer_t { struct wlr_renderer base; }; struct VulkanWlrTexture_t { struct wlr_texture base; struct wlr_buffer *buf; }; inline VkFormat ToSrgbVulkanFormat( VkFormat format ) { switch ( format ) { case VK_FORMAT_B8G8R8A8_UNORM: return VK_FORMAT_B8G8R8A8_SRGB; case VK_FORMAT_R8G8B8A8_UNORM: return VK_FORMAT_R8G8B8A8_SRGB; default: return format; } } inline VkFormat ToLinearVulkanFormat( VkFormat format ) { switch ( format ) { case VK_FORMAT_B8G8R8A8_SRGB: return VK_FORMAT_B8G8R8A8_UNORM; case VK_FORMAT_R8G8B8A8_SRGB: return VK_FORMAT_R8G8B8A8_UNORM; default: return format; } } inline GamescopeAppTextureColorspace VkColorSpaceToGamescopeAppTextureColorSpace(VkFormat format, VkColorSpaceKHR colorspace) { switch (colorspace) { default: case VK_COLOR_SPACE_SRGB_NONLINEAR_KHR: // We will use image view conversions for these 8888 formats. if (ToSrgbVulkanFormat(format) != ToLinearVulkanFormat(format)) return GAMESCOPE_APP_TEXTURE_COLORSPACE_LINEAR; return GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB; case VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT: return GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB; case VK_COLOR_SPACE_HDR10_ST2084_EXT: return GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ; } } class CVulkanTexture : public gamescope::RcObject { public: struct createFlags { createFlags( void ) { bFlippable = false; bMappable = false; bSampled = false; bStorage = false; bTransferSrc = false; bTransferDst = false; bLinear = false; bExportable = false; bOutputImage = false; bColorAttachment = false; imageType = VK_IMAGE_TYPE_2D; } bool bFlippable : 1; bool bMappable : 1; bool bSampled : 1; bool bStorage : 1; bool bTransferSrc : 1; bool bTransferDst : 1; bool bLinear : 1; bool bExportable : 1; bool bOutputImage : 1; bool bColorAttachment : 1; VkImageType imageType; }; bool BInit( uint32_t width, uint32_t height, uint32_t depth, uint32_t drmFormat, createFlags flags, wlr_dmabuf_attributes *pDMA = nullptr, uint32_t contentWidth = 0, uint32_t contentHeight = 0, CVulkanTexture *pExistingImageToReuseMemory = nullptr, gamescope::OwningRc pBackendFb = nullptr ); bool BInitFromSwapchain( VkImage image, uint32_t width, uint32_t height, VkFormat format ); uint32_t IncRef(); uint32_t DecRef(); bool IsInUse(); inline VkImageView view( bool linear ) { return linear ? m_linearView : m_srgbView; } inline VkImageView linearView() { return m_linearView; } inline VkImageView srgbView() { return m_srgbView; } inline VkImageView lumaView() { return m_lumaView; } inline VkImageView chromaView() { return m_chromaView; } inline uint32_t width() { return m_width; } inline uint32_t height() { return m_height; } inline uint32_t depth() { return m_depth; } inline uint32_t contentWidth() {return m_contentWidth; } inline uint32_t contentHeight() {return m_contentHeight; } inline uint32_t rowPitch() { return m_unRowPitch; } inline gamescope::IBackendFb* GetBackendFb() { return m_pBackendFb.get(); } inline uint8_t *mappedData() { return m_pMappedData; } inline VkFormat format() const { return m_format; } inline const struct wlr_dmabuf_attributes& dmabuf() { return m_dmabuf; } inline VkImage vkImage() { return m_vkImage; } inline bool outputImage() { return m_bOutputImage; } inline bool externalImage() { return m_bExternal; } inline VkDeviceSize totalSize() const { return m_size; } inline uint32_t drmFormat() const { return m_drmFormat; } inline uint32_t lumaOffset() const { return m_lumaOffset; } inline uint32_t lumaRowPitch() const { return m_lumaPitch; } inline uint32_t chromaOffset() const { return m_chromaOffset; } inline uint32_t chromaRowPitch() const { return m_chromaPitch; } inline EStreamColorspace streamColorspace() const { return m_streamColorspace; } inline void setStreamColorspace(EStreamColorspace colorspace) { m_streamColorspace = colorspace; } inline bool isYcbcr() const { return format() == VK_FORMAT_G8_B8R8_2PLANE_420_UNORM; } int memoryFence(); CVulkanTexture( void ); ~CVulkanTexture( void ); uint32_t queueFamily = VK_QUEUE_FAMILY_IGNORED; private: bool m_bInitialized = false; bool m_bExternal = false; bool m_bOutputImage = false; uint32_t m_drmFormat = DRM_FORMAT_INVALID; VkImage m_vkImage = VK_NULL_HANDLE; VkDeviceMemory m_vkImageMemory = VK_NULL_HANDLE; VkImageView m_srgbView = VK_NULL_HANDLE; VkImageView m_linearView = VK_NULL_HANDLE; VkImageView m_lumaView = VK_NULL_HANDLE; VkImageView m_chromaView = VK_NULL_HANDLE; uint32_t m_width = 0; uint32_t m_height = 0; uint32_t m_depth = 0; uint32_t m_contentWidth = 0; uint32_t m_contentHeight = 0; uint32_t m_unRowPitch = 0; VkDeviceSize m_size = 0; uint32_t m_lumaOffset = 0; uint32_t m_lumaPitch = 0; uint32_t m_chromaOffset = 0; uint32_t m_chromaPitch = 0; // If this texture owns the backend Fb (ie. it's an internal texture) gamescope::OwningRc m_pBackendFb; uint8_t *m_pMappedData = nullptr; VkFormat m_format = VK_FORMAT_UNDEFINED; EStreamColorspace m_streamColorspace = k_EStreamColorspace_Unknown; struct wlr_dmabuf_attributes m_dmabuf = {}; }; struct vec2_t { float x, y; }; static inline bool float_is_integer(float x) { return fabsf(ceilf(x) - x) <= 0.001f; } inline bool close_enough(float a, float b, float epsilon = 0.001f) { return fabsf(a - b) <= epsilon; } bool DRMFormatHasAlpha( uint32_t nDRMFormat ); enum AlphaBlendingMode_t { ALPHA_BLENDING_MODE_PREMULTIPLIED, ALPHA_BLENDING_MODE_COVERAGE, ALPHA_BLENDING_MODE_NONE, }; //#define DRM_MODE_BLEND_PREMULTI 0 //#define DRM_MODE_BLEND_COVERAGE 1 //#define DRM_MODE_BLEND_PIXEL_NONE 2 struct FrameInfo_t { bool useFSRLayer0; bool useNISLayer0; bool bFadingOut; BlurMode blurLayer0; int blurRadius; gamescope::Rc shaperLut[EOTF_Count]; gamescope::Rc lut3D[EOTF_Count]; bool allowVRR; bool applyOutputColorMgmt; // drm only EOTF outputEncodingEOTF; int layerCount; struct Layer_t { gamescope::Rc tex; int zpos; vec2_t offset; vec2_t scale; float opacity; GamescopeUpscaleFilter filter = GamescopeUpscaleFilter::LINEAR; bool blackBorder; bool applyColorMgmt; // drm only AlphaBlendingMode_t eAlphaBlendingMode = ALPHA_BLENDING_MODE_PREMULTIPLIED; std::shared_ptr ctm; std::shared_ptr hdr_metadata_blob; GamescopeAppTextureColorspace colorspace; bool isYcbcr() const { if ( !tex ) return false; return tex->isYcbcr(); } bool hasAlpha() const { if ( !tex ) return false; return DRMFormatHasAlpha( tex->drmFormat() ); } bool isScreenSize() const { return close_enough(scale.x, 1.0f) && close_enough(scale.y, 1.0f) && float_is_integer(offset.x) && float_is_integer(offset.y); } bool viewConvertsToLinearAutomatically() const { if (isYcbcr()) return true; return colorspace == GAMESCOPE_APP_TEXTURE_COLORSPACE_LINEAR || colorspace == GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB || colorspace == GAMESCOPE_APP_TEXTURE_COLORSPACE_PASSTHRU; } uint32_t integerWidth() const { return tex->width() / scale.x; } uint32_t integerHeight() const { return tex->height() / scale.y; } vec2_t offsetPixelCenter() const { float x = offset.x + 0.5f / scale.x; float y = offset.y + 0.5f / scale.y; return { x, y }; } } layers[ k_nMaxLayers ]; uint32_t borderMask() const { uint32_t result = 0; for (int i = 0; i < layerCount; i++) { if (layers[ i ].blackBorder) result |= 1 << i; } return result; } uint32_t ycbcrMask() const { uint32_t result = 0; for (int i = 0; i < layerCount; i++) { if (layers[ i ].isYcbcr()) result |= 1 << i; } return result; } uint32_t colorspaceMask() const { uint32_t result = 0; for (int i = 0; i < layerCount; i++) { result |= layers[ i ].colorspace << (i * GamescopeAppTextureColorspace_Bits); } return result; } }; extern uint32_t g_uCompositeDebug; extern gamescope::ConVar cv_composite_debug; namespace CompositeDebugFlag { static constexpr uint32_t Markers = 1u << 0; static constexpr uint32_t PlaneBorders = 1u << 1; static constexpr uint32_t Heatmap = 1u << 2; static constexpr uint32_t Heatmap_MSWCG = 1u << 3; static constexpr uint32_t Heatmap_Hard = 1u << 4; static constexpr uint32_t Markers_Partial = 1u << 5; static constexpr uint32_t Tonemap_Reinhard = 1u << 7; }; VkInstance vulkan_get_instance(void); bool vulkan_init(VkInstance instance, VkSurfaceKHR surface); bool vulkan_init_formats(void); bool vulkan_make_output(); gamescope::OwningRc vulkan_create_texture_from_dmabuf( struct wlr_dmabuf_attributes *pDMA, gamescope::OwningRc pBackendFb ); gamescope::OwningRc vulkan_create_texture_from_bits( uint32_t width, uint32_t height, uint32_t contentWidth, uint32_t contentHeight, uint32_t drmFormat, CVulkanTexture::createFlags texCreateFlags, void *bits ); gamescope::OwningRc vulkan_create_texture_from_wlr_buffer( struct wlr_buffer *buf, gamescope::OwningRc pBackendFb ); std::optional vulkan_composite( struct FrameInfo_t *frameInfo, gamescope::Rc pScreenshotTexture, bool partial, gamescope::Rc pOutputOverride = nullptr, bool increment = true, std::unique_ptr pInCommandBuffer = nullptr ); void vulkan_wait( uint64_t ulSeqNo, bool bReset ); gamescope::Rc vulkan_get_last_output_image( bool partial, bool defer ); gamescope::Rc vulkan_acquire_screenshot_texture(uint32_t width, uint32_t height, bool exportable, uint32_t drmFormat, EStreamColorspace colorspace = k_EStreamColorspace_Unknown); void vulkan_present_to_window( void ); void vulkan_garbage_collect( void ); bool vulkan_remake_swapchain( void ); bool vulkan_remake_output_images( void ); bool acquire_next_image( void ); bool vulkan_primary_dev_id(dev_t *id); bool vulkan_supports_modifiers(void); gamescope::Rc vulkan_create_1d_lut(uint32_t size); gamescope::Rc vulkan_create_3d_lut(uint32_t width, uint32_t height, uint32_t depth); void vulkan_update_luts(const gamescope::Rc& lut1d, const gamescope::Rc& lut3d, void* lut1d_data, void* lut3d_data); gamescope::Rc vulkan_get_hacky_blank_texture(); std::optional vulkan_screenshot( const struct FrameInfo_t *frameInfo, gamescope::Rc pScreenshotTexture, gamescope::Rc pYUVOutTexture ); struct wlr_renderer *vulkan_renderer_create( void ); using mat3x4 = std::array, 3>; #include "color_helpers_impl.h" struct gamescope_color_mgmt_t { bool enabled; uint32_t externalDirtyCtr; nightmode_t nightmode; float sdrGamutWideness = -1; // user property to widen gamut float flInternalDisplayBrightness = 500.f; float flSDROnHDRBrightness = 203.f; float flHDRInputGain = 1.f; float flSDRInputGain = 1.f; // HDR Display Metadata Override & Tonemapping ETonemapOperator hdrTonemapOperator = ETonemapOperator_None; tonemap_info_t hdrTonemapDisplayMetadata = { 0 }; tonemap_info_t hdrTonemapSourceMetadata = { 0 }; // the native colorimetry capabilities of the display displaycolorimetry_t displayColorimetry; EOTF displayEOTF; // the output encoding colorimetry // ie. for HDR displays we send an explicit 2020 colorimetry packet. // on SDR displays this is the same as displayColorimetry. displaycolorimetry_t outputEncodingColorimetry; EOTF outputEncodingEOTF; // If non-zero, use this as the emulated "virtual" white point for the output glm::vec2 outputVirtualWhite = { 0.f, 0.f }; EChromaticAdaptationMethod chromaticAdaptationMode = k_EChromaticAdapatationMethod_Bradford; std::shared_ptr appHDRMetadata; bool operator == (const gamescope_color_mgmt_t&) const = default; bool operator != (const gamescope_color_mgmt_t&) const = default; }; //namespace members from "color_helpers_impl.h": using rendervulkan::s_nLutEdgeSize3d; using rendervulkan::s_nLutSize1d; struct gamescope_color_mgmt_luts { bool bHasLut3D = false; bool bHasLut1D = false; uint16_t lut3d[s_nLutEdgeSize3d*s_nLutEdgeSize3d*s_nLutEdgeSize3d*4]; uint16_t lut1d[s_nLutSize1d*4]; gamescope::Rc vk_lut3d; gamescope::Rc vk_lut1d; bool HasLuts() const { return bHasLut3D && bHasLut1D; } void shutdown() { bHasLut1D = false; bHasLut3D = false; vk_lut1d = nullptr; vk_lut3d = nullptr; } void reset() { bHasLut1D = false; bHasLut3D = false; } }; struct gamescope_color_mgmt_tracker_t { gamescope_color_mgmt_t pending{}; gamescope_color_mgmt_t current{}; uint32_t serial{}; }; extern gamescope_color_mgmt_tracker_t g_ColorMgmt; extern gamescope_color_mgmt_luts g_ColorMgmtLuts[ EOTF_Count ]; struct VulkanOutput_t { VkSurfaceKHR surface; VkSurfaceCapabilitiesKHR surfaceCaps; std::vector< VkSurfaceFormatKHR > surfaceFormats; std::vector< VkPresentModeKHR > presentModes; std::shared_ptr swapchainHDRMetadata; VkSwapchainKHR swapChain; VkFence acquireFence; uint32_t nOutImage; // swapchain index in nested mode, or ping/pong between two RTs std::vector> outputImages; std::vector> outputImagesPartialOverlay; gamescope::OwningRc temporaryHackyBlankImage; uint32_t uOutputFormat = DRM_FORMAT_INVALID; uint32_t uOutputFormatOverlay = DRM_FORMAT_INVALID; std::array, 2> pScreenshotImages; // NIS and FSR gamescope::OwningRc tmpOutput; // NIS gamescope::OwningRc nisScalerImage; gamescope::OwningRc nisUsmImage; }; enum ShaderType { SHADER_TYPE_BLIT = 0, SHADER_TYPE_BLUR, SHADER_TYPE_BLUR_COND, SHADER_TYPE_BLUR_FIRST_PASS, SHADER_TYPE_EASU, SHADER_TYPE_RCAS, SHADER_TYPE_NIS, SHADER_TYPE_RGB_TO_NV12, SHADER_TYPE_COUNT }; extern VulkanOutput_t g_output; struct SamplerState { bool bNearest : 1; bool bUnnormalized : 1; SamplerState( void ) { bNearest = false; bUnnormalized = false; } bool operator==( const SamplerState& other ) const { return this->bNearest == other.bNearest && this->bUnnormalized == other.bUnnormalized; } }; namespace std { template <> struct hash { size_t operator()( const SamplerState& k ) const { return k.bNearest | (k.bUnnormalized << 1); } }; } struct PipelineInfo_t { ShaderType shaderType; uint32_t layerCount; uint32_t ycbcrMask; uint32_t blurLayerCount; uint32_t compositeDebug; uint32_t colorspaceMask; uint32_t outputEOTF; bool itmEnable; bool operator==(const PipelineInfo_t& o) const { return shaderType == o.shaderType && layerCount == o.layerCount && ycbcrMask == o.ycbcrMask && blurLayerCount == o.blurLayerCount && compositeDebug == o.compositeDebug && colorspaceMask == o.colorspaceMask && outputEOTF == o.outputEOTF && itmEnable == o.itmEnable; } }; static inline uint32_t hash_combine(uint32_t old_hash, uint32_t new_hash) { return old_hash ^ (new_hash + 0x9e3779b9 + (old_hash << 6) + (old_hash >> 2)); } namespace std { template <> struct hash { size_t operator()( const PipelineInfo_t& k ) const { uint32_t hash = k.shaderType; hash = hash_combine(hash, k.layerCount); hash = hash_combine(hash, k.ycbcrMask); hash = hash_combine(hash, k.blurLayerCount); hash = hash_combine(hash, k.compositeDebug); hash = hash_combine(hash, k.colorspaceMask); hash = hash_combine(hash, k.outputEOTF); hash = hash_combine(hash, k.itmEnable); return hash; } }; } static inline uint32_t div_roundup(uint32_t x, uint32_t y) { return (x + (y - 1)) / y; } #define VULKAN_INSTANCE_FUNCTIONS \ VK_FUNC(CreateDevice) \ VK_FUNC(EnumerateDeviceExtensionProperties) \ VK_FUNC(EnumeratePhysicalDevices) \ VK_FUNC(GetDeviceProcAddr) \ VK_FUNC(GetPhysicalDeviceFeatures2) \ VK_FUNC(GetPhysicalDeviceFormatProperties) \ VK_FUNC(GetPhysicalDeviceFormatProperties2) \ VK_FUNC(GetPhysicalDeviceImageFormatProperties2) \ VK_FUNC(GetPhysicalDeviceMemoryProperties) \ VK_FUNC(GetPhysicalDeviceQueueFamilyProperties) \ VK_FUNC(GetPhysicalDeviceProperties) \ VK_FUNC(GetPhysicalDeviceProperties2) \ VK_FUNC(GetPhysicalDeviceSurfaceCapabilitiesKHR) \ VK_FUNC(GetPhysicalDeviceSurfaceFormatsKHR) \ VK_FUNC(GetPhysicalDeviceSurfacePresentModesKHR) \ VK_FUNC(GetPhysicalDeviceSurfaceSupportKHR) #define VULKAN_DEVICE_FUNCTIONS \ VK_FUNC(AcquireNextImageKHR) \ VK_FUNC(AllocateCommandBuffers) \ VK_FUNC(AllocateDescriptorSets) \ VK_FUNC(AllocateMemory) \ VK_FUNC(BeginCommandBuffer) \ VK_FUNC(BindBufferMemory) \ VK_FUNC(BindImageMemory) \ VK_FUNC(CmdBeginRendering) \ VK_FUNC(CmdBindDescriptorSets) \ VK_FUNC(CmdBindPipeline) \ VK_FUNC(CmdClearColorImage) \ VK_FUNC(CmdCopyBufferToImage) \ VK_FUNC(CmdCopyImage) \ VK_FUNC(CmdDispatch) \ VK_FUNC(CmdDraw) \ VK_FUNC(CmdEndRendering) \ VK_FUNC(CmdPipelineBarrier) \ VK_FUNC(CmdPushConstants) \ VK_FUNC(CreateBuffer) \ VK_FUNC(CreateCommandPool) \ VK_FUNC(CreateComputePipelines) \ VK_FUNC(CreateDescriptorPool) \ VK_FUNC(CreateDescriptorSetLayout) \ VK_FUNC(CreateFence) \ VK_FUNC(CreateGraphicsPipelines) \ VK_FUNC(CreateImage) \ VK_FUNC(CreateImageView) \ VK_FUNC(CreatePipelineLayout) \ VK_FUNC(CreateSampler) \ VK_FUNC(CreateSamplerYcbcrConversion) \ VK_FUNC(CreateSemaphore) \ VK_FUNC(GetSemaphoreFdKHR) \ VK_FUNC(ImportSemaphoreFdKHR) \ VK_FUNC(CreateShaderModule) \ VK_FUNC(CreateSwapchainKHR) \ VK_FUNC(DestroyBuffer) \ VK_FUNC(DestroyDescriptorPool) \ VK_FUNC(DestroyDescriptorSetLayout) \ VK_FUNC(DestroyImage) \ VK_FUNC(DestroyImageView) \ VK_FUNC(DestroyPipeline) \ VK_FUNC(DestroySemaphore) \ VK_FUNC(DestroyPipelineLayout) \ VK_FUNC(DestroySampler) \ VK_FUNC(DestroySwapchainKHR) \ VK_FUNC(EndCommandBuffer) \ VK_FUNC(FreeCommandBuffers) \ VK_FUNC(FreeDescriptorSets) \ VK_FUNC(FreeMemory) \ VK_FUNC(GetBufferMemoryRequirements) \ VK_FUNC(GetDeviceQueue) \ VK_FUNC(GetImageDrmFormatModifierPropertiesEXT) \ VK_FUNC(GetImageMemoryRequirements) \ VK_FUNC(GetImageSubresourceLayout) \ VK_FUNC(GetMemoryFdKHR) \ VK_FUNC(GetSemaphoreCounterValue) \ VK_FUNC(GetSwapchainImagesKHR) \ VK_FUNC(MapMemory) \ VK_FUNC(QueuePresentKHR) \ VK_FUNC(QueueSubmit) \ VK_FUNC(QueueWaitIdle) \ VK_FUNC(ResetCommandBuffer) \ VK_FUNC(ResetFences) \ VK_FUNC(UnmapMemory) \ VK_FUNC(UpdateDescriptorSets) \ VK_FUNC(WaitForFences) \ VK_FUNC(WaitForPresentKHR) \ VK_FUNC(WaitSemaphores) \ VK_FUNC(SetHdrMetadataEXT) template constexpr T align(T what, U to) { return (what + to - 1) & ~(to - 1); } class CVulkanDevice; struct VulkanTimelineSemaphore_t { ~VulkanTimelineSemaphore_t(); CVulkanDevice *pDevice = nullptr; VkSemaphore pVkSemaphore = VK_NULL_HANDLE; int GetFd() const; }; struct VulkanTimelinePoint_t { std::shared_ptr pTimelineSemaphore; uint64_t ulPoint; }; class CVulkanDevice { public: bool BInit(VkInstance instance, VkSurfaceKHR surface); VkSampler sampler(SamplerState key); VkPipeline pipeline(ShaderType type, uint32_t layerCount = 1, uint32_t ycbcrMask = 0, uint32_t blur_layers = 0, uint32_t colorspace_mask = 0, uint32_t output_eotf = EOTF_Gamma22, bool itm_enable = false); int32_t findMemoryType( VkMemoryPropertyFlags properties, uint32_t requiredTypeBits ); std::unique_ptr commandBuffer(); uint64_t submit( std::unique_ptr cmdBuf); uint64_t submitInternal( CVulkanCmdBuffer* cmdBuf ); void wait(uint64_t sequence, bool reset = true); void waitIdle(bool reset = true); void garbageCollect(); inline VkDescriptorSet descriptorSet() { VkDescriptorSet ret = m_descriptorSets[m_currentDescriptorSet]; m_currentDescriptorSet = (m_currentDescriptorSet + 1) % m_descriptorSets.size(); return ret; } std::shared_ptr CreateTimelineSemaphore( uint64_t ulStartingPoint, bool bShared = false ); std::shared_ptr ImportTimelineSemaphore( gamescope::CTimeline *pTimeline ); static const uint32_t upload_buffer_size = 1920 * 1080 * 4; inline VkDevice device() { return m_device; } inline VkPhysicalDevice physDev() {return m_physDev; } inline VkInstance instance() { return m_instance; } inline VkQueue queue() {return m_queue;} inline VkQueue generalQueue() {return m_generalQueue;} inline VkCommandPool commandPool() {return m_commandPool;} inline VkCommandPool generalCommandPool() {return m_generalCommandPool;} inline uint32_t queueFamily() {return m_queueFamily;} inline uint32_t generalQueueFamily() {return m_generalQueueFamily;} inline VkBuffer uploadBuffer() {return m_uploadBuffer;} inline VkPipelineLayout pipelineLayout() {return m_pipelineLayout;} inline int drmRenderFd() {return m_drmRendererFd;} inline bool supportsModifiers() {return m_bSupportsModifiers;} inline bool hasDrmPrimaryDevId() {return m_bHasDrmPrimaryDevId;} inline dev_t primaryDevId() {return m_drmPrimaryDevId;} inline bool supportsFp16() {return m_bSupportsFp16;} inline std::pair uploadBufferData(uint32_t size) { assert(size <= upload_buffer_size); m_uploadBufferOffset = align(m_uploadBufferOffset, 16); if (m_uploadBufferOffset + size > upload_buffer_size) { fprintf(stderr, "Exceeded uploadBufferData\n"); waitIdle(false); } uint32_t uOffset = m_uploadBufferOffset; uint8_t *ptr = ((uint8_t*)m_uploadBufferData) + uOffset; m_uploadBufferOffset += size; return std::make_pair( ptr, uOffset ); } #define VK_FUNC(x) PFN_vk##x x = nullptr; struct { VULKAN_INSTANCE_FUNCTIONS VULKAN_DEVICE_FUNCTIONS } vk; #undef VK_FUNC void resetCmdBuffers(uint64_t sequence); protected: friend class CVulkanCmdBuffer; bool selectPhysDev(VkSurfaceKHR surface); bool createDevice(); bool createLayouts(); bool createPools(); bool createShaders(); bool createScratchResources(); VkPipeline compilePipeline(uint32_t layerCount, uint32_t ycbcrMask, ShaderType type, uint32_t blur_layer_count, uint32_t composite_debug, uint32_t colorspace_mask, uint32_t output_eotf, bool itm_enable); void compileAllPipelines(); VkDevice m_device = nullptr; VkPhysicalDevice m_physDev = nullptr; VkInstance m_instance = nullptr; VkQueue m_queue = nullptr; VkQueue m_generalQueue = nullptr; VkSamplerYcbcrConversion m_ycbcrConversion = VK_NULL_HANDLE; VkSampler m_ycbcrSampler = VK_NULL_HANDLE; VkDescriptorSetLayout m_descriptorSetLayout = VK_NULL_HANDLE; VkPipelineLayout m_pipelineLayout = VK_NULL_HANDLE; VkDescriptorPool m_descriptorPool = VK_NULL_HANDLE; VkCommandPool m_commandPool = VK_NULL_HANDLE; VkCommandPool m_generalCommandPool = VK_NULL_HANDLE; uint32_t m_queueFamily = -1; uint32_t m_generalQueueFamily = -1; int m_drmRendererFd = -1; dev_t m_drmPrimaryDevId = 0; bool m_bSupportsFp16 = false; bool m_bHasDrmPrimaryDevId = false; bool m_bSupportsModifiers = false; bool m_bInitialized = false; VkPhysicalDeviceMemoryProperties m_memoryProperties; std::unordered_map< SamplerState, VkSampler > m_samplerCache; std::array m_shaderModules; std::unordered_map m_pipelineMap; std::mutex m_pipelineMutex; static constexpr uint32_t k_uMaxConcurrentSubmits = 8; // currently just one set, no need to double buffer because we // vkQueueWaitIdle after each submit. // should be moved to the output if we are going to support multiple outputs std::array m_descriptorSets; uint32_t m_currentDescriptorSet = 0; VkBuffer m_uploadBuffer; VkDeviceMemory m_uploadBufferMemory; void *m_uploadBufferData; uint32_t m_uploadBufferOffset = 0; VkSemaphore m_scratchTimelineSemaphore; std::atomic m_submissionSeqNo = { 0 }; std::vector> m_unusedCmdBufs; std::map> m_pendingCmdBufs; }; struct TextureState { bool discarded : 1; bool dirty : 1; bool needsPresentLayout : 1; bool needsExport : 1; bool needsImport : 1; TextureState() { discarded = false; dirty = false; needsPresentLayout = false; needsExport = false; needsImport = false; } }; class CVulkanCmdBuffer { public: CVulkanCmdBuffer(CVulkanDevice *parent, VkCommandBuffer cmdBuffer, VkQueue queue, uint32_t queueFamily); ~CVulkanCmdBuffer(); CVulkanCmdBuffer(const CVulkanCmdBuffer& other) = delete; CVulkanCmdBuffer(CVulkanCmdBuffer&& other) = delete; CVulkanCmdBuffer& operator=(const CVulkanCmdBuffer& other) = delete; CVulkanCmdBuffer& operator=(CVulkanCmdBuffer&& other) = delete; inline VkCommandBuffer rawBuffer() {return m_cmdBuffer;} void reset(); void begin(); void end(); void bindTexture(uint32_t slot, gamescope::Rc texture); void bindColorMgmtLuts(uint32_t slot, gamescope::Rc lut1d, gamescope::Rc lut3d); void setTextureStorage(bool storage); void setTextureSrgb(uint32_t slot, bool srgb); void setSamplerNearest(uint32_t slot, bool nearest); void setSamplerUnnormalized(uint32_t slot, bool unnormalized); void bindTarget(gamescope::Rc target); void clearState(); template void uploadConstants(Args&&... args); void bindPipeline(VkPipeline pipeline); void dispatch(uint32_t x, uint32_t y = 1, uint32_t z = 1); void copyImage(gamescope::Rc src, gamescope::Rc dst); void copyBufferToImage(VkBuffer buffer, VkDeviceSize offset, uint32_t stride, gamescope::Rc dst); void prepareSrcImage(CVulkanTexture *image); void prepareDestImage(CVulkanTexture *image); void discardImage(CVulkanTexture *image); void markDirty(CVulkanTexture *image); void insertBarrier(bool flush = false); VkQueue queue() { return m_queue; } uint32_t queueFamily() { return m_queueFamily; } void AddDependency( std::shared_ptr pTimelineSemaphore, uint64_t ulPoint ); void AddSignal( std::shared_ptr pTimelineSemaphore, uint64_t ulPoint ); const std::vector &GetExternalDependencies() const { return m_ExternalDependencies; } const std::vector &GetExternalSignals() const { return m_ExternalSignals; } private: VkCommandBuffer m_cmdBuffer; CVulkanDevice *m_device; VkQueue m_queue; uint32_t m_queueFamily; // Per Use State std::vector> m_textureRefs; std::unordered_map m_textureState; // Draw State std::array m_boundTextures; std::bitset m_useSrgb; std::array m_samplerState; CVulkanTexture *m_target; std::array m_shaperLut; std::array m_lut3D; std::vector m_ExternalDependencies; std::vector m_ExternalSignals; uint32_t m_renderBufferOffset = 0; }; uint32_t VulkanFormatToDRM( VkFormat vkFormat, std::optional obHasAlphaOverride = std::nullopt ); VkFormat DRMFormatToVulkan( uint32_t nDRMFormat, bool bSrgb ); bool DRMFormatHasAlpha( uint32_t nDRMFormat ); uint32_t DRMFormatGetBPP( uint32_t nDRMFormat ); gamescope::OwningRc vulkan_create_flat_texture( uint32_t width, uint32_t height, uint8_t r, uint8_t g, uint8_t b, uint8_t a ); bool vulkan_supports_hdr10(); void vulkan_wait_idle(); extern CVulkanDevice g_device; ValveSoftware-gamescope-eb620ab/src/reshade/000077500000000000000000000000001502457270500211375ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/src/reshade_effect_manager.cpp000066400000000000000000002337141502457270500246630ustar00rootroot00000000000000#include #include #include #include "reshade_effect_manager.hpp" #include "log.hpp" #include "steamcompmgr.hpp" #include "effect_parser.hpp" #include "effect_codegen.hpp" #include "effect_preprocessor.hpp" #include "gamescope-reshade-protocol.h" #include "reshade_api_format.hpp" #include "convar.h" #include #define STB_IMAGE_RESIZE_IMPLEMENTATION #include #include #include #include #include #include // This is based on wl_array_for_each from `wayland-util.h` in the Wayland client library. #define uint8_array_for_each(pos, data, size) \ for (pos = (decltype(pos))data; (const char *)pos < ((const char *)data + size); (pos)++) static char* g_reshadeEffectPath = nullptr; static std::function g_effectReadyCallback = nullptr; static auto g_runtimeUniforms = std::unordered_map(); static std::mutex g_runtimeUniformsMutex; extern int g_nOutputRefresh; const char *homedir; std::string_view GetHomeDir() { static std::string s_sHomeDir = []() -> std::string { const char *pszHomeDir = getenv( "HOME" ); if ( pszHomeDir ) return pszHomeDir; return getpwuid( getuid() )->pw_dir; }(); return s_sHomeDir; } static std::string GetLocalUsrDir() { return std::string{ GetHomeDir() } + "/.local"; } static std::string GetUsrDir() { return "/usr"; } static LogScope reshade_log("gamescope_reshade"); /////////////// // Uniforms /////////////// class ReshadeUniform { public: ReshadeUniform(const reshadefx::uniform_info& info); virtual ~ReshadeUniform() {}; virtual void update(void* mappedBuffer) = 0; protected: void copy(void* mappedBuffer, const void* data, size_t size); template void copy(void* mappedBuffer, const T* thing); reshadefx::uniform_info m_info; }; class FrameTimeUniform : public ReshadeUniform { public: FrameTimeUniform(reshadefx::uniform_info uniformInfo); virtual void update(void* mappedBuffer) override; virtual ~FrameTimeUniform(); private: std::chrono::time_point lastFrame; }; class FrameCountUniform : public ReshadeUniform { public: FrameCountUniform(reshadefx::uniform_info uniformInfo); virtual void update(void* mappedBuffer) override; virtual ~FrameCountUniform(); private: int32_t count = 0; }; class RefreshRateUniform : public ReshadeUniform { public: RefreshRateUniform(reshadefx::uniform_info uniformInfo); virtual void update(void* mappedBuffer) override; virtual ~RefreshRateUniform(); private: int32_t count = 0; }; class DateUniform : public ReshadeUniform { public: DateUniform(reshadefx::uniform_info uniformInfo); virtual void update(void* mappedBuffer) override; virtual ~DateUniform(); }; class TimerUniform : public ReshadeUniform { public: TimerUniform(reshadefx::uniform_info uniformInfo); virtual void update(void* mappedBuffer) override; virtual ~TimerUniform(); private: std::chrono::time_point start; }; class PingPongUniform : public ReshadeUniform { public: PingPongUniform(reshadefx::uniform_info uniformInfo); virtual void update(void* mappedBuffer) override; virtual ~PingPongUniform(); private: std::chrono::time_point lastFrame; float min = 0.0f; float max = 0.0f; float stepMin = 0.0f; float stepMax = 0.0f; float smoothing = 0.0f; float currentValue[2] = {0.0f, 1.0f}; }; class RandomUniform : public ReshadeUniform { public: RandomUniform(reshadefx::uniform_info uniformInfo); virtual void update(void* mappedBuffer) override; virtual ~RandomUniform(); private: int max = 0; int min = 0; }; class KeyUniform : public ReshadeUniform { public: KeyUniform(reshadefx::uniform_info uniformInfo); virtual void update(void* mappedBuffer) override; virtual ~KeyUniform(); }; class MouseButtonUniform : public ReshadeUniform { public: MouseButtonUniform(reshadefx::uniform_info uniformInfo); virtual void update(void* mappedBuffer) override; virtual ~MouseButtonUniform(); }; class MousePointUniform : public ReshadeUniform { public: MousePointUniform(reshadefx::uniform_info uniformInfo); virtual void update(void* mappedBuffer) override; virtual ~MousePointUniform(); }; class MouseDeltaUniform : public ReshadeUniform { public: MouseDeltaUniform(reshadefx::uniform_info uniformInfo); virtual void update(void* mappedBuffer) override; virtual ~MouseDeltaUniform(); }; class DepthUniform : public ReshadeUniform { public: DepthUniform(reshadefx::uniform_info uniformInfo); virtual void update(void* mappedBuffer) override; virtual ~DepthUniform(); }; class RuntimeUniform : public ReshadeUniform { public: RuntimeUniform(reshadefx::uniform_info uniformInfo); void virtual update(void* mappedBuffer) override; virtual ~RuntimeUniform(); private: uint32_t offset; uint32_t size; std::string name; reshadefx::type type; std::variant, std::vector, std::vector> defaultValue; }; class DataUniform : public ReshadeUniform { public: DataUniform(reshadefx::uniform_info uniformInfo); virtual void update(void* mappedBuffer) override; virtual ~DataUniform(); }; ////////////////////////////////////////////////////////////////////////////////////////////////////////// ReshadeUniform::ReshadeUniform(const reshadefx::uniform_info& info) : m_info(info) { } void ReshadeUniform::copy(void* mappedBuffer, const void* data, size_t size) { assert(size <= m_info.size); std::memcpy(((uint8_t*)mappedBuffer) + m_info.offset, data, size); } template void ReshadeUniform::copy(void* mappedBuffer, const T* thing) { assert(m_info.type.array_length == 0 || m_info.type.array_length == 1); uint32_t zero_data[16] = {}; const auto copy_ = [&](const auto constantDatatype){ T array_stuff[16] = {}; const auto thingBuffer = [&](){ for (uint32_t i = 0; i < m_info.type.components(); i++) array_stuff[i] = (m_info.type.base == reshadefx::type::t_bool) ? !!(thing[i]) : thing[i]; return array_stuff; }; const auto defaultOrZeroBuffer = (m_info.has_initializer_value) ? static_cast(std::begin((m_info.initializer_value).*constantDatatype)) : zero_data; copy(mappedBuffer, (thing) ? thingBuffer() : defaultOrZeroBuffer, sizeof(T) * m_info.type.components()); }; switch (m_info.type.base) { case reshadefx::type::t_bool: // VkBool32 = uint32_t; copy_(&reshadefx::constant::as_uint); break; case reshadefx::type::t_int: copy_(&reshadefx::constant::as_int); break; case reshadefx::type::t_uint: copy_(&reshadefx::constant::as_uint); break; case reshadefx::type::t_float: copy_(&reshadefx::constant::as_float); break; default: reshade_log.errorf("Unknown uniform type!"); break; } } ////////////////////////////////////////////////////////////////////////////////////////////////////////// FrameTimeUniform::FrameTimeUniform(reshadefx::uniform_info uniformInfo) : ReshadeUniform(uniformInfo) { lastFrame = std::chrono::high_resolution_clock::now(); } void FrameTimeUniform::update(void* mappedBuffer) { auto currentFrame = std::chrono::high_resolution_clock::now(); std::chrono::duration duration = currentFrame - lastFrame; lastFrame = currentFrame; float frametime = duration.count(); copy(mappedBuffer, &frametime); } FrameTimeUniform::~FrameTimeUniform() { } ////////////////////////////////////////////////////////////////////////////////////////////////////////// FrameCountUniform::FrameCountUniform(reshadefx::uniform_info uniformInfo) : ReshadeUniform(uniformInfo) { } void FrameCountUniform::update(void* mappedBuffer) { copy(mappedBuffer, &count); count++; } FrameCountUniform::~FrameCountUniform() { } ////////////////////////////////////////////////////////////////////////////////////////////////////////// RefreshRateUniform::RefreshRateUniform(reshadefx::uniform_info uniformInfo) : ReshadeUniform(uniformInfo) { } void RefreshRateUniform::update(void* mappedBuffer) { uint32_t unRefreshRateMhz = (uint32_t)g_nOutputRefresh; copy(mappedBuffer, &unRefreshRateMhz); } RefreshRateUniform::~RefreshRateUniform() { } ////////////////////////////////////////////////////////////////////////////////////////////////////////// DateUniform::DateUniform(reshadefx::uniform_info uniformInfo) : ReshadeUniform(uniformInfo) { } void DateUniform::update(void* mappedBuffer) { auto now = std::chrono::system_clock::now(); std::time_t nowC = std::chrono::system_clock::to_time_t(now); struct tm* currentTime = std::localtime(&nowC); float year = 1900.0f + static_cast(currentTime->tm_year); float month = 1.0f + static_cast(currentTime->tm_mon); float day = static_cast(currentTime->tm_mday); float seconds = static_cast((currentTime->tm_hour * 60 + currentTime->tm_min) * 60 + currentTime->tm_sec); float date[] = {year, month, day, seconds}; copy(mappedBuffer, date); } DateUniform::~DateUniform() { } ////////////////////////////////////////////////////////////////////////////////////////////////////////// TimerUniform::TimerUniform(reshadefx::uniform_info uniformInfo) : ReshadeUniform(uniformInfo) { start = std::chrono::high_resolution_clock::now(); } void TimerUniform::update(void* mappedBuffer) { auto currentFrame = std::chrono::high_resolution_clock::now(); std::chrono::duration duration = currentFrame - start; float timer = duration.count(); copy(mappedBuffer, &timer); } TimerUniform::~TimerUniform() { } ////////////////////////////////////////////////////////////////////////////////////////////////////////// PingPongUniform::PingPongUniform(reshadefx::uniform_info uniformInfo) : ReshadeUniform(uniformInfo) { const auto matchesAnnotationName = [&](const auto& name){ return std::ranges::find_if(uniformInfo.annotations, std::bind_front(std::equal_to{}, name), &reshadefx::annotation::name);}; if (auto minAnnotation = matchesAnnotationName("min"); minAnnotation != uniformInfo.annotations.end()) { min = minAnnotation->type.is_floating_point() ? minAnnotation->value.as_float[0] : static_cast(minAnnotation->value.as_int[0]); } if (auto maxAnnotation = matchesAnnotationName("max"); maxAnnotation != uniformInfo.annotations.end()) { max = maxAnnotation->type.is_floating_point() ? maxAnnotation->value.as_float[0] : static_cast(maxAnnotation->value.as_int[0]); } if (auto smoothingAnnotation = matchesAnnotationName("smoothing"); smoothingAnnotation != uniformInfo.annotations.end()) { smoothing = smoothingAnnotation->type.is_floating_point() ? smoothingAnnotation->value.as_float[0] : static_cast(smoothingAnnotation->value.as_int[0]); } if (auto stepAnnotation = matchesAnnotationName("step"); stepAnnotation != uniformInfo.annotations.end()) { stepMin = stepAnnotation->type.is_floating_point() ? stepAnnotation->value.as_float[0] : static_cast(stepAnnotation->value.as_int[0]); stepMax = stepAnnotation->type.is_floating_point() ? stepAnnotation->value.as_float[1] : static_cast(stepAnnotation->value.as_int[1]); } lastFrame = std::chrono::high_resolution_clock::now(); } void PingPongUniform::update(void* mappedBuffer) { auto currentFrame = std::chrono::high_resolution_clock::now(); std::chrono::duration> frameTime = currentFrame - lastFrame; float increment = stepMax == 0 ? stepMin : (stepMin + std::fmod(static_cast(std::rand()), stepMax - stepMin + 1.0f)); if (currentValue[1] >= 0) { increment = std::max(increment - std::max(0.0f, smoothing - (max - currentValue[0])), 0.05f); increment *= frameTime.count(); if ((currentValue[0] += increment) >= max) { currentValue[0] = max, currentValue[1] = -1.0f; } } else { increment = std::max(increment - std::max(0.0f, smoothing - (currentValue[0] - min)), 0.05f); increment *= frameTime.count(); if ((currentValue[0] -= increment) <= min) { currentValue[0] = min, currentValue[1] = 1.0f; } } copy(mappedBuffer, currentValue); } PingPongUniform::~PingPongUniform() { } ////////////////////////////////////////////////////////////////////////////////////////////////////////// RandomUniform::RandomUniform(reshadefx::uniform_info uniformInfo) : ReshadeUniform(uniformInfo) { if (auto minAnnotation = std::ranges::find_if(uniformInfo.annotations, std::bind_front(std::equal_to{}, "min"), &reshadefx::annotation::name); minAnnotation != uniformInfo.annotations.end()) { min = minAnnotation->type.is_integral() ? minAnnotation->value.as_int[0] : static_cast(minAnnotation->value.as_float[0]); } if (auto maxAnnotation = std::ranges::find_if(uniformInfo.annotations, std::bind_front(std::equal_to{}, "max"), &reshadefx::annotation::name); maxAnnotation != uniformInfo.annotations.end()) { max = maxAnnotation->type.is_integral() ? maxAnnotation->value.as_int[0] : static_cast(maxAnnotation->value.as_float[0]); } } void RandomUniform::update(void* mappedBuffer) { int32_t value = min + (std::rand() % (max - min + 1)); copy(mappedBuffer, &value); } RandomUniform::~RandomUniform() { } ////////////////////////////////////////////////////////////////////////////////////////////////////////// KeyUniform::KeyUniform(reshadefx::uniform_info uniformInfo) : ReshadeUniform(uniformInfo) { } void KeyUniform::update(void* mappedBuffer) { VkBool32 keyDown = VK_FALSE; // TODO copy(mappedBuffer, &keyDown); } KeyUniform::~KeyUniform() { } ////////////////////////////////////////////////////////////////////////////////////////////////////////// MouseButtonUniform::MouseButtonUniform(reshadefx::uniform_info uniformInfo) : ReshadeUniform(uniformInfo) { } void MouseButtonUniform::update(void* mappedBuffer) { VkBool32 keyDown = VK_FALSE; // TODO copy(mappedBuffer, &keyDown); } MouseButtonUniform::~MouseButtonUniform() { } ////////////////////////////////////////////////////////////////////////////////////////////////////////// MousePointUniform::MousePointUniform(reshadefx::uniform_info uniformInfo) : ReshadeUniform(uniformInfo) { } void MousePointUniform::update(void* mappedBuffer) { MouseCursor *cursor = steamcompmgr_get_current_cursor(); int32_t point[2] = {0, 0}; if (cursor) { point[0] = cursor->x(); point[1] = cursor->y(); } copy(mappedBuffer, point); } MousePointUniform::~MousePointUniform() { } ////////////////////////////////////////////////////////////////////////////////////////////////////////// MouseDeltaUniform::MouseDeltaUniform(reshadefx::uniform_info uniformInfo) : ReshadeUniform(uniformInfo) { } void MouseDeltaUniform::update(void* mappedBuffer) { float delta[2] = {0.0f, 0.0f}; // TODO copy(mappedBuffer, delta); } MouseDeltaUniform::~MouseDeltaUniform() { } ////////////////////////////////////////////////////////////////////////////////////////////////////////// DepthUniform::DepthUniform(reshadefx::uniform_info uniformInfo) : ReshadeUniform(uniformInfo) { } void DepthUniform::update(void* mappedBuffer) { VkBool32 hasDepth = VK_FALSE; copy(mappedBuffer, &hasDepth); } DepthUniform::~DepthUniform() { } ////////////////////////////////////////////////////////////////////////////////////////////////////////// RuntimeUniform::RuntimeUniform(reshadefx::uniform_info uniformInfo) : ReshadeUniform(uniformInfo) { offset = uniformInfo.offset; size = uniformInfo.size; type = uniformInfo.type; name = std::find_if(uniformInfo.annotations.begin(), uniformInfo.annotations.end(), [](const auto& a) { return a.name == "source"; })->value.string_data; if (auto defaultValueAnnotation = std::find_if(uniformInfo.annotations.begin(), uniformInfo.annotations.end(), [](const auto& a) { return a.name == "defaultValue"; }); defaultValueAnnotation != uniformInfo.annotations.end()) { reshadefx::constant value = defaultValueAnnotation->value; if (type.is_floating_point()) { defaultValue = std::vector(value.as_float, value.as_float + type.components()); reshade_log.debugf("Found float* runtime uniform %s of size %d\n", name.c_str(), type.components()); } else if (type.is_boolean()) { defaultValue = std::vector(value.as_uint, value.as_uint + type.components()); reshade_log.debugf("Found bool* runtime uniform %s of size %d\n", name.c_str(), type.components()); } else if (type.is_numeric()) { if (type.is_signed()) { defaultValue = std::vector(value.as_int, value.as_int + type.components()); reshade_log.debugf("Found int32_t* runtime uniform %s of size %d\n", name.c_str(), type.components()); } else { defaultValue = std::vector(value.as_uint, value.as_uint + type.components()); reshade_log.debugf("Found uint32_t* runtime uniform %s of size %d\n", name.c_str(), type.components()); } } else { reshade_log.errorf("Tried to create a runtime uniform variable of an unsupported type\n"); } } } void RuntimeUniform::update(void* mappedBuffer) { std::variant, std::vector, std::vector> value; uint8_t* wl_value = nullptr; std::lock_guard lock(g_runtimeUniformsMutex); auto it = g_runtimeUniforms.find(name); if (it != g_runtimeUniforms.end()) { wl_value = it->second; } if (wl_value) { if (type.is_floating_point()) { value = std::vector(); float *float_value = nullptr; uint8_array_for_each(float_value, wl_value, type.components() * sizeof(float)) { std::get>(value).push_back(*float_value); } } else if (type.is_boolean()) { // convert to a uint32_t vector, that's how the reshade uniform code understands booleans value = std::vector(); uint8_t *bool_value = nullptr; uint8_array_for_each(bool_value, wl_value, type.components() * sizeof(uint8_t)) { std::get>(value).push_back(*bool_value); } } else if (type.is_numeric()) { if (type.is_signed()) { value = std::vector(); int32_t *int_value = nullptr; uint8_array_for_each(int_value, wl_value, type.components() * sizeof(int32_t)) { std::get>(value).push_back(*int_value); } } else { value = std::vector(); uint32_t *uint_value = nullptr; uint8_array_for_each(uint_value, wl_value, type.components() * sizeof(uint32_t)) { std::get>(value).push_back(*uint_value); } } } } if (std::holds_alternative(value)) { value = defaultValue; } if (std::holds_alternative>(value)) { std::vector& vec = std::get>(value); std::memcpy((uint8_t*) mappedBuffer + offset, vec.data(), vec.size() * sizeof(float)); } else if (std::holds_alternative>(value)) { std::vector& vec = std::get>(value); std::memcpy((uint8_t*) mappedBuffer + offset, vec.data(), vec.size() * sizeof(int32_t)); } else if (std::holds_alternative>(value)) { std::vector& vec = std::get>(value); std::memcpy((uint8_t*) mappedBuffer + offset, vec.data(), vec.size() * sizeof(uint32_t)); } } RuntimeUniform::~RuntimeUniform() { } ////////////////////////////////////////////////////////////////////////////////////////////////////////// DataUniform::DataUniform(reshadefx::uniform_info uniformInfo) : ReshadeUniform(uniformInfo) { } void DataUniform::update(void* mappedBuffer) { copy(mappedBuffer, nullptr); } DataUniform::~DataUniform() { } static std::vector> createReshadeUniforms(const reshadefx::module& module, uint32_t *pFlags) { std::vector> uniforms; for (auto& uniform : module.uniforms) { auto sourceAnnotation = std::ranges::find_if(uniform.annotations, std::bind_front(std::equal_to{}, "source"), &reshadefx::annotation::name); if (sourceAnnotation == uniform.annotations.end()) { uniforms.push_back(std::make_shared(uniform)); continue; } else { auto& source = sourceAnnotation->value.string_data; if (source == "frametime") { uniforms.push_back(std::make_shared(uniform)); } else if (source == "framecount") { uniforms.push_back(std::make_shared(uniform)); } else if (source == "gamescope_refresh_mhz") { uniforms.push_back(std::make_shared(uniform)); } else if (source == "date") { uniforms.push_back(std::make_shared(uniform)); } else if (source == "timer") { uniforms.push_back(std::make_shared(uniform)); } else if (source == "pingpong") { uniforms.push_back(std::make_shared(uniform)); } else if (source == "random") { uniforms.push_back(std::make_shared(uniform)); } else if (source == "key") { uniforms.push_back(std::make_shared(uniform)); } else if (source == "mousebutton") { uniforms.push_back(std::make_shared(uniform)); } else if (source == "mousepoint") { uniforms.push_back(std::make_shared(uniform)); } else if (source == "mousedelta") { uniforms.push_back(std::make_shared(uniform)); } else if (source == "bufready_depth") { uniforms.push_back(std::make_shared(uniform)); } else if (source == "gamescope_always_paint") { ( *pFlags ) |= ReshadeEffectFlag::AlwaysScanout; } else if (!source.empty()) { uniforms.push_back(std::make_shared(uniform)); } } } return uniforms; } // static reshade::api::color_space ConvertToReshadeColorSpace(GamescopeAppTextureColorspace colorspace) { switch (colorspace) { default: return reshade::api::color_space::unknown; case GAMESCOPE_APP_TEXTURE_COLORSPACE_LINEAR: /* Actually SRGB -> Linear... I should change this cause its confusing */ case GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB: return reshade::api::color_space::srgb_nonlinear; case GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB: return reshade::api::color_space::extended_srgb_linear; case GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ: return reshade::api::color_space::hdr10_st2084; } } static VkFormat ConvertReshadeFormat(reshadefx::texture_format texFormat) { switch (texFormat) { case reshadefx::texture_format::r8: return VK_FORMAT_R8_UNORM; case reshadefx::texture_format::r16f: return VK_FORMAT_R16_SFLOAT; case reshadefx::texture_format::r32f: return VK_FORMAT_R32_SFLOAT; case reshadefx::texture_format::rg8: return VK_FORMAT_R8G8_UNORM; case reshadefx::texture_format::rg16: return VK_FORMAT_R16G16_UNORM; case reshadefx::texture_format::rg16f: return VK_FORMAT_R16G16_SFLOAT; case reshadefx::texture_format::rg32f: return VK_FORMAT_R32G32_SFLOAT; case reshadefx::texture_format::rgba8: return VK_FORMAT_R8G8B8A8_UNORM; case reshadefx::texture_format::rgba16: return VK_FORMAT_R16G16B16A16_UNORM; case reshadefx::texture_format::rgba16f: return VK_FORMAT_R16G16B16A16_SFLOAT; case reshadefx::texture_format::rgba32f: return VK_FORMAT_R32G32B32A32_SFLOAT; case reshadefx::texture_format::rgb10a2: return VK_FORMAT_A2R10G10B10_UNORM_PACK32; default: reshade_log.errorf("Couldn't convert texture format: %d\n", static_cast(texFormat)); return VK_FORMAT_UNDEFINED; } } #if 0 static VkCompareOp ConvertReshadeCompareOp(reshadefx::pass_stencil_func compareOp) { switch (compareOp) { case reshadefx::pass_stencil_func::never: return VK_COMPARE_OP_NEVER; case reshadefx::pass_stencil_func::less: return VK_COMPARE_OP_LESS; case reshadefx::pass_stencil_func::equal: return VK_COMPARE_OP_EQUAL; case reshadefx::pass_stencil_func::less_equal: return VK_COMPARE_OP_LESS_OR_EQUAL; case reshadefx::pass_stencil_func::greater: return VK_COMPARE_OP_GREATER; case reshadefx::pass_stencil_func::not_equal: return VK_COMPARE_OP_NOT_EQUAL; case reshadefx::pass_stencil_func::greater_equal: return VK_COMPARE_OP_GREATER_OR_EQUAL; case reshadefx::pass_stencil_func::always: return VK_COMPARE_OP_ALWAYS; default: return VK_COMPARE_OP_ALWAYS; } } static VkStencilOp ConvertReshadeStencilOp(reshadefx::pass_stencil_op stencilOp) { switch (stencilOp) { case reshadefx::pass_stencil_op::zero: return VK_STENCIL_OP_ZERO; case reshadefx::pass_stencil_op::keep: return VK_STENCIL_OP_KEEP; case reshadefx::pass_stencil_op::replace: return VK_STENCIL_OP_REPLACE; case reshadefx::pass_stencil_op::increment_saturate: return VK_STENCIL_OP_INCREMENT_AND_CLAMP; case reshadefx::pass_stencil_op::decrement_saturate: return VK_STENCIL_OP_DECREMENT_AND_CLAMP; case reshadefx::pass_stencil_op::invert: return VK_STENCIL_OP_INVERT; case reshadefx::pass_stencil_op::increment: return VK_STENCIL_OP_INCREMENT_AND_WRAP; case reshadefx::pass_stencil_op::decrement: return VK_STENCIL_OP_DECREMENT_AND_WRAP; default: return VK_STENCIL_OP_KEEP; } } #endif static VkBlendOp ConvertReshadeBlendOp(reshadefx::pass_blend_op blendOp) { switch (blendOp) { case reshadefx::pass_blend_op::add: return VK_BLEND_OP_ADD; case reshadefx::pass_blend_op::subtract: return VK_BLEND_OP_SUBTRACT; case reshadefx::pass_blend_op::reverse_subtract: return VK_BLEND_OP_REVERSE_SUBTRACT; case reshadefx::pass_blend_op::min: return VK_BLEND_OP_MIN; case reshadefx::pass_blend_op::max: return VK_BLEND_OP_MAX; default: return VK_BLEND_OP_ADD; } } static VkBlendFactor ConvertReshadeBlendFactor(reshadefx::pass_blend_factor blendFactor) { switch (blendFactor) { case reshadefx::pass_blend_factor::zero: return VK_BLEND_FACTOR_ZERO; case reshadefx::pass_blend_factor::one: return VK_BLEND_FACTOR_ONE; case reshadefx::pass_blend_factor::source_color: return VK_BLEND_FACTOR_SRC_COLOR; case reshadefx::pass_blend_factor::source_alpha: return VK_BLEND_FACTOR_SRC_ALPHA; case reshadefx::pass_blend_factor::one_minus_source_color: return VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR; case reshadefx::pass_blend_factor::one_minus_source_alpha: return VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; case reshadefx::pass_blend_factor::dest_alpha: return VK_BLEND_FACTOR_DST_ALPHA; case reshadefx::pass_blend_factor::one_minus_dest_alpha: return VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA; case reshadefx::pass_blend_factor::dest_color: return VK_BLEND_FACTOR_DST_COLOR; case reshadefx::pass_blend_factor::one_minus_dest_color: return VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR; default: return VK_BLEND_FACTOR_ZERO; } } static VkSamplerAddressMode ConvertReshadeAddressMode(reshadefx::texture_address_mode addressMode) { switch (addressMode) { case reshadefx::texture_address_mode::wrap: return VK_SAMPLER_ADDRESS_MODE_REPEAT; case reshadefx::texture_address_mode::mirror: return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT; case reshadefx::texture_address_mode::clamp: return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; case reshadefx::texture_address_mode::border: return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; } return VK_SAMPLER_ADDRESS_MODE_REPEAT; } static void ConvertReshadeFilter(const reshadefx::filter_mode& textureFilter, VkFilter& minFilter, VkFilter& magFilter, VkSamplerMipmapMode& mipmapMode) { switch (textureFilter) { case reshadefx::filter_mode::min_mag_mip_point: minFilter = VK_FILTER_NEAREST; magFilter = VK_FILTER_NEAREST; mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; return; case reshadefx::filter_mode::min_mag_point_mip_linear: minFilter = VK_FILTER_NEAREST; magFilter = VK_FILTER_NEAREST; mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; return; case reshadefx::filter_mode::min_point_mag_linear_mip_point: minFilter = VK_FILTER_NEAREST; magFilter = VK_FILTER_LINEAR; mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; return; case reshadefx::filter_mode::min_point_mag_mip_linear: minFilter = VK_FILTER_NEAREST; magFilter = VK_FILTER_LINEAR; mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; return; case reshadefx::filter_mode::min_linear_mag_mip_point: minFilter = VK_FILTER_LINEAR; magFilter = VK_FILTER_NEAREST; mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; return; case reshadefx::filter_mode::min_linear_mag_point_mip_linear: minFilter = VK_FILTER_LINEAR; magFilter = VK_FILTER_NEAREST; mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; return; case reshadefx::filter_mode::min_mag_linear_mip_point: minFilter = VK_FILTER_LINEAR; magFilter = VK_FILTER_LINEAR; mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; return; default: case reshadefx::filter_mode::min_mag_mip_linear: minFilter = VK_FILTER_LINEAR; magFilter = VK_FILTER_LINEAR; mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; return; } } static uint32_t GetFormatBitDepth(VkFormat format) { switch (format) { default: case VK_FORMAT_G8_B8R8_2PLANE_420_UNORM: case VK_FORMAT_B8G8R8A8_UNORM: case VK_FORMAT_B8G8R8A8_SRGB: case VK_FORMAT_R8G8B8A8_UNORM: case VK_FORMAT_R8G8B8A8_SRGB: return 8; case VK_FORMAT_R5G6B5_UNORM_PACK16: return 6; case VK_FORMAT_R16G16B16A16_SFLOAT: case VK_FORMAT_R16G16B16A16_UNORM: return 16; case VK_FORMAT_A2B10G10R10_UNORM_PACK32: case VK_FORMAT_A2R10G10B10_UNORM_PACK32: return 10; } } ReshadeEffectPipeline::ReshadeEffectPipeline() { } ReshadeEffectPipeline::~ReshadeEffectPipeline() { m_device->waitIdle(); for (auto& pipeline : m_pipelines) m_device->vk.DestroyPipeline(m_device->device(), pipeline, nullptr); m_pipelines.clear(); for (auto& sampler : m_samplers) m_device->vk.DestroySampler(m_device->device(), sampler.sampler, nullptr); m_samplers.clear(); m_uniforms.clear(); m_textures.clear(); m_rt = nullptr; m_cmdBuffer = std::nullopt; m_device->vk.DestroyBuffer(m_device->device(), m_buffer, nullptr); m_device->vk.FreeMemory(m_device->device(), m_bufferMemory, nullptr); m_mappedPtr = nullptr; for (uint32_t i = 0; i < GAMESCOPE_RESHADE_DESCRIPTOR_SET_COUNT; i++) { m_device->vk.FreeDescriptorSets(m_device->device(), m_descriptorPool, 1, &m_descriptorSets[i]); m_device->vk.DestroyDescriptorSetLayout(m_device->device(), m_descriptorSetLayouts[i], nullptr); } m_device->vk.DestroyDescriptorPool(m_device->device(), m_descriptorPool, nullptr); m_device->vk.DestroyPipelineLayout(m_device->device(), m_pipelineLayout, nullptr); } bool ReshadeEffectPipeline::init(CVulkanDevice *device, const ReshadeEffectKey &key) { m_key = key; m_device = device; VkPhysicalDeviceProperties deviceProperties; device->vk.GetPhysicalDeviceProperties(device->physDev(), &deviceProperties); reshadefx::preprocessor pp; pp.add_macro_definition("__RESHADE__", std::to_string(INT_MAX)); pp.add_macro_definition("__RESHADE_PERFORMANCE_MODE__", "0"); pp.add_macro_definition("__VENDOR__", std::to_string(deviceProperties.vendorID)); pp.add_macro_definition("__DEVICE__", std::to_string(deviceProperties.deviceID)); pp.add_macro_definition("__RENDERER__", std::to_string(0x20000)); pp.add_macro_definition("__APPLICATION__", std::to_string(0x0)); pp.add_macro_definition("BUFFER_WIDTH", std::to_string(key.bufferWidth)); pp.add_macro_definition("BUFFER_HEIGHT", std::to_string(key.bufferHeight)); pp.add_macro_definition("BUFFER_RCP_WIDTH", "(1.0 / BUFFER_WIDTH)"); pp.add_macro_definition("BUFFER_RCP_HEIGHT", "(1.0 / BUFFER_HEIGHT)"); pp.add_macro_definition("BUFFER_COLOR_SPACE", std::to_string(static_cast(ConvertToReshadeColorSpace(key.bufferColorSpace)))); pp.add_macro_definition("BUFFER_COLOR_BIT_DEPTH", std::to_string(GetFormatBitDepth(key.bufferFormat))); pp.add_macro_definition("GAMESCOPE", "1"); pp.add_macro_definition("GAMESCOPE_SDR_ON_HDR_NITS", std::to_string(g_ColorMgmt.pending.flSDROnHDRBrightness)); std::string gamescope_reshade_share_path = "/share/gamescope/reshade"; std::string local_reshade_path = GetLocalUsrDir() + gamescope_reshade_share_path; std::string global_reshade_path = GetUsrDir() + gamescope_reshade_share_path; pp.add_include_path(local_reshade_path + "/Shaders"); pp.add_include_path(global_reshade_path + "/Shaders"); std::string local_shader_file_path = local_reshade_path + "/Shaders/" + key.path; std::string global_shader_file_path = global_reshade_path + "/Shaders/" + key.path; if (!pp.append_file(local_shader_file_path)) { if (!pp.append_file(global_shader_file_path)) { reshade_log.errorf("Failed to load reshade fx file: %s (%s or %s) - %s", key.path.c_str(), local_shader_file_path.c_str(), global_shader_file_path.c_str(), pp.errors().c_str()); return false; } } std::string errors = pp.errors(); if (!errors.empty()) { reshade_log.errorf("Failed to parse reshade fx shader module: %s", errors.c_str()); return false; } std::unique_ptr codegen(reshadefx::create_codegen_spirv( true /* vulkan semantics */, true /* debug info */, false /* uniforms to spec constants */, false /*flip vertex shader*/)); reshadefx::parser parser; parser.parse(pp.output(), codegen.get()); errors = parser.errors(); if (!errors.empty()) { reshade_log.errorf("Failed to parse reshade fx shader module: %s", errors.c_str()); return false; } m_module = std::make_unique(); codegen->write_result(*m_module); #if 0 FILE *f = fopen("test.spv", "wb"); fwrite(m_module->code.data(), 1, m_module->code.size(), f); fclose(f); #endif if (m_module->techniques.size() <= key.techniqueIdx) { reshade_log.errorf("Invalid technique index"); return false; } auto& technique = m_module->techniques[key.techniqueIdx]; reshade_log.infof("Using technique: %s\n", technique.name.c_str()); // Allocate command buffers { VkCommandBufferAllocateInfo commandBufferAllocateInfo = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, .commandPool = device->generalCommandPool(), .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, .commandBufferCount = 1 }; VkCommandBuffer cmdBuffer = VK_NULL_HANDLE; VkResult result = device->vk.AllocateCommandBuffers(device->device(), &commandBufferAllocateInfo, &cmdBuffer); if (result != VK_SUCCESS) { reshade_log.errorf("vkAllocateCommandBuffers failed"); return false; } m_cmdBuffer.emplace(device, cmdBuffer, device->generalQueue(), device->generalQueueFamily()); } // Create Uniform Buffer { VkBufferCreateInfo bufferCreateInfo = { .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = m_module->total_uniform_size, .usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, }; VkResult result = device->vk.CreateBuffer(device->device(), &bufferCreateInfo, nullptr, &m_buffer); if (result != VK_SUCCESS) { reshade_log.errorf("vkCreateBuffer failed"); return false; } VkMemoryRequirements memRequirements; device->vk.GetBufferMemoryRequirements(device->device(), m_buffer, &memRequirements); uint32_t memTypeIndex = device->findMemoryType(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, memRequirements.memoryTypeBits); assert(memTypeIndex != ~0u); VkMemoryAllocateInfo allocInfo = { .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .allocationSize = memRequirements.size, .memoryTypeIndex = memTypeIndex, }; result = device->vk.AllocateMemory(device->device(), &allocInfo, nullptr, &m_bufferMemory); if (result != VK_SUCCESS) { reshade_log.errorf("vkAllocateMemory failed"); return false; } device->vk.BindBufferMemory(device->device(), m_buffer, m_bufferMemory, 0); if (result != VK_SUCCESS) { reshade_log.errorf("vkBindBufferMemory failed"); return false; } result = device->vk.MapMemory(device->device(), m_bufferMemory, 0, VK_WHOLE_SIZE, 0, &m_mappedPtr); if (result != VK_SUCCESS) { reshade_log.errorf("vkMapMemory failed"); return false; } } // Create Uniforms m_uniforms = createReshadeUniforms(*m_module, &m_flags); // Create Textures { m_rt = new CVulkanTexture(); CVulkanTexture::createFlags flags; flags.bSampled = true; flags.bStorage = true; flags.bColorAttachment = true; bool ret = m_rt->BInit(m_key.bufferWidth, m_key.bufferHeight, 1, VulkanFormatToDRM(m_key.bufferFormat), flags, nullptr); assert(ret); } for (const auto& tex : m_module->textures) { gamescope::Rc texture; if (tex.semantic.empty()) { texture = new CVulkanTexture(); CVulkanTexture::createFlags flags; flags.bSampled = true; // Always need storage. flags.bStorage = true; if (tex.render_target) flags.bColorAttachment = true; // Not supported rn. assert(tex.levels == 1); assert(tex.type == reshadefx::texture_type::texture_2d); bool ret = texture->BInit(tex.width, tex.height, tex.depth, VulkanFormatToDRM(ConvertReshadeFormat(tex.format)), flags, nullptr); assert(ret); } if (const auto source = std::ranges::find_if(tex.annotations , std::bind_front(std::equal_to{}, "source"), &reshadefx::annotation::name); source != tex.annotations.end()) { std::string filePath = local_reshade_path + "/Textures/" + source->value.string_data; int w, h, channels; unsigned char *data = stbi_load(filePath.c_str(), &w, &h, &channels, STBI_rgb_alpha); if (!data) { filePath = global_reshade_path + "/Textures/" + source->value.string_data; data = stbi_load(filePath.c_str(), &w, &h, &channels, STBI_rgb_alpha); } if (data) { uint8_t *pixels = data; std::vector resized_data; if (w != (int)texture->width() || h != (int)texture->height()) { resized_data.resize(texture->width() * texture->height() * 4); stbir_resize_uint8(data, w, h, 0, resized_data.data(), texture->width(), texture->height(), 0, STBI_rgb_alpha); w = texture->width(); h = texture->height(); pixels = resized_data.data(); } size_t size = w * h * 4; VkBufferCreateInfo bufferCreateInfo = { .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = size, .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, }; VkBuffer scratchBuffer = VK_NULL_HANDLE; VkResult result = device->vk.CreateBuffer(device->device(), &bufferCreateInfo, nullptr, &scratchBuffer); if (result != VK_SUCCESS) { reshade_log.errorf("Failed to create scratch buffer"); return false; } VkMemoryRequirements memRequirements; device->vk.GetBufferMemoryRequirements(device->device(), scratchBuffer, &memRequirements); uint32_t memTypeIndex = device->findMemoryType(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, memRequirements.memoryTypeBits); assert(memTypeIndex != ~0u); VkMemoryAllocateInfo allocInfo = { .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .allocationSize = memRequirements.size, .memoryTypeIndex = memTypeIndex, }; VkDeviceMemory scratchMemory = VK_NULL_HANDLE; result = device->vk.AllocateMemory(device->device(), &allocInfo, nullptr, &scratchMemory); if (result != VK_SUCCESS) { reshade_log.errorf("vkAllocateMemory failed"); return false; } device->vk.BindBufferMemory(device->device(), scratchBuffer, scratchMemory, 0); if (result != VK_SUCCESS) { reshade_log.errorf("vkBindBufferMemory failed"); return false; } void *scratchPtr = nullptr; result = device->vk.MapMemory(device->device(), scratchMemory, 0, VK_WHOLE_SIZE, 0, &scratchPtr); if (result != VK_SUCCESS) { reshade_log.errorf("vkMapMemory failed"); return false; } memcpy(scratchPtr, pixels, size); m_cmdBuffer->reset(); m_cmdBuffer->begin(); m_cmdBuffer->copyBufferToImage(scratchBuffer, 0, 0, texture); device->submitInternal(&*m_cmdBuffer); device->waitIdle(false); free(data); device->vk.DestroyBuffer(device->device(), scratchBuffer, nullptr); device->vk.FreeMemory(device->device(), scratchMemory, nullptr); } } else if (texture) { m_cmdBuffer->reset(); m_cmdBuffer->begin(); VkClearColorValue clearColor{}; VkImageSubresourceRange range = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1, }; m_cmdBuffer->prepareDestImage(texture.get()); m_cmdBuffer->insertBarrier(); device->vk.CmdClearColorImage(m_cmdBuffer->rawBuffer(), texture->vkImage(), VK_IMAGE_LAYOUT_GENERAL, &clearColor, 1, &range); m_cmdBuffer->markDirty(texture.get()); device->submitInternal(&*m_cmdBuffer); device->waitIdle(false); } m_textures.emplace_back(std::move(texture)); } // Create Samplers { for (const auto& sampler : m_module->samplers) { gamescope::Rc tex; tex = findTexture(sampler.texture_name); if (!tex) { reshade_log.errorf("Couldn't find texture with name: %s", sampler.texture_name.c_str()); } VkFilter minFilter; VkFilter magFilter; VkSamplerMipmapMode mipmapMode; ConvertReshadeFilter(sampler.filter, minFilter, magFilter, mipmapMode); VkSamplerCreateInfo samplerCreateInfo; samplerCreateInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; samplerCreateInfo.pNext = nullptr; samplerCreateInfo.flags = 0; samplerCreateInfo.magFilter = magFilter; samplerCreateInfo.minFilter = minFilter; samplerCreateInfo.mipmapMode = mipmapMode; samplerCreateInfo.addressModeU = ConvertReshadeAddressMode(sampler.address_u); samplerCreateInfo.addressModeV = ConvertReshadeAddressMode(sampler.address_v); samplerCreateInfo.addressModeW = ConvertReshadeAddressMode(sampler.address_w); samplerCreateInfo.mipLodBias = sampler.lod_bias; samplerCreateInfo.anisotropyEnable = VK_FALSE; samplerCreateInfo.maxAnisotropy = 0; samplerCreateInfo.compareEnable = VK_FALSE; samplerCreateInfo.compareOp = VK_COMPARE_OP_ALWAYS; samplerCreateInfo.minLod = sampler.min_lod; samplerCreateInfo.maxLod = sampler.max_lod; samplerCreateInfo.borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; samplerCreateInfo.unnormalizedCoordinates = VK_FALSE; VkSampler vkSampler; VkResult result = device->vk.CreateSampler(device->device(), &samplerCreateInfo, nullptr, &vkSampler); if (result != VK_SUCCESS) { reshade_log.errorf("vkCreateSampler failed"); return false; } m_samplers.emplace_back(vkSampler, std::move(tex)); } } // Create Descriptor Set Layouts { VkDescriptorSetLayoutBinding layoutBinding; layoutBinding.binding = 0; layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; layoutBinding.descriptorCount = 1; layoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_COMPUTE_BIT; layoutBinding.pImmutableSamplers = nullptr; VkDescriptorSetLayoutCreateInfo layoutCreateInfo; layoutCreateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; layoutCreateInfo.pNext = nullptr; layoutCreateInfo.flags = 0; layoutCreateInfo.bindingCount = 1; layoutCreateInfo.pBindings = &layoutBinding; VkResult result = device->vk.CreateDescriptorSetLayout(device->device(), &layoutCreateInfo, nullptr, &m_descriptorSetLayouts[GAMESCOPE_RESHADE_DESCRIPTOR_SET_UBO]); if (result != VK_SUCCESS) { reshade_log.errorf("Failed to create descriptor set layout."); return false; } } { std::vector layoutBindings; for (uint32_t i = 0; i < m_module->samplers.size(); i++) { VkDescriptorSetLayoutBinding layoutBinding; layoutBinding.binding = i; layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; layoutBinding.descriptorCount = 1; layoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_COMPUTE_BIT; layoutBinding.pImmutableSamplers = nullptr; layoutBindings.push_back(layoutBinding); } VkDescriptorSetLayoutCreateInfo layoutCreateInfo; layoutCreateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; layoutCreateInfo.pNext = nullptr; layoutCreateInfo.flags = 0; layoutCreateInfo.bindingCount = layoutBindings.size(); layoutCreateInfo.pBindings = layoutBindings.data(); VkResult result = device->vk.CreateDescriptorSetLayout(device->device(), &layoutCreateInfo, nullptr, &m_descriptorSetLayouts[GAMESCOPE_RESHADE_DESCRIPTOR_SET_SAMPLED_IMAGES]); if (result != VK_SUCCESS) { reshade_log.errorf("Failed to create descriptor set layout."); return false; } } { std::vector layoutBindings; for (uint32_t i = 0; i < m_module->samplers.size(); i++) { VkDescriptorSetLayoutBinding layoutBinding; layoutBinding.binding = i; layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; layoutBinding.descriptorCount = 1; layoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_COMPUTE_BIT; layoutBinding.pImmutableSamplers = nullptr; layoutBindings.push_back(layoutBinding); } VkDescriptorSetLayoutCreateInfo layoutCreateInfo; layoutCreateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; layoutCreateInfo.pNext = nullptr; layoutCreateInfo.flags = 0; layoutCreateInfo.bindingCount = layoutBindings.size(); layoutCreateInfo.pBindings = layoutBindings.data(); VkResult result = device->vk.CreateDescriptorSetLayout(device->device(), &layoutCreateInfo, nullptr, &m_descriptorSetLayouts[GAMESCOPE_RESHADE_DESCRIPTOR_SET_STORAGE_IMAGES]); if (result != VK_SUCCESS) { reshade_log.errorf("Failed to create descriptor set layout."); return false; } } { VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo = { .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .pNext = nullptr, .flags = 0, .setLayoutCount = uint32_t(std::size(m_descriptorSetLayouts)), .pSetLayouts = m_descriptorSetLayouts, .pushConstantRangeCount = 0, .pPushConstantRanges = nullptr, }; VkResult result = device->vk.CreatePipelineLayout(device->device(), &pipelineLayoutCreateInfo, nullptr, &m_pipelineLayout); if (result != VK_SUCCESS) { reshade_log.errorf("Failed to create pipeline layout."); return false; } } { VkDescriptorPoolSize descriptorPoolSizes[] = { { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, uint32_t(GAMESCOPE_RESHADE_DESCRIPTOR_SET_COUNT * 1u) }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, uint32_t(GAMESCOPE_RESHADE_DESCRIPTOR_SET_COUNT * m_module->samplers.size()) }, { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, uint32_t(GAMESCOPE_RESHADE_DESCRIPTOR_SET_COUNT * m_module->storages.size()) }, }; VkDescriptorPoolCreateInfo descriptorPoolCreateInfo; descriptorPoolCreateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; descriptorPoolCreateInfo.pNext = nullptr; descriptorPoolCreateInfo.flags = 0; descriptorPoolCreateInfo.maxSets = GAMESCOPE_RESHADE_DESCRIPTOR_SET_COUNT; descriptorPoolCreateInfo.poolSizeCount = std::size(descriptorPoolSizes); descriptorPoolCreateInfo.pPoolSizes = descriptorPoolSizes; VkResult result = device->vk.CreateDescriptorPool(device->device(), &descriptorPoolCreateInfo, nullptr, &m_descriptorPool); if (result != VK_SUCCESS) { reshade_log.errorf("Failed to create descriptor pool."); return false; } } for (uint32_t i = 0; i < GAMESCOPE_RESHADE_DESCRIPTOR_SET_COUNT; i++) { VkDescriptorSetAllocateInfo descriptorSetAllocateInfo; descriptorSetAllocateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; descriptorSetAllocateInfo.pNext = nullptr; descriptorSetAllocateInfo.descriptorPool = m_descriptorPool; descriptorSetAllocateInfo.descriptorSetCount = 1; descriptorSetAllocateInfo.pSetLayouts = &m_descriptorSetLayouts[i]; VkResult result = device->vk.AllocateDescriptorSets(device->device(), &descriptorSetAllocateInfo, &m_descriptorSets[i]); if (result != VK_SUCCESS) { reshade_log.errorf("Failed to allocate descriptor set."); return false; } } // Create Pipelines for (const auto& pass : technique.passes) { reshade_log.infof("Compiling pass: %s", pass.name.c_str()); VkShaderModuleCreateInfo shaderModuleInfo = { .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = m_module->code.size(), .pCode = reinterpret_cast(m_module->code.data()), }; if (!pass.cs_entry_point.empty()) { VkPipelineShaderStageCreateInfo shaderStageCreateInfoCompute = { .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .pNext = &shaderModuleInfo, .stage = VK_SHADER_STAGE_COMPUTE_BIT, .pName = pass.cs_entry_point.c_str(), }; VkComputePipelineCreateInfo pipelineInfo = { .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, .stage = shaderStageCreateInfoCompute, .layout = m_pipelineLayout, }; VkPipeline pipeline = VK_NULL_HANDLE; VkResult result = device->vk.CreateComputePipelines(device->device(), VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline); if (result != VK_SUCCESS) { reshade_log.errorf("Failed to CreateComputePipelines"); return false; } m_pipelines.push_back(pipeline); } else { std::vector attachmentBlendStates; std::vector colorFormats; uint32_t maxRenderWidth = 0; uint32_t maxRenderHeight = 0; for (int i = 0; i < 8; i++) { gamescope::Rc rt; if (i == 0 && pass.render_target_names[0].empty()) rt = m_rt; else if (pass.render_target_names[i].empty()) break; else rt = findTexture(pass.render_target_names[i]); if (rt == nullptr) continue; maxRenderWidth = std::max(maxRenderWidth, rt->width()); maxRenderHeight = std::max(maxRenderHeight, rt->height()); colorFormats.push_back(rt->format()); VkPipelineColorBlendAttachmentState colorBlendAttachment; colorBlendAttachment.blendEnable = pass.blend_enable[i]; colorBlendAttachment.srcColorBlendFactor = ConvertReshadeBlendFactor(pass.src_blend[i]); colorBlendAttachment.dstColorBlendFactor = ConvertReshadeBlendFactor(pass.dest_blend[i]); colorBlendAttachment.colorBlendOp = ConvertReshadeBlendOp(pass.blend_op[i]); colorBlendAttachment.srcAlphaBlendFactor = ConvertReshadeBlendFactor(pass.src_blend_alpha[i]); colorBlendAttachment.dstAlphaBlendFactor = ConvertReshadeBlendFactor(pass.dest_blend_alpha[i]); colorBlendAttachment.alphaBlendOp = ConvertReshadeBlendOp(pass.blend_op_alpha[i]); colorBlendAttachment.colorWriteMask = pass.color_write_mask[i]; attachmentBlendStates.push_back(colorBlendAttachment); } VkPipelineRenderingCreateInfo renderingCreateInfo; renderingCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO; renderingCreateInfo.pNext = nullptr; renderingCreateInfo.viewMask = 0; renderingCreateInfo.colorAttachmentCount = colorFormats.size(); renderingCreateInfo.pColorAttachmentFormats = colorFormats.data(); renderingCreateInfo.depthAttachmentFormat = VK_FORMAT_UNDEFINED; renderingCreateInfo.stencilAttachmentFormat = VK_FORMAT_UNDEFINED; VkRect2D scissor; scissor.offset = {0, 0}; scissor.extent.width = pass.viewport_width ? pass.viewport_width : maxRenderWidth; scissor.extent.height = pass.viewport_height ? pass.viewport_height : maxRenderHeight; VkViewport viewport; viewport.x = 0.0f; viewport.y = static_cast(scissor.extent.height); viewport.width = static_cast(scissor.extent.width); viewport.height = -static_cast(scissor.extent.height); viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; VkPipelineShaderStageCreateInfo shaderStageCreateInfoVert; shaderStageCreateInfoVert.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; shaderStageCreateInfoVert.pNext = &shaderModuleInfo; shaderStageCreateInfoVert.flags = 0; shaderStageCreateInfoVert.stage = VK_SHADER_STAGE_VERTEX_BIT; shaderStageCreateInfoVert.module = VK_NULL_HANDLE; shaderStageCreateInfoVert.pName = pass.vs_entry_point.c_str(); shaderStageCreateInfoVert.pSpecializationInfo = nullptr; VkPipelineShaderStageCreateInfo shaderStageCreateInfoFrag; shaderStageCreateInfoFrag.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; shaderStageCreateInfoFrag.pNext = &shaderModuleInfo; shaderStageCreateInfoFrag.flags = 0; shaderStageCreateInfoFrag.stage = VK_SHADER_STAGE_FRAGMENT_BIT; shaderStageCreateInfoFrag.module = VK_NULL_HANDLE; shaderStageCreateInfoFrag.pName = pass.ps_entry_point.c_str(); shaderStageCreateInfoFrag.pSpecializationInfo = nullptr; VkPipelineShaderStageCreateInfo shaderStages[] = {shaderStageCreateInfoVert, shaderStageCreateInfoFrag}; VkPipelineVertexInputStateCreateInfo vertexInputCreateInfo; vertexInputCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; vertexInputCreateInfo.pNext = nullptr; vertexInputCreateInfo.flags = 0; vertexInputCreateInfo.vertexBindingDescriptionCount = 0; vertexInputCreateInfo.pVertexBindingDescriptions = nullptr; vertexInputCreateInfo.vertexAttributeDescriptionCount = 0; vertexInputCreateInfo.pVertexAttributeDescriptions = nullptr; VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; switch (pass.topology) { case reshadefx::primitive_topology::point_list: topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; break; case reshadefx::primitive_topology::line_list: topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST; break; case reshadefx::primitive_topology::line_strip: topology = VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; break; case reshadefx::primitive_topology::triangle_list: topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; break; case reshadefx::primitive_topology::triangle_strip: topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; break; default: reshade_log.errorf("Unsupported primitive type: %d", (uint32_t) pass.topology); break; } VkPipelineInputAssemblyStateCreateInfo inputAssemblyCreateInfo; inputAssemblyCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; inputAssemblyCreateInfo.pNext = nullptr; inputAssemblyCreateInfo.flags = 0; inputAssemblyCreateInfo.topology = topology; inputAssemblyCreateInfo.primitiveRestartEnable = VK_FALSE; VkPipelineViewportStateCreateInfo viewportStateCreateInfo; viewportStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; viewportStateCreateInfo.pNext = nullptr; viewportStateCreateInfo.flags = 0; viewportStateCreateInfo.viewportCount = 1; viewportStateCreateInfo.pViewports = &viewport; viewportStateCreateInfo.scissorCount = 1; viewportStateCreateInfo.pScissors = &scissor; VkPipelineRasterizationStateCreateInfo rasterizationCreateInfo; rasterizationCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; rasterizationCreateInfo.pNext = nullptr; rasterizationCreateInfo.flags = 0; rasterizationCreateInfo.depthClampEnable = VK_FALSE; rasterizationCreateInfo.rasterizerDiscardEnable = VK_FALSE; rasterizationCreateInfo.polygonMode = VK_POLYGON_MODE_FILL; rasterizationCreateInfo.cullMode = VK_CULL_MODE_NONE; rasterizationCreateInfo.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rasterizationCreateInfo.depthBiasEnable = VK_FALSE; rasterizationCreateInfo.depthBiasConstantFactor = 0.0f; rasterizationCreateInfo.depthBiasClamp = 0.0f; rasterizationCreateInfo.depthBiasSlopeFactor = 0.0f; rasterizationCreateInfo.lineWidth = 1.0f; VkPipelineMultisampleStateCreateInfo multisampleCreateInfo; multisampleCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; multisampleCreateInfo.pNext = nullptr; multisampleCreateInfo.flags = 0; multisampleCreateInfo.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; multisampleCreateInfo.sampleShadingEnable = VK_FALSE; multisampleCreateInfo.minSampleShading = 1.0f; multisampleCreateInfo.pSampleMask = nullptr; multisampleCreateInfo.alphaToCoverageEnable = VK_FALSE; multisampleCreateInfo.alphaToOneEnable = VK_FALSE; VkPipelineColorBlendStateCreateInfo colorBlendCreateInfo; colorBlendCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; colorBlendCreateInfo.pNext = nullptr; colorBlendCreateInfo.flags = 0; colorBlendCreateInfo.logicOpEnable = VK_FALSE; colorBlendCreateInfo.logicOp = VK_LOGIC_OP_NO_OP; colorBlendCreateInfo.attachmentCount = attachmentBlendStates.size(); colorBlendCreateInfo.pAttachments = attachmentBlendStates.data(); colorBlendCreateInfo.blendConstants[0] = 0.0f; colorBlendCreateInfo.blendConstants[1] = 0.0f; colorBlendCreateInfo.blendConstants[2] = 0.0f; colorBlendCreateInfo.blendConstants[3] = 0.0f; VkPipelineDynamicStateCreateInfo dynamicStateCreateInfo; dynamicStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; dynamicStateCreateInfo.pNext = nullptr; dynamicStateCreateInfo.flags = 0; dynamicStateCreateInfo.dynamicStateCount = 0; dynamicStateCreateInfo.pDynamicStates = nullptr; #if 0 VkPipelineDepthStencilStateCreateInfo depthStencilStateCreateInfo = {}; depthStencilStateCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; depthStencilStateCreateInfo.pNext = nullptr; depthStencilStateCreateInfo.depthTestEnable = VK_FALSE; depthStencilStateCreateInfo.depthWriteEnable = VK_FALSE; depthStencilStateCreateInfo.depthCompareOp = VK_COMPARE_OP_ALWAYS; depthStencilStateCreateInfo.depthBoundsTestEnable = VK_FALSE; depthStencilStateCreateInfo.stencilTestEnable = pass.stencil_enable; depthStencilStateCreateInfo.front.failOp = convertReshadeStencilOp(pass.stencil_op_fail); depthStencilStateCreateInfo.front.passOp = convertReshadeStencilOp(pass.stencil_op_pass); depthStencilStateCreateInfo.front.depthFailOp = convertReshadeStencilOp(pass.stencil_op_depth_fail); depthStencilStateCreateInfo.front.compareOp = convertReshadeCompareOp(pass.stencil_comparison_func); depthStencilStateCreateInfo.front.compareMask = pass.stencil_read_mask; depthStencilStateCreateInfo.front.writeMask = pass.stencil_write_mask; depthStencilStateCreateInfo.front.reference = pass.stencil_reference_value; depthStencilStateCreateInfo.back = depthStencilStateCreateInfo.front; depthStencilStateCreateInfo.minDepthBounds = 0.0f; depthStencilStateCreateInfo.maxDepthBounds = 1.0f; #endif VkGraphicsPipelineCreateInfo pipelineCreateInfo; pipelineCreateInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; pipelineCreateInfo.pNext = &renderingCreateInfo; pipelineCreateInfo.flags = 0; pipelineCreateInfo.stageCount = 2; pipelineCreateInfo.pStages = shaderStages; pipelineCreateInfo.pVertexInputState = &vertexInputCreateInfo; pipelineCreateInfo.pInputAssemblyState = &inputAssemblyCreateInfo; pipelineCreateInfo.pTessellationState = nullptr; pipelineCreateInfo.pViewportState = &viewportStateCreateInfo; pipelineCreateInfo.pRasterizationState = &rasterizationCreateInfo; pipelineCreateInfo.pMultisampleState = &multisampleCreateInfo; // pipelineCreateInfo.pDepthStencilState = &depthStencilStateCreateInfo; pipelineCreateInfo.pDepthStencilState = nullptr; pipelineCreateInfo.pColorBlendState = &colorBlendCreateInfo; pipelineCreateInfo.pDynamicState = &dynamicStateCreateInfo; pipelineCreateInfo.layout = m_pipelineLayout; pipelineCreateInfo.renderPass = VK_NULL_HANDLE; pipelineCreateInfo.subpass = 0; pipelineCreateInfo.basePipelineHandle = VK_NULL_HANDLE; pipelineCreateInfo.basePipelineIndex = -1; VkPipeline pipeline = VK_NULL_HANDLE; VkResult result = device->vk.CreateGraphicsPipelines(device->device(), VK_NULL_HANDLE, 1, &pipelineCreateInfo, nullptr, &pipeline); if (result != VK_SUCCESS) { reshade_log.errorf("Failed to vkCreateGraphicsPipelines"); return false; } m_pipelines.push_back(pipeline); } } return true; } void ReshadeEffectPipeline::update() { if (g_effectReadyCallback && g_reshadeEffectPath) { g_effectReadyCallback(g_reshadeEffectPath); g_effectReadyCallback = nullptr; } for (auto& uniform : m_uniforms) uniform->update(m_mappedPtr); } uint64_t ReshadeEffectPipeline::execute(gamescope::Rc inImage, gamescope::Rc *outImage) { CVulkanDevice *device = m_device; this->update(); // Update descriptor sets. { VkDescriptorBufferInfo bufferInfo = { .buffer = m_buffer, .range = VK_WHOLE_SIZE, }; VkWriteDescriptorSet writeDescriptorSet = { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = m_descriptorSets[GAMESCOPE_RESHADE_DESCRIPTOR_SET_UBO], .dstBinding = 0, .descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, .pBufferInfo = &bufferInfo, }; device->vk.UpdateDescriptorSets(device->device(), 1, &writeDescriptorSet, 0, nullptr); } //device->vk.UpdateDescriptorSets(cmd, ) for (size_t i = 0; i < m_samplers.size(); i++) { bool srgb = m_module->samplers[i].srgb; VkDescriptorImageInfo imageInfo = { .sampler = m_samplers[i].sampler, .imageView = m_samplers[i].texture ? m_samplers[i].texture->view(srgb) : inImage->view(srgb), .imageLayout = VK_IMAGE_LAYOUT_GENERAL, }; VkWriteDescriptorSet writeDescriptorSet = { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = m_descriptorSets[GAMESCOPE_RESHADE_DESCRIPTOR_SET_SAMPLED_IMAGES], .dstBinding = uint32_t(i), .descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .pImageInfo = &imageInfo, }; device->vk.UpdateDescriptorSets(device->device(), 1, &writeDescriptorSet, 0, nullptr); } for (size_t i = 0; i < m_module->storages.size(); i++) { // TODO: Cache auto tex = findTexture(m_module->storages[i].texture_name); VkDescriptorImageInfo imageInfo = { .imageView = tex ? tex->srgbView() : VK_NULL_HANDLE, .imageLayout = VK_IMAGE_LAYOUT_GENERAL, }; VkWriteDescriptorSet writeDescriptorSet = { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = m_descriptorSets[GAMESCOPE_RESHADE_DESCRIPTOR_SET_STORAGE_IMAGES], .dstBinding = uint32_t(i), .descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .pImageInfo = &imageInfo, }; device->vk.UpdateDescriptorSets(device->device(), 1, &writeDescriptorSet, 0, nullptr); } // Draw and compute time! m_cmdBuffer->reset(); m_cmdBuffer->begin(); VkCommandBuffer cmd = m_cmdBuffer->rawBuffer(); device->vk.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipelineLayout, 0, std::size(m_descriptorSets), m_descriptorSets, 0, nullptr); device->vk.CmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, m_pipelineLayout, 0, std::size(m_descriptorSets), m_descriptorSets, 0, nullptr); for (size_t i = 0; i < m_textures.size(); i++) { auto& tex = m_textures[i]; auto& texInfo = m_module->textures[i]; if (tex && (texInfo.storage_access || texInfo.render_target)) m_cmdBuffer->discardImage(tex.get()); } if (m_rt) m_cmdBuffer->discardImage(m_rt.get()); gamescope::Rc lastRT; auto& technique = m_module->techniques[m_key.techniqueIdx]; uint32_t passIdx = 0; for (auto& pass : technique.passes) { for (size_t i = 0; i < m_textures.size(); i++) { auto& tex = m_textures[i]; auto& texInfo = m_module->textures[i]; if (tex && texInfo.storage_access) m_cmdBuffer->prepareDestImage(tex.get()); else m_cmdBuffer->prepareSrcImage(tex != nullptr ? tex.get() : inImage.get()); } m_cmdBuffer->insertBarrier(); std::array, 8> rts{}; if (!pass.cs_entry_point.empty()) { device->vk.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, m_pipelines[passIdx]); device->vk.CmdDispatch(cmd, pass.viewport_width ? pass.viewport_width : m_key.bufferWidth, pass.viewport_height ? pass.viewport_height : m_key.bufferHeight, pass.viewport_dispatch_z); } else { for (int i = 0; i < 8; i++) { if (i == 0 && pass.render_target_names[0].empty()) rts[i] = m_rt; else if (pass.render_target_names[i].empty()) break; else rts[i] = findTexture(pass.render_target_names[i]); } for (int i = 0; i < 8; i++) { if (rts[i]) m_cmdBuffer->prepareDestImage(rts[i].get()); } device->vk.CmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipelines[passIdx]); std::vector colorAttachmentInfos; uint32_t maxRenderWidth = 0; uint32_t maxRenderHeight = 0; for (int i = 0; i < 8; i++) { if (rts[i]) { maxRenderWidth = std::max(maxRenderWidth, rts[i]->width()); maxRenderHeight = std::max(maxRenderHeight, rts[i]->height()); const VkRenderingAttachmentInfo colorAttachmentInfo { .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, .imageView = rts[i]->view(pass.srgb_write_enable), .imageLayout = VK_IMAGE_LAYOUT_GENERAL, .loadOp = pass.clear_render_targets ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD, .storeOp = VK_ATTACHMENT_STORE_OP_STORE, }; colorAttachmentInfos.push_back(colorAttachmentInfo); } } const VkRenderingInfo renderInfo { .sType = VK_STRUCTURE_TYPE_RENDERING_INFO, .renderArea = { { 0, 0, }, { maxRenderWidth, maxRenderHeight }}, .layerCount = 1, .colorAttachmentCount = uint32_t(colorAttachmentInfos.size()), .pColorAttachments = colorAttachmentInfos.data(), }; device->vk.CmdBeginRendering(cmd, &renderInfo); device->vk.CmdDraw(cmd, pass.num_vertices, 1, 0, 0); device->vk.CmdEndRendering(cmd); } for (int i = 0; i < 8; i++) { if (rts[i]) m_cmdBuffer->markDirty(rts[i].get()); } // Insert a stupidly huge fat barrier. VkMemoryBarrier memBarrier = {}; memBarrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER; memBarrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; memBarrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; device->vk.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memBarrier, 0, NULL, 0, NULL); if (rts[0]) lastRT = rts[0]; passIdx++; } if (lastRT) *outImage = lastRT; return device->submitInternal(&*m_cmdBuffer); } gamescope::Rc ReshadeEffectPipeline::findTexture(std::string_view name) { for (size_t i = 0; i < m_module->textures.size(); i++) { if (m_module->textures[i].unique_name == name) return m_textures[i]; } return nullptr; } //////////////////////////////// // ReshadeEffectManager //////////////////////////////// ReshadeEffectManager::ReshadeEffectManager() { } void ReshadeEffectManager::init(CVulkanDevice *device) { m_device = device; } void ReshadeEffectManager::clear() { m_lastKey = ReshadeEffectKey{}; m_lastPipeline = nullptr; } ReshadeEffectPipeline* ReshadeEffectManager::pipeline(const ReshadeEffectKey &key) { if (m_lastKey == key) return m_lastPipeline.get(); m_lastKey = key; m_lastPipeline = nullptr; auto pipeline = std::make_unique(); if (!pipeline->init(m_device, key)) return nullptr; m_lastPipeline = std::move(pipeline); return m_lastPipeline.get(); } ReshadeEffectManager g_reshadeManager; void reshade_effect_manager_set_uniform_variable(const char *key, uint8_t* value) { std::lock_guard lock(g_runtimeUniformsMutex); auto it = g_runtimeUniforms.find(key); if (it != g_runtimeUniforms.end()) { delete[] it->second; } g_runtimeUniforms[std::string(key)] = value; force_repaint(); } void reshade_effect_manager_set_effect(const char *path, std::function callback) { g_runtimeUniforms.clear(); if (g_reshadeEffectPath) free(g_reshadeEffectPath); g_reshadeEffectPath = strdup(path); g_effectReadyCallback = callback; } void reshade_effect_manager_enable_effect() { if (g_reshadeEffectPath) gamescope_set_reshade_effect(g_reshadeEffectPath); } void reshade_effect_manager_disable_effect() { gamescope_clear_reshade_effect(); }ValveSoftware-gamescope-eb620ab/src/reshade_effect_manager.hpp000066400000000000000000000057051502457270500246650ustar00rootroot00000000000000#pragma once #include "rendervulkan.hpp" #include namespace reshadefx { struct module; } class ReshadeUniform; struct ReshadeCombinedImageSampler { VkSampler sampler; gamescope::Rc texture; }; struct ReshadeEffectKey { std::string path; uint32_t bufferWidth; uint32_t bufferHeight; GamescopeAppTextureColorspace bufferColorSpace; VkFormat bufferFormat; uint32_t techniqueIdx; bool operator==(const ReshadeEffectKey& other) const = default; bool operator!=(const ReshadeEffectKey& other) const = default; }; enum ReshadeDescriptorSets { GAMESCOPE_RESHADE_DESCRIPTOR_SET_UBO = 0, GAMESCOPE_RESHADE_DESCRIPTOR_SET_SAMPLED_IMAGES, GAMESCOPE_RESHADE_DESCRIPTOR_SET_STORAGE_IMAGES, GAMESCOPE_RESHADE_DESCRIPTOR_SET_COUNT, }; namespace ReshadeEffectFlag { static constexpr uint32_t AlwaysScanout = 1u << 0; } using ReshadeEffectFlags = uint32_t; class ReshadeEffectPipeline { public: ReshadeEffectPipeline(); ~ReshadeEffectPipeline(); bool init(CVulkanDevice *device, const ReshadeEffectKey &key); void update(); uint64_t execute(gamescope::Rc inImage, gamescope::Rc *outImage); const ReshadeEffectKey& key() const { return m_key; } reshadefx::module *module() { return m_module.get(); } ReshadeEffectFlags flags() const { return m_flags; } gamescope::Rc findTexture(std::string_view name); private: ReshadeEffectKey m_key; CVulkanDevice *m_device; std::unique_ptr m_module; std::vector m_pipelines; std::vector> m_textures; gamescope::OwningRc m_rt; std::vector m_samplers; std::vector> m_uniforms; std::optional m_cmdBuffer = std::nullopt; VkBuffer m_buffer = VK_NULL_HANDLE; VkDeviceMemory m_bufferMemory = VK_NULL_HANDLE; void* m_mappedPtr = nullptr; VkPipelineLayout m_pipelineLayout = VK_NULL_HANDLE; VkDescriptorPool m_descriptorPool = VK_NULL_HANDLE; VkDescriptorSetLayout m_descriptorSetLayouts[GAMESCOPE_RESHADE_DESCRIPTOR_SET_COUNT] = {}; VkDescriptorSet m_descriptorSets[GAMESCOPE_RESHADE_DESCRIPTOR_SET_COUNT] = {}; ReshadeEffectFlags m_flags = 0; }; class ReshadeEffectManager { public: ReshadeEffectManager(); void init(CVulkanDevice *device); void clear(); ReshadeEffectPipeline* pipeline(const ReshadeEffectKey &key); private: ReshadeEffectKey m_lastKey{}; std::unique_ptr m_lastPipeline; CVulkanDevice *m_device; }; extern ReshadeEffectManager g_reshadeManager; void reshade_effect_manager_set_uniform_variable(const char *key, uint8_t* value); void reshade_effect_manager_set_effect(const char *path, std::function callback); void reshade_effect_manager_enable_effect(); void reshade_effect_manager_disable_effect();ValveSoftware-gamescope-eb620ab/src/sdlscancodetable.hpp000066400000000000000000000310601502457270500235270ustar00rootroot00000000000000 static const uint32_t s_ScancodeTable[] = { KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 0 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 1 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 2 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 3 */ KEY_A, /* SDL_SCANCODE_A 4 */ KEY_B, /* SDL_SCANCODE_B 5 */ KEY_C, /* SDL_SCANCODE_C 6 */ KEY_D, /* SDL_SCANCODE_D 7 */ KEY_E, /* SDL_SCANCODE_E 8 */ KEY_F, /* SDL_SCANCODE_F 9 */ KEY_G, /* SDL_SCANCODE_G 10 */ KEY_H, /* SDL_SCANCODE_H 11 */ KEY_I, /* SDL_SCANCODE_I 12 */ KEY_J, /* SDL_SCANCODE_J 13 */ KEY_K, /* SDL_SCANCODE_K 14 */ KEY_L, /* SDL_SCANCODE_L 15 */ KEY_M, /* SDL_SCANCODE_M 16 */ KEY_N, /* SDL_SCANCODE_N 17 */ KEY_O, /* SDL_SCANCODE_O 18 */ KEY_P, /* SDL_SCANCODE_P 19 */ KEY_Q, /* SDL_SCANCODE_Q 20 */ KEY_R, /* SDL_SCANCODE_R 21 */ KEY_S, /* SDL_SCANCODE_S 22 */ KEY_T, /* SDL_SCANCODE_T 23 */ KEY_U, /* SDL_SCANCODE_U 24 */ KEY_V, /* SDL_SCANCODE_V 25 */ KEY_W, /* SDL_SCANCODE_W 26 */ KEY_X, /* SDL_SCANCODE_X 27 */ KEY_Y, /* SDL_SCANCODE_Y 28 */ KEY_Z, /* SDL_SCANCODE_Z 29 */ KEY_1, /* SDL_SCANCODE_1 30 */ KEY_2, /* SDL_SCANCODE_2 31 */ KEY_3, /* SDL_SCANCODE_3 32 */ KEY_4, /* SDL_SCANCODE_4 33 */ KEY_5, /* SDL_SCANCODE_5 34 */ KEY_6, /* SDL_SCANCODE_6 35 */ KEY_7, /* SDL_SCANCODE_7 36 */ KEY_8, /* SDL_SCANCODE_8 37 */ KEY_9, /* SDL_SCANCODE_9 38 */ KEY_0, /* SDL_SCANCODE_0 39 */ KEY_ENTER, /* SDL_SCANCODE_RETURN 40 */ KEY_ESC, /* SDL_SCANCODE_ESCAPE 41 */ KEY_BACKSPACE, /* SDL_SCANCODE_BACKSPACE 42 */ KEY_TAB, /* SDL_SCANCODE_TAB 43 */ KEY_SPACE, /* SDL_SCANCODE_SPACE 44 */ KEY_MINUS, /* SDL_SCANCODE_MINUS 45 */ KEY_EQUAL, /* SDL_SCANCODE_EQUALS 46 */ KEY_LEFTBRACE, /* SDL_SCANCODE_LEFTBRACKET 47 */ KEY_RIGHTBRACE, /* SDL_SCANCODE_RIGHTBRACKET 48 */ KEY_BACKSLASH, /* SDL_SCANCODE_BACKSLASH 49 */ KEY_RESERVED, /* SDL_SCANCODE_NONUSHASH 50 */ KEY_SEMICOLON, /* SDL_SCANCODE_SEMICOLON 51 */ KEY_APOSTROPHE, /* SDL_SCANCODE_APOSTROPHE 52 */ KEY_GRAVE, /* SDL_SCANCODE_GRAVE 53 */ KEY_COMMA, /* SDL_SCANCODE_COMMA 54 */ KEY_DOT, /* SDL_SCANCODE_PERIOD 55 */ KEY_SLASH, /* SDL_SCANCODE_SLASH 56 */ KEY_CAPSLOCK, /* SDL_SCANCODE_CAPSLOCK 57 */ KEY_F1, /* SDL_SCANCODE_F1 58 */ KEY_F2, /* SDL_SCANCODE_F2 59 */ KEY_F3, /* SDL_SCANCODE_F3 60 */ KEY_F4, /* SDL_SCANCODE_F4 61 */ KEY_F5, /* SDL_SCANCODE_F5 62 */ KEY_F6, /* SDL_SCANCODE_F6 63 */ KEY_F7, /* SDL_SCANCODE_F7 64 */ KEY_F8, /* SDL_SCANCODE_F8 65 */ KEY_F9, /* SDL_SCANCODE_F9 66 */ KEY_F10, /* SDL_SCANCODE_F10 67 */ KEY_F11, /* SDL_SCANCODE_F11 68 */ KEY_F12, /* SDL_SCANCODE_F12 69 */ KEY_RESERVED, /* SDL_SCANCODE_PRINTSCREEN 70 */ KEY_SCROLLLOCK, /* SDL_SCANCODE_SCROLLLOCK 71 */ KEY_PAUSE, /* SDL_SCANCODE_PAUSE 72 */ KEY_INSERT, /* SDL_SCANCODE_INSERT 73 */ KEY_HOME, /* SDL_SCANCODE_HOME 74 */ KEY_PAGEUP, /* SDL_SCANCODE_PAGEUP 75 */ KEY_DELETE, /* SDL_SCANCODE_DELETE 76 */ KEY_END, /* SDL_SCANCODE_END 77 */ KEY_PAGEDOWN, /* SDL_SCANCODE_PAGEDOWN 78 */ KEY_RIGHT, /* SDL_SCANCODE_RIGHT 79 */ KEY_LEFT, /* SDL_SCANCODE_LEFT 80 */ KEY_DOWN, /* SDL_SCANCODE_DOWN 81 */ KEY_UP, /* SDL_SCANCODE_UP 82 */ KEY_NUMLOCK, /* SDL_SCANCODE_NUMLOCKCLEAR 83 */ KEY_KPSLASH, /* SDL_SCANCODE_KP_DIVIDE 84 */ KEY_KPASTERISK, /* SDL_SCANCODE_KP_MULTIPLY 85 */ KEY_KPMINUS, /* SDL_SCANCODE_KP_MINUS 86 */ KEY_KPPLUS, /* SDL_SCANCODE_KP_PLUS 87 */ KEY_KPENTER, /* SDL_SCANCODE_KP_ENTER 88 */ KEY_KP1, /* SDL_SCANCODE_KP_1 89 */ KEY_KP2, /* SDL_SCANCODE_KP_2 90 */ KEY_KP3, /* SDL_SCANCODE_KP_3 91 */ KEY_KP4, /* SDL_SCANCODE_KP_4 92 */ KEY_KP5, /* SDL_SCANCODE_KP_5 93 */ KEY_KP6, /* SDL_SCANCODE_KP_6 94 */ KEY_KP7, /* SDL_SCANCODE_KP_7 95 */ KEY_KP8, /* SDL_SCANCODE_KP_8 96 */ KEY_KP9, /* SDL_SCANCODE_KP_9 97 */ KEY_KP0, /* SDL_SCANCODE_KP_0 98 */ KEY_KPDOT, /* SDL_SCANCODE_KP_PERIOD 99 */ KEY_102ND, /* SDL_SCANCODE_NONUSBACKSLASH 100 */ KEY_COMPOSE, /* SDL_SCANCODE_APPLICATION 101 */ KEY_POWER, /* SDL_SCANCODE_POWER 102 */ KEY_KPEQUAL, /* SDL_SCANCODE_KP_EQUALS 103 */ KEY_F13, /* SDL_SCANCODE_F13 104 */ KEY_F14, /* SDL_SCANCODE_F14 105 */ KEY_F15, /* SDL_SCANCODE_F15 106 */ KEY_F16, /* SDL_SCANCODE_F16 107 */ KEY_F17, /* SDL_SCANCODE_F17 108 */ KEY_F18, /* SDL_SCANCODE_F18 109 */ KEY_F19, /* SDL_SCANCODE_F19 110 */ KEY_F20, /* SDL_SCANCODE_F20 111 */ KEY_F21, /* SDL_SCANCODE_F21 112 */ KEY_F22, /* SDL_SCANCODE_F22 113 */ KEY_F23, /* SDL_SCANCODE_F23 114 */ KEY_F24, /* SDL_SCANCODE_F24 115 */ KEY_RESERVED, /* SDL_SCANCODE_EXECUTE 116 */ KEY_HELP, /* SDL_SCANCODE_HELP 117 */ KEY_MENU, /* SDL_SCANCODE_MENU 118 */ KEY_RESERVED, /* SDL_SCANCODE_SELECT 119 */ KEY_STOP, /* SDL_SCANCODE_STOP 120 */ KEY_AGAIN, /* SDL_SCANCODE_AGAIN 121 */ KEY_UNDO, /* SDL_SCANCODE_UNDO 122 */ KEY_CUT, /* SDL_SCANCODE_CUT 123 */ KEY_COPY, /* SDL_SCANCODE_COPY 124 */ KEY_PASTE, /* SDL_SCANCODE_PASTE 125 */ KEY_FIND, /* SDL_SCANCODE_FIND 126 */ KEY_MUTE, /* SDL_SCANCODE_MUTE 127 */ KEY_VOLUMEUP, /* SDL_SCANCODE_VOLUMEUP 128 */ KEY_VOLUMEDOWN, /* SDL_SCANCODE_VOLUMEDOWN 129 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 130 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 131 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 132 */ KEY_KPJPCOMMA, /* SDL_SCANCODE_KP_COMMA 133 */ KEY_RESERVED, /* SDL_SCANCODE_KP_EQUALSAS400 134 */ KEY_RESERVED, /* SDL_SCANCODE_INTERNATIONAL1 135 */ KEY_RESERVED, /* SDL_SCANCODE_INTERNATIONAL2 136 */ KEY_YEN, /* SDL_SCANCODE_INTERNATIONAL3 137 */ KEY_RESERVED, /* SDL_SCANCODE_INTERNATIONAL4 138 */ KEY_RESERVED, /* SDL_SCANCODE_INTERNATIONAL5 139 */ KEY_RESERVED, /* SDL_SCANCODE_INTERNATIONAL6 140 */ KEY_RESERVED, /* SDL_SCANCODE_INTERNATIONAL7 141 */ KEY_RESERVED, /* SDL_SCANCODE_INTERNATIONAL8 142 */ KEY_RESERVED, /* SDL_SCANCODE_INTERNATIONAL9 143 */ KEY_HANGEUL, /* SDL_SCANCODE_LANG1 144 */ KEY_HANJA, /* SDL_SCANCODE_LANG2 145 */ KEY_KATAKANA, /* SDL_SCANCODE_LANG3 146 */ KEY_HIRAGANA, /* SDL_SCANCODE_LANG4 147 */ KEY_ZENKAKUHANKAKU, /* SDL_SCANCODE_LANG5 148 */ KEY_RESERVED, /* SDL_SCANCODE_LANG6 149 */ KEY_RESERVED, /* SDL_SCANCODE_LANG7 150 */ KEY_RESERVED, /* SDL_SCANCODE_LANG8 151 */ KEY_RESERVED, /* SDL_SCANCODE_LANG9 152 */ KEY_RESERVED, /* SDL_SCANCODE_ALTERASE 153 */ KEY_SYSRQ, /* SDL_SCANCODE_SYSREQ 154 */ KEY_RESERVED, /* SDL_SCANCODE_CANCEL 155 */ KEY_RESERVED, /* SDL_SCANCODE_CLEAR 156 */ KEY_RESERVED, /* SDL_SCANCODE_PRIOR 157 */ KEY_RESERVED, /* SDL_SCANCODE_RETURN2 158 */ KEY_RESERVED, /* SDL_SCANCODE_SEPARATOR 159 */ KEY_RESERVED, /* SDL_SCANCODE_OUT 160 */ KEY_RESERVED, /* SDL_SCANCODE_OPER 161 */ KEY_RESERVED, /* SDL_SCANCODE_CLEARAGAIN 162 */ KEY_RESERVED, /* SDL_SCANCODE_CRSEL 163 */ KEY_RESERVED, /* SDL_SCANCODE_EXSEL 164 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 165 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 166 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 167 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 168 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 169 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 170 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 171 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 172 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 173 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 174 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 175 */ KEY_RESERVED, /* SDL_SCANCODE_KP_00 176 */ KEY_RESERVED, /* SDL_SCANCODE_KP_000 177 */ KEY_RESERVED, /* SDL_SCANCODE_THOUSANDSSEPARATOR 178 */ KEY_RESERVED, /* SDL_SCANCODE_DECIMALSEPARATOR 179 */ KEY_RESERVED, /* SDL_SCANCODE_CURRENCYUNIT 180 */ KEY_RESERVED, /* SDL_SCANCODE_CURRENCYSUBUNIT 181 */ KEY_KPLEFTPAREN, /* SDL_SCANCODE_KP_LEFTPAREN 182 */ KEY_KPRIGHTPAREN, /* SDL_SCANCODE_KP_RIGHTPAREN 183 */ KEY_RESERVED, /* SDL_SCANCODE_KP_LEFTBRACE 184 */ KEY_RESERVED, /* SDL_SCANCODE_KP_RIGHTBRACE 185 */ KEY_RESERVED, /* SDL_SCANCODE_KP_TAB 186 */ KEY_RESERVED, /* SDL_SCANCODE_KP_BACKSPACE 187 */ KEY_RESERVED, /* SDL_SCANCODE_KP_A 188 */ KEY_RESERVED, /* SDL_SCANCODE_KP_B 189 */ KEY_RESERVED, /* SDL_SCANCODE_KP_C 190 */ KEY_RESERVED, /* SDL_SCANCODE_KP_D 191 */ KEY_RESERVED, /* SDL_SCANCODE_KP_E 192 */ KEY_RESERVED, /* SDL_SCANCODE_KP_F 193 */ KEY_RESERVED, /* SDL_SCANCODE_KP_XOR 194 */ KEY_RESERVED, /* SDL_SCANCODE_KP_POWER 195 */ KEY_RESERVED, /* SDL_SCANCODE_KP_PERCENT 196 */ KEY_RESERVED, /* SDL_SCANCODE_KP_LESS 197 */ KEY_RESERVED, /* SDL_SCANCODE_KP_GREATER 198 */ KEY_RESERVED, /* SDL_SCANCODE_KP_AMPERSAND 199 */ KEY_RESERVED, /* SDL_SCANCODE_KP_DBLAMPERSAND 200 */ KEY_RESERVED, /* SDL_SCANCODE_KP_VERTICALBAR 201 */ KEY_RESERVED, /* SDL_SCANCODE_KP_DBLVERTICALBAR 202 */ KEY_RESERVED, /* SDL_SCANCODE_KP_COLON 203 */ KEY_RESERVED, /* SDL_SCANCODE_KP_HASH 204 */ KEY_RESERVED, /* SDL_SCANCODE_KP_SPACE 205 */ KEY_RESERVED, /* SDL_SCANCODE_KP_AT 206 */ KEY_RESERVED, /* SDL_SCANCODE_KP_EXCLAM 207 */ KEY_RESERVED, /* SDL_SCANCODE_KP_MEMSTORE 208 */ KEY_RESERVED, /* SDL_SCANCODE_KP_MEMRECALL 209 */ KEY_RESERVED, /* SDL_SCANCODE_KP_MEMCLEAR 210 */ KEY_RESERVED, /* SDL_SCANCODE_KP_MEMADD 211 */ KEY_RESERVED, /* SDL_SCANCODE_KP_MEMSUBTRACT 212 */ KEY_RESERVED, /* SDL_SCANCODE_KP_MEMMULTIPLY 213 */ KEY_RESERVED, /* SDL_SCANCODE_KP_MEMDIVIDE 214 */ KEY_RESERVED, /* SDL_SCANCODE_KP_PLUSMINUS 215 */ KEY_RESERVED, /* SDL_SCANCODE_KP_CLEAR 216 */ KEY_RESERVED, /* SDL_SCANCODE_KP_CLEARENTRY 217 */ KEY_RESERVED, /* SDL_SCANCODE_KP_BINARY 218 */ KEY_RESERVED, /* SDL_SCANCODE_KP_OCTAL 219 */ KEY_RESERVED, /* SDL_SCANCODE_KP_DECIMAL 220 */ KEY_RESERVED, /* SDL_SCANCODE_KP_HEXADECIMAL 221 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 222 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 223 */ KEY_LEFTCTRL, /* SDL_SCANCODE_LCTRL 224 */ KEY_LEFTSHIFT, /* SDL_SCANCODE_LSHIFT 225 */ KEY_LEFTALT, /* SDL_SCANCODE_LALT 226 */ KEY_LEFTMETA, /* SDL_SCANCODE_LGUI 227 */ KEY_RIGHTCTRL, /* SDL_SCANCODE_RCTRL 228 */ KEY_RIGHTSHIFT, /* SDL_SCANCODE_RSHIFT 229 */ KEY_RIGHTALT, /* SDL_SCANCODE_RALT 230 */ KEY_RIGHTMETA, /* SDL_SCANCODE_RGUI 231 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 232 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 233 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 234 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 235 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 236 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 237 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 238 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 239 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 240 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 241 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 242 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 243 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 244 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 245 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 246 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 247 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 248 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 249 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 250 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 251 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 252 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 253 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 254 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 255 */ KEY_RESERVED, /* SDL_SCANCODE_UNKNOWN 256 */ KEY_RESERVED, /* SDL_SCANCODE_MODE 257 */ KEY_NEXTSONG, /* SDL_SCANCODE_AUDIONEXT 258 */ KEY_PREVIOUSSONG, /* SDL_SCANCODE_AUDIOPREV 259 */ KEY_STOPCD, /* SDL_SCANCODE_AUDIOSTOP 260 */ KEY_PLAYPAUSE, /* SDL_SCANCODE_AUDIOPLAY 261 */ KEY_RESERVED, /* SDL_SCANCODE_AUDIOMUTE 262 */ KEY_RESERVED, /* SDL_SCANCODE_MEDIASELECT 263 */ KEY_WWW, /* SDL_SCANCODE_WWW 264 */ KEY_MAIL, /* SDL_SCANCODE_MAIL 265 */ KEY_CALC, /* SDL_SCANCODE_CALCULATOR 266 */ KEY_COMPUTER, /* SDL_SCANCODE_COMPUTER 267 */ KEY_RESERVED, /* SDL_SCANCODE_AC_SEARCH 268 */ KEY_HOMEPAGE, /* SDL_SCANCODE_AC_HOME 269 */ KEY_BACK, /* SDL_SCANCODE_AC_BACK 270 */ KEY_FORWARD, /* SDL_SCANCODE_AC_FORWARD 271 */ KEY_RESERVED, /* SDL_SCANCODE_AC_STOP 272 */ KEY_REFRESH, /* SDL_SCANCODE_AC_REFRESH 273 */ KEY_BOOKMARKS, /* SDL_SCANCODE_AC_BOOKMARKS 274 */ KEY_RESERVED, /* SDL_SCANCODE_BRIGHTNESSDOWN 275 */ KEY_RESERVED, /* SDL_SCANCODE_BRIGHTNESSUP 276 */ KEY_RESERVED, /* SDL_SCANCODE_DISPLAYSWITCH 277 */ KEY_RESERVED, /* SDL_SCANCODE_KBDILLUMTOGGLE 278 */ KEY_RESERVED, /* SDL_SCANCODE_KBDILLUMDOWN 279 */ KEY_RESERVED, /* SDL_SCANCODE_KBDILLUMUP 280 */ KEY_EJECTCD, /* SDL_SCANCODE_EJECT 281 */ KEY_SLEEP, /* SDL_SCANCODE_SLEEP 282 */ KEY_PROG1, /* SDL_SCANCODE_APP1 283 */ KEY_RESERVED, /* SDL_SCANCODE_APP2 284 */ }; inline uint32_t SDLScancodeToLinuxKey( uint32_t nScancode ) { if ( nScancode < sizeof( s_ScancodeTable ) / sizeof( s_ScancodeTable[0] ) ) { return s_ScancodeTable[ nScancode ]; } return KEY_RESERVED; } inline int SDLButtonToLinuxButton( int SDLButton ) { switch ( SDLButton ) { case SDL_BUTTON_LEFT: return BTN_LEFT; case SDL_BUTTON_MIDDLE: return BTN_MIDDLE; case SDL_BUTTON_RIGHT: return BTN_RIGHT; case SDL_BUTTON_X1: return BTN_SIDE; case SDL_BUTTON_X2: return BTN_EXTRA; default: return 0; } } ValveSoftware-gamescope-eb620ab/src/shaders/000077500000000000000000000000001502457270500211555ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/src/shaders/NVIDIAImageScaling/000077500000000000000000000000001502457270500243735ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/src/shaders/NVIDIAImageScaling/NIS/000077500000000000000000000000001502457270500250245ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/src/shaders/NVIDIAImageScaling/NIS/NIS_Config.h000066400000000000000000000653071502457270500271260ustar00rootroot00000000000000// The MIT License(MIT) // // Copyright(c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files(the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions : // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. //--------------------------------------------------------------------------------- // NVIDIA Image Scaling SDK - v1.0.3 //--------------------------------------------------------------------------------- // Configuration //--------------------------------------------------------------------------------- #pragma once #include #include #include #ifndef NIS_ALIGNED #if defined(_MSC_VER) #define NIS_ALIGNED(x) __declspec(align(x)) #else #if defined(__GNUC__) #define NIS_ALIGNED(x) __attribute__ ((aligned(x))) #endif #endif #endif struct NIS_ALIGNED(256) NISConfig { float kDetectRatio; float kDetectThres; float kMinContrastRatio; float kRatioNorm; float kContrastBoost; float kEps; float kSharpStartY; float kSharpScaleY; float kSharpStrengthMin; float kSharpStrengthScale; float kSharpLimitMin; float kSharpLimitScale; float kScaleX; float kScaleY; float kDstNormX; float kDstNormY; float kSrcNormX; float kSrcNormY; uint32_t kInputViewportOriginX; uint32_t kInputViewportOriginY; uint32_t kInputViewportWidth; uint32_t kInputViewportHeight; uint32_t kOutputViewportOriginX; uint32_t kOutputViewportOriginY; uint32_t kOutputViewportWidth; uint32_t kOutputViewportHeight; float reserved0; float reserved1; }; enum class NISHDRMode : uint32_t { None = 0, Linear = 1, PQ = 2 }; enum class NISGPUArchitecture : uint32_t { NVIDIA_Generic = 0, AMD_Generic = 1, Intel_Generic = 2, NVIDIA_Generic_fp16 = 3 }; struct NISOptimizer { bool isUpscaling; NISGPUArchitecture gpuArch; constexpr NISOptimizer(bool isUpscaling = true, NISGPUArchitecture gpuArch = NISGPUArchitecture::NVIDIA_Generic) : isUpscaling(isUpscaling) , gpuArch(gpuArch) {} constexpr uint32_t GetOptimalBlockWidth() { switch (gpuArch) { case NISGPUArchitecture::NVIDIA_Generic: return 32; case NISGPUArchitecture::NVIDIA_Generic_fp16: return 32; case NISGPUArchitecture::AMD_Generic: return 32; case NISGPUArchitecture::Intel_Generic: return 32; } return 32; } constexpr uint32_t GetOptimalBlockHeight() { switch (gpuArch) { case NISGPUArchitecture::NVIDIA_Generic: return isUpscaling ? 24 : 32; case NISGPUArchitecture::NVIDIA_Generic_fp16: return isUpscaling ? 32 : 32; case NISGPUArchitecture::AMD_Generic: return isUpscaling ? 24 : 32; case NISGPUArchitecture::Intel_Generic: return isUpscaling ? 24 : 32; } return isUpscaling ? 24 : 32; } constexpr uint32_t GetOptimalThreadGroupSize() { switch (gpuArch) { case NISGPUArchitecture::NVIDIA_Generic: return 128; case NISGPUArchitecture::NVIDIA_Generic_fp16: return 128; case NISGPUArchitecture::AMD_Generic: return 256; case NISGPUArchitecture::Intel_Generic: return 256; } return 256; } }; inline bool NVScalerUpdateConfig(NISConfig& config, float sharpness, uint32_t inputViewportOriginX, uint32_t inputViewportOriginY, uint32_t inputViewportWidth, uint32_t inputViewportHeight, uint32_t inputTextureWidth, uint32_t inputTextureHeight, uint32_t outputViewportOriginX, uint32_t outputViewportOriginY, uint32_t outputViewportWidth, uint32_t outputViewportHeight, uint32_t outputTextureWidth, uint32_t outputTextureHeight, NISHDRMode hdrMode = NISHDRMode::None) { // adjust params based on value from sharpness slider sharpness = std::max(std::min(1.f, sharpness), 0.f); float sharpen_slider = sharpness - 0.5f; // Map 0 to 1 to -0.5 to +0.5 // Different range for 0 to 50% vs 50% to 100% // The idea is to make sure sharpness of 0% map to no-sharpening, // while also ensuring that sharpness of 100% doesn't cause too much over-sharpening. const float MaxScale = (sharpen_slider >= 0.0f) ? 1.25f : 1.75f; const float MinScale = (sharpen_slider >= 0.0f) ? 1.25f : 1.0f; const float LimitScale = (sharpen_slider >= 0.0f) ? 1.25f : 1.0f; float kDetectRatio = 2 * 1127.f / 1024.f; // Params for SDR float kDetectThres = 64.0f / 1024.0f; float kMinContrastRatio = 2.0f; float kMaxContrastRatio = 10.0f; float kSharpStartY = 0.45f; float kSharpEndY = 0.9f; float kSharpStrengthMin = std::max(0.0f, 0.4f + sharpen_slider * MinScale * 1.2f); float kSharpStrengthMax = 1.6f + sharpen_slider * MaxScale * 1.8f; float kSharpLimitMin = std::max(0.1f, 0.14f + sharpen_slider * LimitScale * 0.32f); float kSharpLimitMax = 0.5f + sharpen_slider * LimitScale * 0.6f; if (hdrMode == NISHDRMode::Linear || hdrMode == NISHDRMode::PQ) { kDetectThres = 32.0f / 1024.0f; kMinContrastRatio = 1.5f; kMaxContrastRatio = 5.0f; kSharpStrengthMin = std::max(0.0f, 0.4f + sharpen_slider * MinScale * 1.1f); kSharpStrengthMax = 2.2f + sharpen_slider * MaxScale * 1.8f; kSharpLimitMin = std::max(0.06f, 0.10f + sharpen_slider * LimitScale * 0.28f); kSharpLimitMax = 0.6f + sharpen_slider * LimitScale * 0.6f; if (hdrMode == NISHDRMode::PQ) { kSharpStartY = 0.35f; kSharpEndY = 0.55f; } else { kSharpStartY = 0.3f; kSharpEndY = 0.5f; } } float kRatioNorm = 1.0f / (kMaxContrastRatio - kMinContrastRatio); float kSharpScaleY = 1.0f / (kSharpEndY - kSharpStartY); float kSharpStrengthScale = kSharpStrengthMax - kSharpStrengthMin; float kSharpLimitScale = kSharpLimitMax - kSharpLimitMin; config.kInputViewportWidth = inputViewportWidth == 0 ? inputTextureWidth : inputViewportWidth; config.kInputViewportHeight = inputViewportHeight == 0 ? inputTextureHeight : inputViewportHeight; config.kOutputViewportWidth = outputViewportWidth == 0 ? outputTextureWidth : outputViewportWidth; config.kOutputViewportHeight = outputViewportHeight == 0 ? outputTextureHeight : outputViewportHeight; if (config.kInputViewportWidth == 0 || config.kInputViewportHeight == 0 || config.kOutputViewportWidth == 0 || config.kOutputViewportHeight == 0) return false; config.kInputViewportOriginX = inputViewportOriginX; config.kInputViewportOriginY = inputViewportOriginY; config.kOutputViewportOriginX = outputViewportOriginX; config.kOutputViewportOriginY = outputViewportOriginY; config.kSrcNormX = 1.f / inputTextureWidth; config.kSrcNormY = 1.f / inputTextureHeight; config.kDstNormX = 1.f / outputTextureWidth; config.kDstNormY = 1.f / outputTextureHeight; config.kScaleX = config.kInputViewportWidth / float(config.kOutputViewportWidth); config.kScaleY = config.kInputViewportHeight / float(config.kOutputViewportHeight); config.kDetectRatio = kDetectRatio; config.kDetectThres = kDetectThres; config.kMinContrastRatio = kMinContrastRatio; config.kRatioNorm = kRatioNorm; config.kContrastBoost = 1.0f; config.kEps = 1.0f / 255.0f; config.kSharpStartY = kSharpStartY; config.kSharpScaleY = kSharpScaleY; config.kSharpStrengthMin = kSharpStrengthMin; config.kSharpStrengthScale = kSharpStrengthScale; config.kSharpLimitMin = kSharpLimitMin; config.kSharpLimitScale = kSharpLimitScale; if (config.kScaleX < 0.5f || config.kScaleX > 1.f || config.kScaleY < 0.5f || config.kScaleY > 1.f) return false; return true; } inline bool NVSharpenUpdateConfig(NISConfig& config, float sharpness, uint32_t inputViewportOriginX, uint32_t inputViewportOriginY, uint32_t inputViewportWidth, uint32_t inputViewportHeight, uint32_t inputTextureWidth, uint32_t inputTextureHeight, uint32_t outputViewportOriginX, uint32_t outputViewportOriginY, NISHDRMode hdrMode = NISHDRMode::None) { return NVScalerUpdateConfig(config, sharpness, inputViewportOriginX, inputViewportOriginY, inputViewportWidth, inputViewportHeight, inputTextureWidth, inputTextureHeight, outputViewportOriginX, outputViewportOriginY, inputViewportWidth, inputViewportHeight, inputTextureWidth, inputTextureHeight, hdrMode); } namespace { constexpr size_t kPhaseCount = 64; constexpr size_t kFilterSize = 8; constexpr float coef_scale[kPhaseCount][kFilterSize] = { {0.0f, 0.0f, 1.0000f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, {0.0029f, -0.0127f, 1.0000f, 0.0132f, -0.0034f, 0.0f, 0.0f, 0.0f}, {0.0063f, -0.0249f, 0.9985f, 0.0269f, -0.0068f, 0.0f, 0.0f, 0.0f}, {0.0088f, -0.0361f, 0.9956f, 0.0415f, -0.0103f, 0.0005f, 0.0f, 0.0f}, {0.0117f, -0.0474f, 0.9932f, 0.0562f, -0.0142f, 0.0005f, 0.0f, 0.0f}, {0.0142f, -0.0576f, 0.9897f, 0.0713f, -0.0181f, 0.0005f, 0.0f, 0.0f}, {0.0166f, -0.0674f, 0.9844f, 0.0874f, -0.0220f, 0.0010f, 0.0f, 0.0f}, {0.0186f, -0.0762f, 0.9785f, 0.1040f, -0.0264f, 0.0015f, 0.0f, 0.0f}, {0.0205f, -0.0850f, 0.9727f, 0.1206f, -0.0308f, 0.0020f, 0.0f, 0.0f}, {0.0225f, -0.0928f, 0.9648f, 0.1382f, -0.0352f, 0.0024f, 0.0f, 0.0f}, {0.0239f, -0.1006f, 0.9575f, 0.1558f, -0.0396f, 0.0029f, 0.0f, 0.0f}, {0.0254f, -0.1074f, 0.9487f, 0.1738f, -0.0439f, 0.0034f, 0.0f, 0.0f}, {0.0264f, -0.1138f, 0.9390f, 0.1929f, -0.0488f, 0.0044f, 0.0f, 0.0f}, {0.0278f, -0.1191f, 0.9282f, 0.2119f, -0.0537f, 0.0049f, 0.0f, 0.0f}, {0.0288f, -0.1245f, 0.9170f, 0.2310f, -0.0581f, 0.0059f, 0.0f, 0.0f}, {0.0293f, -0.1294f, 0.9058f, 0.2510f, -0.0630f, 0.0063f, 0.0f, 0.0f}, {0.0303f, -0.1333f, 0.8926f, 0.2710f, -0.0679f, 0.0073f, 0.0f, 0.0f}, {0.0308f, -0.1367f, 0.8789f, 0.2915f, -0.0728f, 0.0083f, 0.0f, 0.0f}, {0.0308f, -0.1401f, 0.8657f, 0.3120f, -0.0776f, 0.0093f, 0.0f, 0.0f}, {0.0313f, -0.1426f, 0.8506f, 0.3330f, -0.0825f, 0.0103f, 0.0f, 0.0f}, {0.0313f, -0.1445f, 0.8354f, 0.3540f, -0.0874f, 0.0112f, 0.0f, 0.0f}, {0.0313f, -0.1460f, 0.8193f, 0.3755f, -0.0923f, 0.0122f, 0.0f, 0.0f}, {0.0313f, -0.1470f, 0.8022f, 0.3965f, -0.0967f, 0.0137f, 0.0f, 0.0f}, {0.0308f, -0.1479f, 0.7856f, 0.4185f, -0.1016f, 0.0146f, 0.0f, 0.0f}, {0.0303f, -0.1479f, 0.7681f, 0.4399f, -0.1060f, 0.0156f, 0.0f, 0.0f}, {0.0298f, -0.1479f, 0.7505f, 0.4614f, -0.1104f, 0.0166f, 0.0f, 0.0f}, {0.0293f, -0.1470f, 0.7314f, 0.4829f, -0.1147f, 0.0181f, 0.0f, 0.0f}, {0.0288f, -0.1460f, 0.7119f, 0.5049f, -0.1187f, 0.0190f, 0.0f, 0.0f}, {0.0278f, -0.1445f, 0.6929f, 0.5264f, -0.1226f, 0.0200f, 0.0f, 0.0f}, {0.0273f, -0.1431f, 0.6724f, 0.5479f, -0.1260f, 0.0215f, 0.0f, 0.0f}, {0.0264f, -0.1411f, 0.6528f, 0.5693f, -0.1299f, 0.0225f, 0.0f, 0.0f}, {0.0254f, -0.1387f, 0.6323f, 0.5903f, -0.1328f, 0.0234f, 0.0f, 0.0f}, {0.0244f, -0.1357f, 0.6113f, 0.6113f, -0.1357f, 0.0244f, 0.0f, 0.0f}, {0.0234f, -0.1328f, 0.5903f, 0.6323f, -0.1387f, 0.0254f, 0.0f, 0.0f}, {0.0225f, -0.1299f, 0.5693f, 0.6528f, -0.1411f, 0.0264f, 0.0f, 0.0f}, {0.0215f, -0.1260f, 0.5479f, 0.6724f, -0.1431f, 0.0273f, 0.0f, 0.0f}, {0.0200f, -0.1226f, 0.5264f, 0.6929f, -0.1445f, 0.0278f, 0.0f, 0.0f}, {0.0190f, -0.1187f, 0.5049f, 0.7119f, -0.1460f, 0.0288f, 0.0f, 0.0f}, {0.0181f, -0.1147f, 0.4829f, 0.7314f, -0.1470f, 0.0293f, 0.0f, 0.0f}, {0.0166f, -0.1104f, 0.4614f, 0.7505f, -0.1479f, 0.0298f, 0.0f, 0.0f}, {0.0156f, -0.1060f, 0.4399f, 0.7681f, -0.1479f, 0.0303f, 0.0f, 0.0f}, {0.0146f, -0.1016f, 0.4185f, 0.7856f, -0.1479f, 0.0308f, 0.0f, 0.0f}, {0.0137f, -0.0967f, 0.3965f, 0.8022f, -0.1470f, 0.0313f, 0.0f, 0.0f}, {0.0122f, -0.0923f, 0.3755f, 0.8193f, -0.1460f, 0.0313f, 0.0f, 0.0f}, {0.0112f, -0.0874f, 0.3540f, 0.8354f, -0.1445f, 0.0313f, 0.0f, 0.0f}, {0.0103f, -0.0825f, 0.3330f, 0.8506f, -0.1426f, 0.0313f, 0.0f, 0.0f}, {0.0093f, -0.0776f, 0.3120f, 0.8657f, -0.1401f, 0.0308f, 0.0f, 0.0f}, {0.0083f, -0.0728f, 0.2915f, 0.8789f, -0.1367f, 0.0308f, 0.0f, 0.0f}, {0.0073f, -0.0679f, 0.2710f, 0.8926f, -0.1333f, 0.0303f, 0.0f, 0.0f}, {0.0063f, -0.0630f, 0.2510f, 0.9058f, -0.1294f, 0.0293f, 0.0f, 0.0f}, {0.0059f, -0.0581f, 0.2310f, 0.9170f, -0.1245f, 0.0288f, 0.0f, 0.0f}, {0.0049f, -0.0537f, 0.2119f, 0.9282f, -0.1191f, 0.0278f, 0.0f, 0.0f}, {0.0044f, -0.0488f, 0.1929f, 0.9390f, -0.1138f, 0.0264f, 0.0f, 0.0f}, {0.0034f, -0.0439f, 0.1738f, 0.9487f, -0.1074f, 0.0254f, 0.0f, 0.0f}, {0.0029f, -0.0396f, 0.1558f, 0.9575f, -0.1006f, 0.0239f, 0.0f, 0.0f}, {0.0024f, -0.0352f, 0.1382f, 0.9648f, -0.0928f, 0.0225f, 0.0f, 0.0f}, {0.0020f, -0.0308f, 0.1206f, 0.9727f, -0.0850f, 0.0205f, 0.0f, 0.0f}, {0.0015f, -0.0264f, 0.1040f, 0.9785f, -0.0762f, 0.0186f, 0.0f, 0.0f}, {0.0010f, -0.0220f, 0.0874f, 0.9844f, -0.0674f, 0.0166f, 0.0f, 0.0f}, {0.0005f, -0.0181f, 0.0713f, 0.9897f, -0.0576f, 0.0142f, 0.0f, 0.0f}, {0.0005f, -0.0142f, 0.0562f, 0.9932f, -0.0474f, 0.0117f, 0.0f, 0.0f}, {0.0005f, -0.0103f, 0.0415f, 0.9956f, -0.0361f, 0.0088f, 0.0f, 0.0f}, {0.0f, -0.0068f, 0.0269f, 0.9985f, -0.0249f, 0.0063f, 0.0f, 0.0f}, {0.0f, -0.0034f, 0.0132f, 1.0000f, -0.0127f, 0.0029f, 0.0f, 0.0f} }; constexpr float coef_usm[kPhaseCount][kFilterSize] = { {0.0f, -0.6001f, 1.2002f, -0.6001f, 0.0f, 0.0f, 0.0f, 0.0f}, {0.0029f, -0.6084f, 1.1987f, -0.5903f, -0.0029f, 0.0f, 0.0f, 0.0f}, {0.0049f, -0.6147f, 1.1958f, -0.5791f, -0.0068f, 0.0005f, 0.0f, 0.0f}, {0.0073f, -0.6196f, 1.1890f, -0.5659f, -0.0103f, 0.0f, 0.0f, 0.0f}, {0.0093f, -0.6235f, 1.1802f, -0.5513f, -0.0151f, 0.0f, 0.0f, 0.0f}, {0.0112f, -0.6265f, 1.1699f, -0.5352f, -0.0195f, 0.0005f, 0.0f, 0.0f}, {0.0122f, -0.6270f, 1.1582f, -0.5181f, -0.0259f, 0.0005f, 0.0f, 0.0f}, {0.0142f, -0.6284f, 1.1455f, -0.5005f, -0.0317f, 0.0005f, 0.0f, 0.0f}, {0.0156f, -0.6265f, 1.1274f, -0.4790f, -0.0386f, 0.0005f, 0.0f, 0.0f}, {0.0166f, -0.6235f, 1.1089f, -0.4570f, -0.0454f, 0.0010f, 0.0f, 0.0f}, {0.0176f, -0.6187f, 1.0879f, -0.4346f, -0.0532f, 0.0010f, 0.0f, 0.0f}, {0.0181f, -0.6138f, 1.0659f, -0.4102f, -0.0615f, 0.0015f, 0.0f, 0.0f}, {0.0190f, -0.6069f, 1.0405f, -0.3843f, -0.0698f, 0.0015f, 0.0f, 0.0f}, {0.0195f, -0.6006f, 1.0161f, -0.3574f, -0.0796f, 0.0020f, 0.0f, 0.0f}, {0.0200f, -0.5928f, 0.9893f, -0.3286f, -0.0898f, 0.0024f, 0.0f, 0.0f}, {0.0200f, -0.5820f, 0.9580f, -0.2988f, -0.1001f, 0.0029f, 0.0f, 0.0f}, {0.0200f, -0.5728f, 0.9292f, -0.2690f, -0.1104f, 0.0034f, 0.0f, 0.0f}, {0.0200f, -0.5620f, 0.8975f, -0.2368f, -0.1226f, 0.0039f, 0.0f, 0.0f}, {0.0205f, -0.5498f, 0.8643f, -0.2046f, -0.1343f, 0.0044f, 0.0f, 0.0f}, {0.0200f, -0.5371f, 0.8301f, -0.1709f, -0.1465f, 0.0049f, 0.0f, 0.0f}, {0.0195f, -0.5239f, 0.7944f, -0.1367f, -0.1587f, 0.0054f, 0.0f, 0.0f}, {0.0195f, -0.5107f, 0.7598f, -0.1021f, -0.1724f, 0.0059f, 0.0f, 0.0f}, {0.0190f, -0.4966f, 0.7231f, -0.0649f, -0.1865f, 0.0063f, 0.0f, 0.0f}, {0.0186f, -0.4819f, 0.6846f, -0.0288f, -0.1997f, 0.0068f, 0.0f, 0.0f}, {0.0186f, -0.4668f, 0.6460f, 0.0093f, -0.2144f, 0.0073f, 0.0f, 0.0f}, {0.0176f, -0.4507f, 0.6055f, 0.0479f, -0.2290f, 0.0083f, 0.0f, 0.0f}, {0.0171f, -0.4370f, 0.5693f, 0.0859f, -0.2446f, 0.0088f, 0.0f, 0.0f}, {0.0161f, -0.4199f, 0.5283f, 0.1255f, -0.2598f, 0.0098f, 0.0f, 0.0f}, {0.0161f, -0.4048f, 0.4883f, 0.1655f, -0.2754f, 0.0103f, 0.0f, 0.0f}, {0.0151f, -0.3887f, 0.4497f, 0.2041f, -0.2910f, 0.0107f, 0.0f, 0.0f}, {0.0142f, -0.3711f, 0.4072f, 0.2446f, -0.3066f, 0.0117f, 0.0f, 0.0f}, {0.0137f, -0.3555f, 0.3672f, 0.2852f, -0.3228f, 0.0122f, 0.0f, 0.0f}, {0.0132f, -0.3394f, 0.3262f, 0.3262f, -0.3394f, 0.0132f, 0.0f, 0.0f}, {0.0122f, -0.3228f, 0.2852f, 0.3672f, -0.3555f, 0.0137f, 0.0f, 0.0f}, {0.0117f, -0.3066f, 0.2446f, 0.4072f, -0.3711f, 0.0142f, 0.0f, 0.0f}, {0.0107f, -0.2910f, 0.2041f, 0.4497f, -0.3887f, 0.0151f, 0.0f, 0.0f}, {0.0103f, -0.2754f, 0.1655f, 0.4883f, -0.4048f, 0.0161f, 0.0f, 0.0f}, {0.0098f, -0.2598f, 0.1255f, 0.5283f, -0.4199f, 0.0161f, 0.0f, 0.0f}, {0.0088f, -0.2446f, 0.0859f, 0.5693f, -0.4370f, 0.0171f, 0.0f, 0.0f}, {0.0083f, -0.2290f, 0.0479f, 0.6055f, -0.4507f, 0.0176f, 0.0f, 0.0f}, {0.0073f, -0.2144f, 0.0093f, 0.6460f, -0.4668f, 0.0186f, 0.0f, 0.0f}, {0.0068f, -0.1997f, -0.0288f, 0.6846f, -0.4819f, 0.0186f, 0.0f, 0.0f}, {0.0063f, -0.1865f, -0.0649f, 0.7231f, -0.4966f, 0.0190f, 0.0f, 0.0f}, {0.0059f, -0.1724f, -0.1021f, 0.7598f, -0.5107f, 0.0195f, 0.0f, 0.0f}, {0.0054f, -0.1587f, -0.1367f, 0.7944f, -0.5239f, 0.0195f, 0.0f, 0.0f}, {0.0049f, -0.1465f, -0.1709f, 0.8301f, -0.5371f, 0.0200f, 0.0f, 0.0f}, {0.0044f, -0.1343f, -0.2046f, 0.8643f, -0.5498f, 0.0205f, 0.0f, 0.0f}, {0.0039f, -0.1226f, -0.2368f, 0.8975f, -0.5620f, 0.0200f, 0.0f, 0.0f}, {0.0034f, -0.1104f, -0.2690f, 0.9292f, -0.5728f, 0.0200f, 0.0f, 0.0f}, {0.0029f, -0.1001f, -0.2988f, 0.9580f, -0.5820f, 0.0200f, 0.0f, 0.0f}, {0.0024f, -0.0898f, -0.3286f, 0.9893f, -0.5928f, 0.0200f, 0.0f, 0.0f}, {0.0020f, -0.0796f, -0.3574f, 1.0161f, -0.6006f, 0.0195f, 0.0f, 0.0f}, {0.0015f, -0.0698f, -0.3843f, 1.0405f, -0.6069f, 0.0190f, 0.0f, 0.0f}, {0.0015f, -0.0615f, -0.4102f, 1.0659f, -0.6138f, 0.0181f, 0.0f, 0.0f}, {0.0010f, -0.0532f, -0.4346f, 1.0879f, -0.6187f, 0.0176f, 0.0f, 0.0f}, {0.0010f, -0.0454f, -0.4570f, 1.1089f, -0.6235f, 0.0166f, 0.0f, 0.0f}, {0.0005f, -0.0386f, -0.4790f, 1.1274f, -0.6265f, 0.0156f, 0.0f, 0.0f}, {0.0005f, -0.0317f, -0.5005f, 1.1455f, -0.6284f, 0.0142f, 0.0f, 0.0f}, {0.0005f, -0.0259f, -0.5181f, 1.1582f, -0.6270f, 0.0122f, 0.0f, 0.0f}, {0.0005f, -0.0195f, -0.5352f, 1.1699f, -0.6265f, 0.0112f, 0.0f, 0.0f}, {0.0f, -0.0151f, -0.5513f, 1.1802f, -0.6235f, 0.0093f, 0.0f, 0.0f}, {0.0f, -0.0103f, -0.5659f, 1.1890f, -0.6196f, 0.0073f, 0.0f, 0.0f}, {0.0005f, -0.0068f, -0.5791f, 1.1958f, -0.6147f, 0.0049f, 0.0f, 0.0f}, {0.0f, -0.0029f, -0.5903f, 1.1987f, -0.6084f, 0.0029f, 0.0f, 0.0f} }; constexpr uint16_t coef_scale_fp16[kPhaseCount][kFilterSize] = { { 0, 0, 15360, 0, 0, 0, 0, 0 }, { 6640, 41601, 15360, 8898, 39671, 0, 0, 0 }, { 7796, 42592, 15357, 9955, 40695, 0, 0, 0 }, { 8321, 43167, 15351, 10576, 41286, 4121, 0, 0 }, { 8702, 43537, 15346, 11058, 41797, 4121, 0, 0 }, { 9029, 43871, 15339, 11408, 42146, 4121, 0, 0 }, { 9280, 44112, 15328, 11672, 42402, 5145, 0, 0 }, { 9411, 44256, 15316, 11944, 42690, 5669, 0, 0 }, { 9535, 44401, 15304, 12216, 42979, 6169, 0, 0 }, { 9667, 44528, 15288, 12396, 43137, 6378, 0, 0 }, { 9758, 44656, 15273, 12540, 43282, 6640, 0, 0 }, { 9857, 44768, 15255, 12688, 43423, 6903, 0, 0 }, { 9922, 44872, 15235, 12844, 43583, 7297, 0, 0 }, { 10014, 44959, 15213, 13000, 43744, 7429, 0, 0 }, { 10079, 45048, 15190, 13156, 43888, 7691, 0, 0 }, { 10112, 45092, 15167, 13316, 44040, 7796, 0, 0 }, { 10178, 45124, 15140, 13398, 44120, 8058, 0, 0 }, { 10211, 45152, 15112, 13482, 44201, 8256, 0, 0 }, { 10211, 45180, 15085, 13566, 44279, 8387, 0, 0 }, { 10242, 45200, 15054, 13652, 44360, 8518, 0, 0 }, { 10242, 45216, 15023, 13738, 44440, 8636, 0, 0 }, { 10242, 45228, 14990, 13826, 44520, 8767, 0, 0 }, { 10242, 45236, 14955, 13912, 44592, 8964, 0, 0 }, { 10211, 45244, 14921, 14002, 44673, 9082, 0, 0 }, { 10178, 45244, 14885, 14090, 44745, 9213, 0, 0 }, { 10145, 45244, 14849, 14178, 44817, 9280, 0, 0 }, { 10112, 45236, 14810, 14266, 44887, 9378, 0, 0 }, { 10079, 45228, 14770, 14346, 44953, 9437, 0, 0 }, { 10014, 45216, 14731, 14390, 45017, 9503, 0, 0 }, { 9981, 45204, 14689, 14434, 45064, 9601, 0, 0 }, { 9922, 45188, 14649, 14478, 45096, 9667, 0, 0 }, { 9857, 45168, 14607, 14521, 45120, 9726, 0, 0 }, { 9791, 45144, 14564, 14564, 45144, 9791, 0, 0 }, { 9726, 45120, 14521, 14607, 45168, 9857, 0, 0 }, { 9667, 45096, 14478, 14649, 45188, 9922, 0, 0 }, { 9601, 45064, 14434, 14689, 45204, 9981, 0, 0 }, { 9503, 45017, 14390, 14731, 45216, 10014, 0, 0 }, { 9437, 44953, 14346, 14770, 45228, 10079, 0, 0 }, { 9378, 44887, 14266, 14810, 45236, 10112, 0, 0 }, { 9280, 44817, 14178, 14849, 45244, 10145, 0, 0 }, { 9213, 44745, 14090, 14885, 45244, 10178, 0, 0 }, { 9082, 44673, 14002, 14921, 45244, 10211, 0, 0 }, { 8964, 44592, 13912, 14955, 45236, 10242, 0, 0 }, { 8767, 44520, 13826, 14990, 45228, 10242, 0, 0 }, { 8636, 44440, 13738, 15023, 45216, 10242, 0, 0 }, { 8518, 44360, 13652, 15054, 45200, 10242, 0, 0 }, { 8387, 44279, 13566, 15085, 45180, 10211, 0, 0 }, { 8256, 44201, 13482, 15112, 45152, 10211, 0, 0 }, { 8058, 44120, 13398, 15140, 45124, 10178, 0, 0 }, { 7796, 44040, 13316, 15167, 45092, 10112, 0, 0 }, { 7691, 43888, 13156, 15190, 45048, 10079, 0, 0 }, { 7429, 43744, 13000, 15213, 44959, 10014, 0, 0 }, { 7297, 43583, 12844, 15235, 44872, 9922, 0, 0 }, { 6903, 43423, 12688, 15255, 44768, 9857, 0, 0 }, { 6640, 43282, 12540, 15273, 44656, 9758, 0, 0 }, { 6378, 43137, 12396, 15288, 44528, 9667, 0, 0 }, { 6169, 42979, 12216, 15304, 44401, 9535, 0, 0 }, { 5669, 42690, 11944, 15316, 44256, 9411, 0, 0 }, { 5145, 42402, 11672, 15328, 44112, 9280, 0, 0 }, { 4121, 42146, 11408, 15339, 43871, 9029, 0, 0 }, { 4121, 41797, 11058, 15346, 43537, 8702, 0, 0 }, { 4121, 41286, 10576, 15351, 43167, 8321, 0, 0 }, { 0, 40695, 9955, 15357, 42592, 7796, 0, 0 }, { 0, 39671, 8898, 15360, 41601, 6640, 0, 0 }, }; constexpr uint16_t coef_usm_fp16[kPhaseCount][kFilterSize] = { { 0, 47309, 15565, 47309, 0, 0, 0, 0 }, { 6640, 47326, 15563, 47289, 39408, 0, 0, 0 }, { 7429, 47339, 15560, 47266, 40695, 4121, 0, 0 }, { 8058, 47349, 15554, 47239, 41286, 0, 0, 0 }, { 8387, 47357, 15545, 47209, 41915, 0, 0, 0 }, { 8636, 47363, 15534, 47176, 42238, 4121, 0, 0 }, { 8767, 47364, 15522, 47141, 42657, 4121, 0, 0 }, { 9029, 47367, 15509, 47105, 43023, 4121, 0, 0 }, { 9213, 47363, 15490, 47018, 43249, 4121, 0, 0 }, { 9280, 47357, 15472, 46928, 43472, 5145, 0, 0 }, { 9345, 47347, 15450, 46836, 43727, 5145, 0, 0 }, { 9378, 47337, 15427, 46736, 43999, 5669, 0, 0 }, { 9437, 47323, 15401, 46630, 44152, 5669, 0, 0 }, { 9470, 47310, 15376, 46520, 44312, 6169, 0, 0 }, { 9503, 47294, 15338, 46402, 44479, 6378, 0, 0 }, { 9503, 47272, 15274, 46280, 44648, 6640, 0, 0 }, { 9503, 47253, 15215, 46158, 44817, 6903, 0, 0 }, { 9503, 47231, 15150, 45972, 45017, 7165, 0, 0 }, { 9535, 47206, 15082, 45708, 45132, 7297, 0, 0 }, { 9503, 47180, 15012, 45432, 45232, 7429, 0, 0 }, { 9470, 47153, 14939, 45152, 45332, 7560, 0, 0 }, { 9470, 47126, 14868, 44681, 45444, 7691, 0, 0 }, { 9437, 47090, 14793, 44071, 45560, 7796, 0, 0 }, { 9411, 47030, 14714, 42847, 45668, 7927, 0, 0 }, { 9411, 46968, 14635, 8387, 45788, 8058, 0, 0 }, { 9345, 46902, 14552, 10786, 45908, 8256, 0, 0 }, { 9313, 46846, 14478, 11647, 46036, 8321, 0, 0 }, { 9247, 46776, 14394, 12292, 46120, 8453, 0, 0 }, { 9247, 46714, 14288, 12620, 46184, 8518, 0, 0 }, { 9147, 46648, 14130, 12936, 46248, 8570, 0, 0 }, { 9029, 46576, 13956, 13268, 46312, 8702, 0, 0 }, { 8964, 46512, 13792, 13456, 46378, 8767, 0, 0 }, { 8898, 46446, 13624, 13624, 46446, 8898, 0, 0 }, { 8767, 46378, 13456, 13792, 46512, 8964, 0, 0 }, { 8702, 46312, 13268, 13956, 46576, 9029, 0, 0 }, { 8570, 46248, 12936, 14130, 46648, 9147, 0, 0 }, { 8518, 46184, 12620, 14288, 46714, 9247, 0, 0 }, { 8453, 46120, 12292, 14394, 46776, 9247, 0, 0 }, { 8321, 46036, 11647, 14478, 46846, 9313, 0, 0 }, { 8256, 45908, 10786, 14552, 46902, 9345, 0, 0 }, { 8058, 45788, 8387, 14635, 46968, 9411, 0, 0 }, { 7927, 45668, 42847, 14714, 47030, 9411, 0, 0 }, { 7796, 45560, 44071, 14793, 47090, 9437, 0, 0 }, { 7691, 45444, 44681, 14868, 47126, 9470, 0, 0 }, { 7560, 45332, 45152, 14939, 47153, 9470, 0, 0 }, { 7429, 45232, 45432, 15012, 47180, 9503, 0, 0 }, { 7297, 45132, 45708, 15082, 47206, 9535, 0, 0 }, { 7165, 45017, 45972, 15150, 47231, 9503, 0, 0 }, { 6903, 44817, 46158, 15215, 47253, 9503, 0, 0 }, { 6640, 44648, 46280, 15274, 47272, 9503, 0, 0 }, { 6378, 44479, 46402, 15338, 47294, 9503, 0, 0 }, { 6169, 44312, 46520, 15376, 47310, 9470, 0, 0 }, { 5669, 44152, 46630, 15401, 47323, 9437, 0, 0 }, { 5669, 43999, 46736, 15427, 47337, 9378, 0, 0 }, { 5145, 43727, 46836, 15450, 47347, 9345, 0, 0 }, { 5145, 43472, 46928, 15472, 47357, 9280, 0, 0 }, { 4121, 43249, 47018, 15490, 47363, 9213, 0, 0 }, { 4121, 43023, 47105, 15509, 47367, 9029, 0, 0 }, { 4121, 42657, 47141, 15522, 47364, 8767, 0, 0 }, { 4121, 42238, 47176, 15534, 47363, 8636, 0, 0 }, { 0, 41915, 47209, 15545, 47357, 8387, 0, 0 }, { 0, 41286, 47239, 15554, 47349, 8058, 0, 0 }, { 4121, 40695, 47266, 15560, 47339, 7429, 0, 0 }, { 0, 39408, 47289, 15563, 47326, 6640, 0, 0 }, }; }ValveSoftware-gamescope-eb620ab/src/shaders/NVIDIAImageScaling/NIS/NIS_Main.glsl000066400000000000000000000061401502457270500273050ustar00rootroot00000000000000// The MIT License(MIT) // // Copyright(c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files(the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions : // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. //--------------------------------------------------------------------------------- // NVIDIA Image Scaling SDK - v1.0.3 //--------------------------------------------------------------------------------- // GLSL main example //--------------------------------------------------------------------------------- #version 450 #extension GL_ARB_separate_shader_objects : enable #extension GL_ARB_shading_language_420pack : enable #extension GL_GOOGLE_include_directive : enable #extension GL_EXT_shader_16bit_storage : require #extension GL_EXT_shader_explicit_arithmetic_types : require #define NIS_GLSL 1 #ifndef NIS_SCALER #define NIS_SCALER 1 #endif layout(set=0,binding=0) uniform const_buffer { float kDetectRatio; float kDetectThres; float kMinContrastRatio; float kRatioNorm; float kContrastBoost; float kEps; float kSharpStartY; float kSharpScaleY; float kSharpStrengthMin; float kSharpStrengthScale; float kSharpLimitMin; float kSharpLimitScale; float kScaleX; float kScaleY; float kDstNormX; float kDstNormY; float kSrcNormX; float kSrcNormY; uint kInputViewportOriginX; uint kInputViewportOriginY; uint kInputViewportWidth; uint kInputViewportHeight; uint kOutputViewportOriginX; uint kOutputViewportOriginY; uint kOutputViewportWidth; uint kOutputViewportHeight; float reserved0; float reserved1; }; layout(set=0,binding=1) uniform sampler samplerLinearClamp; layout(set=0,binding=2) uniform texture2D in_texture; layout(set=0,binding=3) uniform writeonly image2D out_texture; #if NIS_SCALER layout(set=0,binding=4) uniform texture2D coef_scaler; layout(set=0,binding=5) uniform texture2D coef_usm; #endif #include "NIS_Scaler.h" layout(local_size_x=NIS_THREAD_GROUP_SIZE) in; void main() { #if NIS_SCALER NVScaler(gl_WorkGroupID.xy, gl_LocalInvocationID.x); #else NVSharpen(gl_WorkGroupID.xy, gl_LocalInvocationID.x); #endif }ValveSoftware-gamescope-eb620ab/src/shaders/NVIDIAImageScaling/NIS/NIS_Main.hlsl000066400000000000000000000064051502457270500273120ustar00rootroot00000000000000// The MIT License(MIT) // // Copyright(c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files(the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions : // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. //--------------------------------------------------------------------------------- // NVIDIA Image Scaling SDK - v1.0.3 //--------------------------------------------------------------------------------- // HLSL main example //--------------------------------------------------------------------------------- #define NIS_HLSL 1 #ifndef NIS_SCALER #define NIS_SCALER 1 #endif #ifndef NIS_DXC #define NIS_DXC 0 #endif #if NIS_DXC #define NIS_PUSH_CONSTANT [[vk::push_constant]] #define NIS_BINDING(bindingIndex) [[vk::binding(bindingIndex, 0)]] #else #define NIS_PUSH_CONSTANT #define NIS_BINDING(bindingIndex) #endif NIS_BINDING(0) cbuffer cb : register(b0) { float kDetectRatio; float kDetectThres; float kMinContrastRatio; float kRatioNorm; float kContrastBoost; float kEps; float kSharpStartY; float kSharpScaleY; float kSharpStrengthMin; float kSharpStrengthScale; float kSharpLimitMin; float kSharpLimitScale; float kScaleX; float kScaleY; float kDstNormX; float kDstNormY; float kSrcNormX; float kSrcNormY; uint kInputViewportOriginX; uint kInputViewportOriginY; uint kInputViewportWidth; uint kInputViewportHeight; uint kOutputViewportOriginX; uint kOutputViewportOriginY; uint kOutputViewportWidth; uint kOutputViewportHeight; float reserved0; float reserved1; }; NIS_BINDING(1) SamplerState samplerLinearClamp : register(s0); #if NIS_NV12_SUPPORT NIS_BINDING(2) Texture2D in_texture_y : register(t0); NIS_BINDING(2) Texture2D in_texture_uv : register(t3); #else NIS_BINDING(2) Texture2D in_texture : register(t0); #endif NIS_BINDING(3) RWTexture2D out_texture : register(u0); #if NIS_SCALER NIS_BINDING(4) Texture2D coef_scaler : register(t1); NIS_BINDING(5) Texture2D coef_usm : register(t2); #endif #include "NIS_Scaler.h" [numthreads(NIS_THREAD_GROUP_SIZE, 1, 1)] void main(uint3 blockIdx : SV_GroupID, uint3 threadIdx : SV_GroupThreadID) { #if NIS_SCALER NVScaler(blockIdx.xy, threadIdx.x); #else NVSharpen(blockIdx.xy, threadIdx.x); #endif } ValveSoftware-gamescope-eb620ab/src/shaders/NVIDIAImageScaling/NIS/NIS_Scaler.h000066400000000000000000001114131502457270500271200ustar00rootroot00000000000000// The MIT License(MIT) // // Copyright(c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files(the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions : // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. //--------------------------------------------------------------------------------- // NVIDIA Image Scaling SDK - v1.0.3 //--------------------------------------------------------------------------------- // The NVIDIA Image Scaling SDK provides a single spatial scaling and sharpening algorithm // for cross-platform support. The scaling algorithm uses a 6-tap scaling filter combined // with 4 directional scaling and adaptive sharpening filters, which creates nice smooth images // and sharp edges. In addition, the SDK provides a state-of-the-art adaptive directional sharpening algorithm // for use in applications where no scaling is required. // // The directional scaling and sharpening algorithm is named NVScaler while the adaptive-directional-sharpening-only // algorithm is named NVSharpen. Both algorithms are provided as compute shaders and // developers are free to integrate them in their applications. Note that if you integrate NVScaler, you // should NOT integrate NVSharpen, as NVScaler already includes a sharpening pass // // Pipeline Placement // ------------------ // The call into the NVIDIA Image Scaling shaders must occur during the post-processing phase after tone-mapping. // Applying the scaling in linear HDR in-game color-space may result in a sharpening effect that is // either not visible or too strong. Since sharpening algorithms can enhance noisy or grainy regions, it is recommended // that certain effects such as film grain should occur after NVScaler or NVSharpen. Low-pass filters such as motion blur or // light bloom are recommended to be applied before NVScaler or NVSharpen to avoid sharpening attenuation. // // Color Space and Ranges // ---------------------- // NVIDIA Image Scaling shaders can process color textures stored as either LDR or HDR with the following // restrictions: // 1) LDR // - The range of color values must be in the [0, 1] range // - The input color texture must be in display-referred color-space after tone mapping and OETF (gamma-correction) // has been applied // 2) HDR PQ // - The range of color values must be in the [0, 1] range // - The input color texture must be in display-referred color-space after tone mapping with Rec.2020 PQ OETF applied // 3) HDR Linear // - The recommended range of color values is [0, 12.5], where luminance value (as per BT. 709) of // 1.0 maps to brightness value of 80nits (sRGB peak) and 12.5 maps to 1000nits // - The input color texture may have luminance values that are either linear and scene-referred or // linear and display-referred (after tone mapping) // // If the input color texture sent to NVScaler/NVSharpen is in HDR format set NIS_HDR_MODE define to either // NIS_HDR_MODE_LINEAR (1) or NIS_HDR_MODE_PQ (2). // // Supported Texture Formats // ------------------------- // Input and output formats: // Input and output formats are expected to be in the rages defined in previous section and should be // specified using non-integer data types such as DXGI_FORMAT_R8G8B8A8_UNORM. // // Coefficients formats: // The scaler coefficients and USM coefficients format should be specified using float4 type such as // DXGI_FORMAT_R32G32B32A32_FLOAT or DXGI_FORMAT_R16G16B16A16_FLOAT. // // Resource States, Buffers, and Sampler: // The game or application calling NVIDIA Image Scaling SDK shaders must ensure that the textures are in // the correct state. // - Input color textures must be in pixel shader read state. Shader Resource View (SRV) in DirectX // - The output texture must be in read/write state. Unordered Access View (UAV) in DirectX // - The coefficients texture for NVScaler must be in read state. Shader Resource View (SRV) in DirectX // - The configuration variables must be passed as constant buffer. Constant Buffer View (CBV) in DirectX // - The sampler for texture pixel sampling. Linear clamp SamplerState in Direct // // Adding NVIDIA Image Scaling SDK to a Project // -------------------------------------------- // Include NIS_Scaler.h directly in your application or alternative use the provided NIS_Main.hlsl shader file. // Use NIS_Config.h to get the ideal shader dispatch values for your platform, to configure the algorithm constant // values (NVScalerUpdateConfig, and NVSharpenUpdateConfig), and to access the algorithm coefficients (coef_scale and coef_USM). // // Defines: // NIS_SCALER: default (1) NVScaler, (0) fast NVSharpen only, no upscaling // NIS_HDR_MODE: default (0) disabled, (1) Linear, (2) PQ // NIS_BLOCK_WIDTH: pixels per block width. Use GetOptimalBlockWidth query for your platform // NIS_BLOCK_HEIGHT: pixels per block height. Use GetOptimalBlockHeight query for your platform // NIS_THREAD_GROUP_SIZE: number of threads per group. Use GetOptimalThreadGroupSize query for your platform // NIS_USE_HALF_PRECISION: default (0) disabled, (1) enable half pression computation // NIS_HLSL: (1) enabled, (0) disabled // NIS_HLSL_6_2: default (0) HLSL v5, (1) HLSL v6.2 forces NIS_HLSL=1 // NIS_GLSL: (1) enabled, (0) disabled // NIS_VIEWPORT_SUPPORT: default(0) disabled, (1) enable input/output viewport support // NIS_NV12_SUPPORT: default(0) disabled, (1) enable NV12 input // NIS_CLAMP_OUTPUT: default(0) disabled, (1) enable output clamp // // Default NVScaler shader constants: // [NIS_BLOCK_WIDTH, NIS_BLOCK_HEIGHT, NIS_THREAD_GROUP_SIZE] = [32, 24, 256] // // Default NVSharpen shader constants: // [NIS_BLOCK_WIDTH, NIS_BLOCK_HEIGHT, NIS_THREAD_GROUP_SIZE] = [32, 32, 256] // // NIS_UNROLL: default [unroll] // NIS_UNROLL_INNER: default NIS_UNROLL, define in case of a compiler error for inner nested loops //--------------------------------------------------------------------------------- // NVScaler enable by default. Set to 0 for NVSharpen only #ifndef NIS_SCALER #define NIS_SCALER 1 #endif // HDR Modes #define NIS_HDR_MODE_NONE 0 #define NIS_HDR_MODE_LINEAR 1 #define NIS_HDR_MODE_PQ 2 #ifndef NIS_HDR_MODE #define NIS_HDR_MODE NIS_HDR_MODE_NONE #endif #define kHDRCompressionFactor 0.282842712f // Viewport support #ifndef NIS_VIEWPORT_SUPPORT #define NIS_VIEWPORT_SUPPORT 0 #endif // HLSL, GLSL #if NIS_HLSL==0 && !defined(NIS_GLSL) #define NIS_GLSL 1 #endif #if NIS_HLSL_6_2 || (!NIS_GLSL && !NIS_HLSL) #if defined(NIS_HLSL) #undef NIS_HLSL #endif #define NIS_HLSL 1 #endif #if NIS_HLSL && NIS_GLSL #undef NIS_GLSL #define NIS_GLSL 0 #endif // Half precision #ifndef NIS_USE_HALF_PRECISION #define NIS_USE_HALF_PRECISION 0 #endif #if NIS_HLSL // Generic type and function aliases for HLSL #define NVF float #define NVF2 float2 #define NVF3 float3 #define NVF4 float4 #define NVI int #define NVI2 int2 #define NVU uint #define NVU2 uint2 #define NVB bool #if NIS_USE_HALF_PRECISION #if NIS_HLSL_6_2 #define NVH float16_t #define NVH2 float16_t2 #define NVH3 float16_t3 #define NVH4 float16_t4 #else #define NVH min16float #define NVH2 min16float2 #define NVH3 min16float3 #define NVH4 min16float4 #endif // NIS_HLSL_6_2 #else // FP32 types #define NVH NVF #define NVH2 NVF2 #define NVH3 NVF3 #define NVH4 NVF4 #endif // NIS_USE_HALF_PRECISION #define NVSHARED groupshared #define NVTEX_LOAD(x, pos) x[pos] #define NVTEX_SAMPLE(x, sampler, pos) x.SampleLevel(sampler, pos, 0) #define NVTEX_SAMPLE_RED(x, sampler, pos) x.GatherRed(sampler, pos) #define NVTEX_SAMPLE_GREEN(x, sampler, pos) x.GatherGreen(sampler, pos) #define NVTEX_SAMPLE_BLUE(x, sampler, pos) x.GatherBlue(sampler, pos) #define NVTEX_STORE(x, pos, v) x[pos] = v #ifndef NIS_UNROLL #define NIS_UNROLL [unroll] #endif #endif // NIS_HLSL // Generic type and function aliases for GLSL #if NIS_GLSL #define NVF float #define NVF2 vec2 #define NVF3 vec3 #define NVF4 vec4 #define NVI int #define NVI2 ivec2 #define NVU uint #define NVU2 uvec2 #define NVB bool #if NIS_USE_HALF_PRECISION #define NVH float16_t #define NVH2 f16vec2 #define NVH3 f16vec3 #define NVH4 f16vec4 #else // FP32 types #define NVH NVF #define NVH2 NVF2 #define NVH3 NVF3 #define NVH4 NVF4 #endif // NIS_USE_HALF_PRECISION #define NVSHARED shared #define NVTEX_LOAD(x, pos) texelFetch(sampler2D(x, samplerLinearClamp), pos, 0) #define NVTEX_SAMPLE(x, sampler, pos) textureLod(sampler2D(x, sampler), pos, 0) #define NVTEX_SAMPLE_RED(x, sampler, pos) textureGather(sampler2D(x, sampler), pos, 0) #define NVTEX_SAMPLE_GREEN(x, sampler, pos) textureGather(sampler2D(x, sampler), pos, 1) #define NVTEX_SAMPLE_BLUE(x, sampler, pos) textureGather(sampler2D(x, sampler), pos, 2) #define NVTEX_STORE(x, pos, v) imageStore(x, NVI2(pos), v) #define saturate(x) clamp(x, 0, 1) #define lerp(a, b, x) mix(a, b, x) #define GroupMemoryBarrierWithGroupSync() groupMemoryBarrier(); barrier() #ifndef NIS_UNROLL #define NIS_UNROLL #endif #endif // NIS_GLSL #ifndef NIS_UNROLL_INNER #define NIS_UNROLL_INNER NIS_UNROLL #endif // Texture gather #ifndef NIS_TEXTURE_GATHER #define NIS_TEXTURE_GATHER 0 #endif // NIS Scaling #define NIS_SCALE_INT 1 #define NIS_SCALE_FLOAT NVF(1.f) // NIS output clamp #if NIS_CLAMP_OUTPUT #if NIS_HDR_MODE == NIS_HDR_MODE_LINEAR #define NVCLAMP(x) ( clamp(x, 0.0f, 12.5f) ) #else #define NVCLAMP(x) ( saturate(x) ) #endif #else #define NVCLAMP(x) (x) #endif NVF getY(NVF3 rgba) { #if NIS_HDR_MODE == NIS_HDR_MODE_PQ return NVF(0.262f) * rgba.x + NVF(0.678f) * rgba.y + NVF(0.0593f) * rgba.z; #elif NIS_HDR_MODE == NIS_HDR_MODE_LINEAR return sqrt(NVF(0.2126f) * rgba.x + NVF(0.7152f) * rgba.y + NVF(0.0722f) * rgba.z) * kHDRCompressionFactor; #else return NVF(0.2126f) * rgba.x + NVF(0.7152f) * rgba.y + NVF(0.0722f) * rgba.z; #endif } NVF getYLinear(NVF3 rgba) { return NVF(0.2126f) * rgba.x + NVF(0.7152f) * rgba.y + NVF(0.0722f) * rgba.z; } NVF3 YUVtoRGB(NVF3 yuv) { float y = yuv.x - 16.0f / 255.0f; float u = yuv.y - 128.0f / 255.0f; float v = yuv.z - 128.0f / 255.0f; NVF3 rgb; rgb.x = saturate(1.164f * y + 1.596f * v); rgb.y = saturate(1.164f * y - 0.392f * u - 0.813f * v); rgb.z = saturate(1.164f * y + 2.017f * u); return rgb; } #if NIS_SCALER NVF4 GetEdgeMap(NVF p[4][4], NVI i, NVI j) #else NVF4 GetEdgeMap(NVF p[5][5], NVI i, NVI j) #endif { const NVF g_0 = abs(p[0 + i][0 + j] + p[0 + i][1 + j] + p[0 + i][2 + j] - p[2 + i][0 + j] - p[2 + i][1 + j] - p[2 + i][2 + j]); const NVF g_45 = abs(p[1 + i][0 + j] + p[0 + i][0 + j] + p[0 + i][1 + j] - p[2 + i][1 + j] - p[2 + i][2 + j] - p[1 + i][2 + j]); const NVF g_90 = abs(p[0 + i][0 + j] + p[1 + i][0 + j] + p[2 + i][0 + j] - p[0 + i][2 + j] - p[1 + i][2 + j] - p[2 + i][2 + j]); const NVF g_135 = abs(p[1 + i][0 + j] + p[2 + i][0 + j] + p[2 + i][1 + j] - p[0 + i][1 + j] - p[0 + i][2 + j] - p[1 + i][2 + j]); const NVF g_0_90_max = max(g_0, g_90); const NVF g_0_90_min = min(g_0, g_90); const NVF g_45_135_max = max(g_45, g_135); const NVF g_45_135_min = min(g_45, g_135); NVF e_0_90 = 0; NVF e_45_135 = 0; if (g_0_90_max + g_45_135_max == 0) { return NVF4(0, 0, 0, 0); } e_0_90 = min(g_0_90_max / (g_0_90_max + g_45_135_max), 1.0f); e_45_135 = 1.0f - e_0_90; NVB c_0_90 = (g_0_90_max > (g_0_90_min * kDetectRatio)) && (g_0_90_max > kDetectThres) && (g_0_90_max > g_45_135_min); NVB c_45_135 = (g_45_135_max > (g_45_135_min * kDetectRatio)) && (g_45_135_max > kDetectThres) && (g_45_135_max > g_0_90_min); NVB c_g_0_90 = g_0_90_max == g_0; NVB c_g_45_135 = g_45_135_max == g_45; NVF f_e_0_90 = (c_0_90 && c_45_135) ? e_0_90 : 1.0f; NVF f_e_45_135 = (c_0_90 && c_45_135) ? e_45_135 : 1.0f; NVF weight_0 = (c_0_90 && c_g_0_90) ? f_e_0_90 : 0.0f; NVF weight_90 = (c_0_90 && !c_g_0_90) ? f_e_0_90 : 0.0f; NVF weight_45 = (c_45_135 && c_g_45_135) ? f_e_45_135 : 0.0f; NVF weight_135 = (c_45_135 && !c_g_45_135) ? f_e_45_135 : 0.0f; return NVF4(weight_0, weight_90, weight_45, weight_135); } #if NIS_SCALER #ifndef NIS_BLOCK_WIDTH #define NIS_BLOCK_WIDTH 32 #endif #ifndef NIS_BLOCK_HEIGHT #define NIS_BLOCK_HEIGHT 24 #endif #ifndef NIS_THREAD_GROUP_SIZE #define NIS_THREAD_GROUP_SIZE 256 #endif #define kPhaseCount 64 #define kFilterSize 6 #define kSupportSize 6 #define kPadSize kSupportSize // 'Tile' is the region of source luminance values that we load into shPixelsY. // It is the area of source pixels covered by the destination 'Block' plus a // 3 pixel border of support pixels. #define kTilePitch (NIS_BLOCK_WIDTH + kPadSize) #define kTileSize (kTilePitch * (NIS_BLOCK_HEIGHT + kPadSize)) // 'EdgeMap' is the region of source pixels for which edge map vectors are derived. // It is the area of source pixels covered by the destination 'Block' plus a // 1 pixel border. #define kEdgeMapPitch (NIS_BLOCK_WIDTH + 2) #define kEdgeMapSize (kEdgeMapPitch * (NIS_BLOCK_HEIGHT + 2)) NVSHARED NVF shPixelsY[kTileSize]; NVSHARED NVH shCoefScaler[kPhaseCount][kFilterSize]; NVSHARED NVH shCoefUSM[kPhaseCount][kFilterSize]; NVSHARED NVH4 shEdgeMap[kEdgeMapSize]; void LoadFilterBanksSh(NVI i0) { // Load up filter banks to shared memory // The work is spread over (kPhaseCount * 2) threads NVI i = i0; #if( kPhaseCount * 2 > NIS_THREAD_GROUP_SIZE ) for (; i < kPhaseCount * 2; i += NIS_THREAD_GROUP_SIZE) #else if (i < kPhaseCount * 2) #endif { NVI phase = i >> 1; NVI vIdx = i & 1; NVH4 v = NVH4(NVTEX_LOAD(coef_scaler, NVI2(vIdx, phase))); NVI filterOffset = vIdx * 4; shCoefScaler[phase][filterOffset + 0] = v.x; shCoefScaler[phase][filterOffset + 1] = v.y; if (vIdx == 0) { shCoefScaler[phase][2] = v.z; shCoefScaler[phase][3] = v.w; } v = NVH4(NVTEX_LOAD(coef_usm, NVI2(vIdx, phase))); shCoefUSM[phase][filterOffset + 0] = v.x; shCoefUSM[phase][filterOffset + 1] = v.y; if (vIdx == 0) { shCoefUSM[phase][2] = v.z; shCoefUSM[phase][3] = v.w; } } } NVF CalcLTI(NVF p0, NVF p1, NVF p2, NVF p3, NVF p4, NVF p5, NVI phase_index) { const NVB selector = (phase_index <= kPhaseCount / 2); NVF sel = selector ? p0 : p3; const NVF a_min = min(min(p1, p2), sel); const NVF a_max = max(max(p1, p2), sel); sel = selector ? p2 : p5; const NVF b_min = min(min(p3, p4), sel); const NVF b_max = max(max(p3, p4), sel); const NVF a_cont = a_max - a_min; const NVF b_cont = b_max - b_min; const NVF cont_ratio = max(a_cont, b_cont) / (min(a_cont, b_cont) + kEps); return (1.0f - saturate((cont_ratio - kMinContrastRatio) * kRatioNorm)) * kContrastBoost; } NVF4 GetInterpEdgeMap(const NVF4 edge[2][2], NVF phase_frac_x, NVF phase_frac_y) { NVF4 h0 = lerp(edge[0][0], edge[0][1], phase_frac_x); NVF4 h1 = lerp(edge[1][0], edge[1][1], phase_frac_x); return lerp(h0, h1, phase_frac_y); } NVF EvalPoly6(const NVF pxl[6], NVI phase_int) { NVF y = 0.f; { NIS_UNROLL for (NVI i = 0; i < 6; ++i) { y += shCoefScaler[phase_int][i] * pxl[i]; } } NVF y_usm = 0.f; { NIS_UNROLL for (NVI i = 0; i < 6; ++i) { y_usm += shCoefUSM[phase_int][i] * pxl[i]; } } // let's compute a piece-wise ramp based on luma const NVF y_scale = 1.0f - saturate((y * (1.0f / NIS_SCALE_FLOAT) - kSharpStartY) * kSharpScaleY); // scale the ramp to sharpen as a function of luma const NVF y_sharpness = y_scale * kSharpStrengthScale + kSharpStrengthMin; y_usm *= y_sharpness; // scale the ramp to limit USM as a function of luma const NVF y_sharpness_limit = (y_scale * kSharpLimitScale + kSharpLimitMin) * y; y_usm = min(y_sharpness_limit, max(-y_sharpness_limit, y_usm)); // reduce ringing y_usm *= CalcLTI(pxl[0], pxl[1], pxl[2], pxl[3], pxl[4], pxl[5], phase_int); return y + y_usm; } NVF FilterNormal(const NVF p[6][6], NVI phase_x_frac_int, NVI phase_y_frac_int) { NVF h_acc = 0.0f; NIS_UNROLL for (NVI j = 0; j < 6; ++j) { NVF v_acc = 0.0f; NIS_UNROLL for (NVI i = 0; i < 6; ++i) { v_acc += p[i][j] * shCoefScaler[phase_y_frac_int][i]; } h_acc += v_acc * shCoefScaler[phase_x_frac_int][j]; } // let's return the sum unpacked -> we can accumulate it later return h_acc; } NVF AddDirFilters(NVF p[6][6], NVF phase_x_frac, NVF phase_y_frac, NVI phase_x_frac_int, NVI phase_y_frac_int, NVF4 w) { NVF f = 0; if (w.x > 0.0f) { // 0 deg filter NVF interp0Deg[6]; { NIS_UNROLL for (NVI i = 0; i < 6; ++i) { interp0Deg[i] = lerp(p[i][2], p[i][3], phase_x_frac); } } f += EvalPoly6(interp0Deg, phase_y_frac_int) * w.x; } if (w.y > 0.0f) { // 90 deg filter NVF interp90Deg[6]; { NIS_UNROLL for (NVI i = 0; i < 6; ++i) { interp90Deg[i] = lerp(p[2][i], p[3][i], phase_y_frac); } } f += EvalPoly6(interp90Deg, phase_x_frac_int) * w.y; } if (w.z > 0.0f) { //45 deg filter NVF pphase_b45 = 0.5f + 0.5f * (phase_x_frac - phase_y_frac); NVF temp_interp45Deg[7]; temp_interp45Deg[1] = lerp(p[2][1], p[1][2], pphase_b45); temp_interp45Deg[3] = lerp(p[3][2], p[2][3], pphase_b45); temp_interp45Deg[5] = lerp(p[4][3], p[3][4], pphase_b45); { pphase_b45 = pphase_b45 - 0.5f; NVF a = (pphase_b45 >= 0.f) ? p[0][2] : p[2][0]; NVF b = (pphase_b45 >= 0.f) ? p[1][3] : p[3][1]; NVF c = (pphase_b45 >= 0.f) ? p[2][4] : p[4][2]; NVF d = (pphase_b45 >= 0.f) ? p[3][5] : p[5][3]; temp_interp45Deg[0] = lerp(p[1][1], a, abs(pphase_b45)); temp_interp45Deg[2] = lerp(p[2][2], b, abs(pphase_b45)); temp_interp45Deg[4] = lerp(p[3][3], c, abs(pphase_b45)); temp_interp45Deg[6] = lerp(p[4][4], d, abs(pphase_b45)); } NVF interp45Deg[6]; NVF pphase_p45 = phase_x_frac + phase_y_frac; if (pphase_p45 >= 1) { NIS_UNROLL for (NVI i = 0; i < 6; i++) { interp45Deg[i] = temp_interp45Deg[i + 1]; } pphase_p45 = pphase_p45 - 1; } else { NIS_UNROLL for (NVI i = 0; i < 6; i++) { interp45Deg[i] = temp_interp45Deg[i]; } } f += EvalPoly6(interp45Deg, NVI(pphase_p45 * 64)) * w.z; } if (w.w > 0.0f) { //135 deg filter NVF pphase_b135 = 0.5f * (phase_x_frac + phase_y_frac); NVF temp_interp135Deg[7]; temp_interp135Deg[1] = lerp(p[3][1], p[4][2], pphase_b135); temp_interp135Deg[3] = lerp(p[2][2], p[3][3], pphase_b135); temp_interp135Deg[5] = lerp(p[1][3], p[2][4], pphase_b135); { pphase_b135 = pphase_b135 - 0.5f; NVF a = (pphase_b135 >= 0.f) ? p[5][2] : p[3][0]; NVF b = (pphase_b135 >= 0.f) ? p[4][3] : p[2][1]; NVF c = (pphase_b135 >= 0.f) ? p[3][4] : p[1][2]; NVF d = (pphase_b135 >= 0.f) ? p[2][5] : p[0][3]; temp_interp135Deg[0] = lerp(p[4][1], a, abs(pphase_b135)); temp_interp135Deg[2] = lerp(p[3][2], b, abs(pphase_b135)); temp_interp135Deg[4] = lerp(p[2][3], c, abs(pphase_b135)); temp_interp135Deg[6] = lerp(p[1][4], d, abs(pphase_b135)); } NVF interp135Deg[6]; NVF pphase_p135 = 1 + (phase_x_frac - phase_y_frac); if (pphase_p135 >= 1) { NIS_UNROLL for (NVI i = 0; i < 6; ++i) { interp135Deg[i] = temp_interp135Deg[i + 1]; } pphase_p135 = pphase_p135 - 1; } else { NIS_UNROLL for (NVI i = 0; i < 6; ++i) { interp135Deg[i] = temp_interp135Deg[i]; } } f += EvalPoly6(interp135Deg, NVI(pphase_p135 * 64)) * w.w; } return f; } //----------------------------------------------------------------------------------------------- // NVScaler //----------------------------------------------------------------------------------------------- void NVScaler(NVU2 blockIdx, NVU threadIdx) { // Figure out the range of pixels from input image that would be needed to be loaded for this thread-block NVI dstBlockX = NVI(NIS_BLOCK_WIDTH * blockIdx.x); NVI dstBlockY = NVI(NIS_BLOCK_HEIGHT * blockIdx.y); const NVI srcBlockStartX = NVI(floor((dstBlockX + 0.5f) * kScaleX - 0.5f)); const NVI srcBlockStartY = NVI(floor((dstBlockY + 0.5f) * kScaleY - 0.5f)); const NVI srcBlockEndX = NVI(ceil((dstBlockX + NIS_BLOCK_WIDTH + 0.5f) * kScaleX - 0.5f)); const NVI srcBlockEndY = NVI(ceil((dstBlockY + NIS_BLOCK_HEIGHT + 0.5f) * kScaleY - 0.5f)); NVI numTilePixelsX = srcBlockEndX - srcBlockStartX + kSupportSize - 1; NVI numTilePixelsY = srcBlockEndY - srcBlockStartY + kSupportSize - 1; // round-up load region to even size since we're loading in 2x2 batches numTilePixelsX += numTilePixelsX & 0x1; numTilePixelsY += numTilePixelsY & 0x1; const NVI numTilePixels = numTilePixelsX * numTilePixelsY; // calculate the equivalent values for the edge map const NVI numEdgeMapPixelsX = numTilePixelsX - kSupportSize + 2; const NVI numEdgeMapPixelsY = numTilePixelsY - kSupportSize + 2; const NVI numEdgeMapPixels = numEdgeMapPixelsX * numEdgeMapPixelsY; // fill in input luma tile (shPixelsY) in batches of 2x2 pixels // we use texture gather to get extra support necessary // to compute 2x2 edge map outputs too { for (NVU i = threadIdx * 2; i < NVU(numTilePixels) >> 1; i += NIS_THREAD_GROUP_SIZE * 2) { NVU py = (i / numTilePixelsX) * 2; NVU px = i % numTilePixelsX; // 0.5 to be in the center of texel // - (kSupportSize - 1) / 2 to shift by the kernel support size NVF kShift = 0.5f - (kSupportSize - 1) / 2; #if NIS_VIEWPORT_SUPPORT const NVF tx = (srcBlockStartX + px + kInputViewportOriginX + kShift) * kSrcNormX; const NVF ty = (srcBlockStartY + py + kInputViewportOriginY + kShift) * kSrcNormY; #else const NVF tx = (srcBlockStartX + px + kShift) * kSrcNormX; const NVF ty = (srcBlockStartY + py + kShift) * kSrcNormY; #endif NVF p[2][2]; #if NIS_TEXTURE_GATHER { const NVF4 sr = NVTEX_SAMPLE_RED(in_texture, samplerLinearClamp, NVF2(tx, ty)); const NVF4 sg = NVTEX_SAMPLE_GREEN(in_texture, samplerLinearClamp, NVF2(tx, ty)); const NVF4 sb = NVTEX_SAMPLE_BLUE(in_texture, samplerLinearClamp, NVF2(tx, ty)); p[0][0] = getY(NVF3(sr.w, sg.w, sb.w)); p[0][1] = getY(NVF3(sr.z, sg.z, sb.z)); p[1][0] = getY(NVF3(sr.x, sg.x, sb.x)); p[1][1] = getY(NVF3(sr.y, sg.y, sb.y)); } #else NIS_UNROLL_INNER for (NVI j = 0; j < 2; j++) { NIS_UNROLL_INNER for (NVI k = 0; k < 2; k++) { #if NIS_NV12_SUPPORT p[j][k] = NVTEX_SAMPLE(in_texture_y, samplerLinearClamp, NVF2(tx + k * kSrcNormX, ty + j * kSrcNormY)); #else const NVF4 px = NVTEX_SAMPLE(in_texture, samplerLinearClamp, NVF2(tx + k * kSrcNormX, ty + j * kSrcNormY)); p[j][k] = getY(px.xyz); #endif } } #endif const NVU idx = py * kTilePitch + px; shPixelsY[idx] = NVH(p[0][0]); shPixelsY[idx + 1] = NVH(p[0][1]); shPixelsY[idx + kTilePitch] = NVH(p[1][0]); shPixelsY[idx + kTilePitch + 1] = NVH(p[1][1]); } } GroupMemoryBarrierWithGroupSync(); { // fill in the edge map of 2x2 pixels for (NVU i = threadIdx * 2; i < NVU(numEdgeMapPixels) >> 1; i += NIS_THREAD_GROUP_SIZE * 2) { NVU py = (i / numEdgeMapPixelsX) * 2; NVU px = i % numEdgeMapPixelsX; const NVU edgeMapIdx = py * kEdgeMapPitch + px; NVU tileCornerIdx = (py + 1) * kTilePitch + px + 1; NVF p[4][4]; NIS_UNROLL_INNER for (NVI j = 0; j < 4; j++) { NIS_UNROLL_INNER for (NVI k = 0; k < 4; k++) { p[j][k] = shPixelsY[tileCornerIdx + j * kTilePitch + k]; } } shEdgeMap[edgeMapIdx] = NVH4(GetEdgeMap(p, 0, 0)); shEdgeMap[edgeMapIdx + 1] = NVH4(GetEdgeMap(p, 0, 1)); shEdgeMap[edgeMapIdx + kEdgeMapPitch] = NVH4(GetEdgeMap(p, 1, 0)); shEdgeMap[edgeMapIdx + kEdgeMapPitch + 1] = NVH4(GetEdgeMap(p, 1, 1)); } } LoadFilterBanksSh(NVI(threadIdx)); GroupMemoryBarrierWithGroupSync(); // output coord within a tile const NVI2 pos = NVI2(NVU(threadIdx) % NVU(NIS_BLOCK_WIDTH), NVU(threadIdx) / NVU(NIS_BLOCK_WIDTH)); // x coord inside the output image const NVI dstX = dstBlockX + pos.x; // x coord inside the input image const NVF srcX = (0.5f + dstX) * kScaleX - 0.5f; // nearest integer part const NVI px = NVI(floor(srcX) - srcBlockStartX); // fractional part const NVF fx = srcX - floor(srcX); // discretized phase const NVI fx_int = NVI(fx * kPhaseCount); #if NIS_VIEWPORT_SUPPORT if (NVU(srcX) > kInputViewportWidth || NVU(dstX) > kOutputViewportWidth) { return; } #endif for (NVI k = 0; k < NIS_BLOCK_WIDTH * NIS_BLOCK_HEIGHT / NIS_THREAD_GROUP_SIZE; ++k) { // y coord inside the output image const NVI dstY = dstBlockY + pos.y + k * (NIS_THREAD_GROUP_SIZE / NIS_BLOCK_WIDTH); // y coord inside the input image const NVF srcY = (0.5f + dstY) * kScaleY - 0.5f; #if NIS_VIEWPORT_SUPPORT if (!(NVU(srcY) > kInputViewportHeight || NVU(dstY) > kOutputViewportHeight)) #endif { // nearest integer part const NVI py = NVI(floor(srcY) - srcBlockStartY); // fractional part const NVF fy = srcY - floor(srcY); // discretized phase const NVI fy_int = NVI(fy * kPhaseCount); // generate weights for directional filters const NVI startEdgeMapIdx = py * kEdgeMapPitch + px; NVF4 edge[2][2]; NIS_UNROLL for (NVI i = 0; i < 2; i++) { NIS_UNROLL for (NVI j = 0; j < 2; j++) { // need to shift edge map sampling since it's a 2x2 centered inside 6x6 grid edge[i][j] = shEdgeMap[startEdgeMapIdx + (i * kEdgeMapPitch) + j]; } } const NVF4 w = GetInterpEdgeMap(edge, fx, fy) * NIS_SCALE_INT; // load 6x6 support to regs const NVI startTileIdx = py * kTilePitch + px; NVF p[6][6]; { NIS_UNROLL for (NVI i = 0; i < 6; ++i) { NIS_UNROLL for (NVI j = 0; j < 6; ++j) { p[i][j] = shPixelsY[startTileIdx + i * kTilePitch + j]; } } } // weigth for luma const NVF baseWeight = NIS_SCALE_FLOAT - w.x - w.y - w.z - w.w; // final luma is a weighted product of directional & normal filters NVF opY = 0; // get traditional scaler filter output opY += FilterNormal(p, fx_int, fy_int) * baseWeight; // get directional filter bank output opY += AddDirFilters(p, fx, fy, fx_int, fy_int, w); #if NIS_VIEWPORT_SUPPORT NVF2 coord = NVF2((srcX + kInputViewportOriginX + 0.5f) * kSrcNormX, (srcY + kInputViewportOriginY + 0.5f) * kSrcNormY); NVF2 dstCoord = NVF2(dstX + kOutputViewportOriginX, dstY + kOutputViewportOriginY); #else NVF2 coord = NVF2((srcX + 0.5f) * kSrcNormX, (srcY + 0.5f) * kSrcNormY); NVF2 dstCoord = NVF2(dstX, dstY); #endif // do bilinear tap for chroma upscaling #if NIS_NV12_SUPPORT NVF y = NVTEX_SAMPLE(in_texture_y, samplerLinearClamp, coord); NVF2 uv = NVTEX_SAMPLE(in_texture_uv, samplerLinearClamp, coord); NVF4 op = NVF4(YUVtoRGB(NVF3(y, uv)), 1.0f); #else NVF4 op = NVTEX_SAMPLE(in_texture, samplerLinearClamp, coord); NVF y = getY(NVF3(op.x, op.y, op.z)); #endif #if NIS_HDR_MODE == NIS_HDR_MODE_LINEAR const NVF kEps = 1e-4f; const NVF kNorm = 1.0f / (NIS_SCALE_FLOAT * kHDRCompressionFactor); const NVF opYN = max(opY, 0.0f) * kNorm; const NVF corr = (opYN * opYN + kEps) / (max(getYLinear(NVF3(op.x, op.y, op.z)), 0.0f) + kEps); op.x *= corr; op.y *= corr; op.z *= corr; #else const NVF corr = opY * (1.0f / NIS_SCALE_FLOAT) - y; op.x += corr; op.y += corr; op.z += corr; #endif NVTEX_STORE(out_texture, dstCoord, NVCLAMP(op)); } } } #else #ifndef NIS_BLOCK_WIDTH #define NIS_BLOCK_WIDTH 32 #endif #ifndef NIS_BLOCK_HEIGHT #define NIS_BLOCK_HEIGHT 32 #endif #ifndef NIS_THREAD_GROUP_SIZE #define NIS_THREAD_GROUP_SIZE 256 #endif #define kSupportSize 5 #define kNumPixelsX (NIS_BLOCK_WIDTH + kSupportSize + 1) #define kNumPixelsY (NIS_BLOCK_HEIGHT + kSupportSize + 1) NVSHARED NVF shPixelsY[kNumPixelsY][kNumPixelsX]; NVF CalcLTIFast(const NVF y[5]) { const NVF a_min = min(min(y[0], y[1]), y[2]); const NVF a_max = max(max(y[0], y[1]), y[2]); const NVF b_min = min(min(y[2], y[3]), y[4]); const NVF b_max = max(max(y[2], y[3]), y[4]); const NVF a_cont = a_max - a_min; const NVF b_cont = b_max - b_min; const NVF cont_ratio = max(a_cont, b_cont) / (min(a_cont, b_cont) + kEps); return (1.0f - saturate((cont_ratio - kMinContrastRatio) * kRatioNorm)) * kContrastBoost; } NVF EvalUSM(const NVF pxl[5], const NVF sharpnessStrength, const NVF sharpnessLimit) { // USM profile NVF y_usm = -0.6001f * pxl[1] + 1.2002f * pxl[2] - 0.6001f * pxl[3]; // boost USM profile y_usm *= sharpnessStrength; // clamp to the limit y_usm = min(sharpnessLimit, max(-sharpnessLimit, y_usm)); // reduce ringing y_usm *= CalcLTIFast(pxl); return y_usm; } NVF4 GetDirUSM(const NVF p[5][5]) { // sharpness boost & limit are the same for all directions const NVF scaleY = 1.0f - saturate((p[2][2] - kSharpStartY) * kSharpScaleY); // scale the ramp to sharpen as a function of luma const NVF sharpnessStrength = scaleY * kSharpStrengthScale + kSharpStrengthMin; // scale the ramp to limit USM as a function of luma const NVF sharpnessLimit = (scaleY * kSharpLimitScale + kSharpLimitMin) * p[2][2]; NVF4 rval; // 0 deg filter NVF interp0Deg[5]; { for (NVI i = 0; i < 5; ++i) { interp0Deg[i] = p[i][2]; } } rval.x = EvalUSM(interp0Deg, sharpnessStrength, sharpnessLimit); // 90 deg filter NVF interp90Deg[5]; { for (NVI i = 0; i < 5; ++i) { interp90Deg[i] = p[2][i]; } } rval.y = EvalUSM(interp90Deg, sharpnessStrength, sharpnessLimit); //45 deg filter NVF interp45Deg[5]; interp45Deg[0] = p[1][1]; interp45Deg[1] = lerp(p[2][1], p[1][2], 0.5f); interp45Deg[2] = p[2][2]; interp45Deg[3] = lerp(p[3][2], p[2][3], 0.5f); interp45Deg[4] = p[3][3]; rval.z = EvalUSM(interp45Deg, sharpnessStrength, sharpnessLimit); //135 deg filter NVF interp135Deg[5]; interp135Deg[0] = p[3][1]; interp135Deg[1] = lerp(p[3][2], p[2][1], 0.5f); interp135Deg[2] = p[2][2]; interp135Deg[3] = lerp(p[2][3], p[1][2], 0.5f); interp135Deg[4] = p[1][3]; rval.w = EvalUSM(interp135Deg, sharpnessStrength, sharpnessLimit); return rval; } //----------------------------------------------------------------------------------------------- // NVSharpen //----------------------------------------------------------------------------------------------- void NVSharpen(NVU2 blockIdx, NVU threadIdx) { const NVI dstBlockX = NVI(NIS_BLOCK_WIDTH * blockIdx.x); const NVI dstBlockY = NVI(NIS_BLOCK_HEIGHT * blockIdx.y); // fill in input luma tile in batches of 2x2 pixels // we use texture gather to get extra support necessary // to compute 2x2 edge map outputs too const NVF kShift = 0.5f - kSupportSize / 2; for (NVI i = NVI(threadIdx) * 2; i < kNumPixelsX * kNumPixelsY / 2; i += NIS_THREAD_GROUP_SIZE * 2) { NVU2 pos = NVU2(NVU(i) % NVU(kNumPixelsX), NVU(i) / NVU(kNumPixelsX) * 2); NIS_UNROLL for (NVI dy = 0; dy < 2; dy++) { NIS_UNROLL for (NVI dx = 0; dx < 2; dx++) { #if NIS_VIEWPORT_SUPPORT const NVF tx = (dstBlockX + pos.x + kInputViewportOriginX + dx + kShift) * kSrcNormX; const NVF ty = (dstBlockY + pos.y + kInputViewportOriginY + dy + kShift) * kSrcNormY; #else const NVF tx = (dstBlockX + pos.x + dx + kShift) * kSrcNormX; const NVF ty = (dstBlockY + pos.y + dy + kShift) * kSrcNormY; #endif #if NIS_NV12_SUPPORT shPixelsY[pos.y + dy][pos.x + dx] = NVTEX_SAMPLE(in_texture_y, samplerLinearClamp, NVF2(tx, ty)); #else const NVF4 px = NVTEX_SAMPLE(in_texture, samplerLinearClamp, NVF2(tx, ty)); shPixelsY[pos.y + dy][pos.x + dx] = getY(px.xyz); #endif } } } GroupMemoryBarrierWithGroupSync(); for (NVI k = NVI(threadIdx); k < NIS_BLOCK_WIDTH * NIS_BLOCK_HEIGHT; k += NIS_THREAD_GROUP_SIZE) { const NVI2 pos = NVI2(NVU(k) % NVU(NIS_BLOCK_WIDTH), NVU(k) / NVU(NIS_BLOCK_WIDTH)); // load 5x5 support to regs NVF p[5][5]; NIS_UNROLL for (NVI i = 0; i < 5; ++i) { NIS_UNROLL for (NVI j = 0; j < 5; ++j) { p[i][j] = shPixelsY[pos.y + i][pos.x + j]; } } // get directional filter bank output NVF4 dirUSM = GetDirUSM(p); // generate weights for directional filters NVF4 w = GetEdgeMap(p, kSupportSize / 2 - 1, kSupportSize / 2 - 1); // final USM is a weighted sum filter outputs const NVF usmY = (dirUSM.x * w.x + dirUSM.y * w.y + dirUSM.z * w.z + dirUSM.w * w.w); // do bilinear tap and correct rgb texel so it produces new sharpened luma const NVI dstX = dstBlockX + pos.x; const NVI dstY = dstBlockY + pos.y; #if NIS_VIEWPORT_SUPPORT NVF2 coord = NVF2((dstX + kInputViewportOriginX + 0.5f) * kSrcNormX, (dstY + kInputViewportOriginY + 0.5f) * kSrcNormY); NVF2 dstCoord = NVF2(dstX + kOutputViewportOriginX, dstY + kOutputViewportOriginY); if (!(NVU(dstX) > kOutputViewportWidth || NVU(dstY) > kOutputViewportHeight)) #else NVF2 coord = NVF2((dstX + 0.5f) * kSrcNormX, (dstY + 0.5f) * kSrcNormY); NVF2 dstCoord = NVF2(dstX, dstY); #endif { #if NIS_NV12_SUPPORT NVF y = NVTEX_SAMPLE(in_texture_y, samplerLinearClamp, coord); NVF2 uv = NVTEX_SAMPLE(in_texture_uv, samplerLinearClamp, coord); NVF4 op = NVF4(YUVtoRGB(NVF3(y, uv)), 1.0f); #else NVF4 op = NVTEX_SAMPLE(in_texture, samplerLinearClamp, coord); #endif #if NIS_HDR_MODE == NIS_HDR_MODE_LINEAR const NVF kEps = 1e-4f * kHDRCompressionFactor * kHDRCompressionFactor; NVF newY = p[2][2] + usmY; newY = max(newY, 0.0f); const NVF oldY = p[2][2]; const NVF corr = (newY * newY + kEps) / (oldY * oldY + kEps); op.x *= corr; op.y *= corr; op.z *= corr; #else op.x += usmY; op.y += usmY; op.z += usmY; #endif NVTEX_STORE(out_texture, dstCoord, NVCLAMP(op)); } } } #endifValveSoftware-gamescope-eb620ab/src/shaders/NVIDIAImageScaling/README.md000066400000000000000000000400451502457270500256550ustar00rootroot00000000000000# NVIDIA Image Scaling SDK v1.0.3 The MIT License(MIT) Copyright(c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions : The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## Introduction The NVIDIA Image Scaling SDK provides a single spatial scaling and sharpening algorithm for cross-platform support. The scaling algorithm uses a 6-tap scaling filter combined with 4 directional scaling and adaptive sharpening filters, which creates nice smooth images and sharp edges. In addition, the SDK provides a state-of-the-art adaptive directional sharpening algorithm for use in applications where no scaling is required.\ The directional scaling and sharpening algorithm is named NVScaler while the adaptive-directional-sharpening-only algorithm is named NVSharpen. Both algorithms are provided as compute shaders and developers are free to integrate them in their applications. Note that if you integrate NVScaler, you should NOT integrate NVSharpen, as NVScaler already includes a sharpening pass ## Pipeline Placement The call into the NVIDIA Image Scaling shaders must occur during the post-processing phase after tone-mapping. Applying the scaling in linear HDR in-game color-space may result in a sharpening effect that is either not visible or too strong. Since sharpening algorithms can enhance noisy or grainy regions, it is recommended that certain effects such as film grain should occur after NVScaler or NVSharpen. Low-pass filters such as motion blur or light bloom are recommended to be applied before NVScaler or NVSharpen to avoid sharpening attenuation. ## Color Space and Ranges NVIDIA Image Scaling shaders can process color textures stored as either LDR or HDR with the following restrictions: 1) LDR - The range of color values must be in the [0, 1] range - The input color texture must be in display-referred color-space after tone mapping and OETF (gamma-correction) has been applied 2) HDR PQ - The range of color values must be in the [0, 1] range - The input color texture must be in display-referred color-space after tone mapping with Rec.2020 PQ OETF applied 3) HDR Linear - The recommended range of color values is [0, 12.5], where luminance value (as per BT. 709) of 1.0 maps to brightness value of 80nits (sRGB peak) and 12.5 maps to 1000nits - The input color texture may have luminance values that are either linear and scene-referred or linear and display-referred (after tone mapping) If the input color texture sent to NVScaler/NVSharpen is in HDR format set NIS_HDR_MODE define to either NIS_HDR_MODE_LINEAR (1) or NIS_HDR_MODE_PQ (2). ## Supported Texture Formats ### Input and output formats: Input and output formats are expected to be in the rages defined in previous section and should be specified using non-integer data types such as DXGI_FORMAT_R8G8B8A8_UNORM or DXGI_FORMAT_NV12. ### Coefficients formats: The scaler coefficients and USM coefficients format should be specified using float4 type such as DXGI_FORMAT_R32G32B32A32_FLOAT or DXGI_FORMAT_R16G16B16A16_FLOAT. The coefficients are included in NIS_Config.h file: fp32 format: coef_scaler, coef_USM fp16 format: coef_scaler_fp16, coef_USM_fp16 ### Resource States, Buffers, and Sampler: The game or application calling NVIDIA Image Scaling SDK shaders must ensure that the textures are in the correct state. - Input color textures must be in pixel shader read state. Shader Resource View (SRV) in DirectX - The output texture must be in read/write state. Unordered Access View (UAV) in DirectX - The coefficients texture for NVScaler must be in read state. Shader Resource View (SRV) in DirectX - The configuration variables must be passed as constant buffer. Constant Buffer View (CBV) in DirectX - The sampler for texture pixel sampling must use linear filter interpolation and clamp to edge addressing mode ## Adding NVIDIA Image Scaling SDK to a Project Include NIS_Scaler.h directly in your application or alternative use the provided NIS_Main.hlsl or NIS_Main.glsl shader files. Use NIS_Config.h to get the ideal shader dispatch values for your platform, to configure the algorithm constant values (NVScalerUpdateConfig, and NVSharpenUpdateConfig), and to access the algorithm coefficients (coef_scale, coef_USM, coef_scale_fp16, coef_USM_fp16). - Device\ NIS_Scaler.h : HLSL shader file\ NIS_Main.hlsl : Main HLSL shader example (can be replaced by your own) \ NIS_Main.glsl : Main GLSL shader example (can ge replaced by your own) - Host Configuration\ NIS_Config.h : Configuration structure ### Defines: **NIS_SCALER**: default (**1**) NVScaler, (0) fast NVSharpen only, no upscaling\ **NIS_HDR_MODE**: default(**0**) disabled, (1) Linear, (2) PQ\ **NIS_BLOCK_WIDTH**: pixels per block width. Use GetOptimalBlockWidth query for your platform\ **NIS_BLOCK_HEIGHT**: pixels per block height. Use GetOptimalBlockHeight query for your platform\ **NIS_THREAD_GROUP_SIZE**: number of threads per group. Use GetOptimalThreadGroupSize query for your platform\ **NIS_USE_HALF_PRECISION**: default(**0**) disabled, (1) enable half pression computation\ **NIS_HLSL**: default (**1**) enabled, (0) disabled\ **NIS_HLSL_6_2**: default (**0**) HLSL v5, (1) HLSL v6.2 forces NIS_HLSL=1\ **NIS_GLSL**: default (**0**) disabled, (1) enabled\ **NIS_VIEWPORT_SUPPORT**: default(**0**) disabled, (1) enable input/output viewport support\ **NIS_NV12_SUPPORT**: default(**0**) disabled, (1) enable NV12 input\ **NIS_CLAMP_OUTPUT**: default(**0**) disabled, (1) enable output clamp *Default NVScaler shader constants:* [**NIS_BLOCK_WIDTH**, **NIS_BLOCK_HEIGHT**, **NIS_THREAD_GROUP_SIZE**] = [32, 24, 256] *Default NVSharpen shader constants:* [**NIS_BLOCK_WIDTH**, **NIS_BLOCK_HEIGHT**, **NIS_THREAD_GROUP_SIZE**] = [32, 32, 256] *Defines for HLSL with DXC bindings:* **NIS_DXC**: (0) disabled, (1) enable HLSL DXC Vulkan support ## Optimal shader settings To get optimal performance of NVScaler and NVSharpen for current and future hardware, it is recommended that the following API is used to obtain the values for NIS_BLOCK_WIDTH, NIS_BLOCK_HEIGHT, and NIS_THREAD_GROUP_SIZE. These values can be used to compile permutations of NVScaler and NVSharpen offline. ``` enum class NISGPUArchitecture : uint32_t { NVIDIA_Generic = 0, AMD_Generic = 1, Intel_Generic = 2, NVIDIA_Generic_fp16 = 3, }; ``` ``` struct NISOptimizer { bool isUpscaling; NISGPUArchitecture gpuArch; NISOptimizer(bool isUpscaling = true, NISGPUArchitecture gpuArch = NISGPUArchitecture::NVIDIA_Generic); uint32_t GetOptimalBlockWidth(); uint32_t GetOptimalBlockHeight(); uint32_t GetOptimalThreadGroupSize(); }; ``` ## HDR shader settings Use the following enum values for setting NIS_HDR_MODE ``` enum class NISHDRMode : uint32_t { None = 0, Linear = 1, PQ = 2 }; ``` ## Integration of NVScaler The integration instructions in this section can be applied with minimal changes to your own DX11, DX12, or Vulkan application, using HLSL or GLSL. ### Compile the NIS_Main.hlsl shader NIS_SCALER should be set to 1, and the isUpscaling argument should set to true. ``` bool isUpscaling = true; // Note: NISOptimizer is optional and these values can be cached offline NISOptimizer opt(isUpscaling, NISGPUArchitecture::NVIDIA_Generic); uint32_t blockWidth = opt.GetOptimalBlockWidth(); uint32_t blockHeight = opt.GetOptimalBlockHeight(); uint32_t threadGroupSize = opt.GetOptimalThreadGroupSize(); Defines defines; defines.add("NIS_SCALER", isUpscaling); defines.add("NIS_HDR_MODE", hdrMode); defines.add("NIS_BLOCK_WIDTH", blockWidth); defines.add("NIS_BLOCK_HEIGHT", blockHeight); defines.add("NIS_THREAD_GROUP_SIZE", threadGroupSize); NVScalerCS = CompileComputeShader(device, "NIS_Main.hlsl”, &defines); ``` ### Create NVIDIA Image Scaling SDK configuration constant buffer ``` struct NISConfig { float kDetectRatio; float kDetectThres; float kMinContrastRatio; float kRatioNorm; ... }; NISConfig config; createConstBuffer(&config, &csBuffer); ``` ### Create SRV textures for the scaler and USM phase coefficients ``` const int rowPitch = kFilterSize * sizeof(float); // use for fp32: float, fp16: uint16_t const int coeffSize = rowPitch * kPhaseCount; // Since we are using RGBA format the texture width = kFilterSize / 4 createTexture2D(kFilterSize / 4, kPhaseCount, DXGI_FORMAT_R32G32B32A32_FLOAT, D3D11_USAGE_DEFAULT, coef_scaler, rowPitch, coeffSize, &scalerTex); createTexture2D(kFilterSize / 4, kPhaseCount, DXGI_FORMAT_R32G32B32A32_FLOAT, D3D11_USAGE_DEFAULT, coef_usm, rowPitch, coeffSize, &usmTex); createSRV(scalerTex.Get(), DXGI_FORMAT_R32G32B32A32_FLOAT, &scalerSRV); createSRV(usmTex.Get(), DXGI_FORMAT_R32G32B32A32_FLOAT, &usmSRV); ``` ### Create Sampler ``` createLinearClampSampler(&linearClampSampler); ``` ### Update NVIDIA Image Scaling SDK NVScaler configuration and constant buffer Use the following API call to update the NVIDIA Image Scaling SDK configuration ``` void NVScalerUpdateConfig(NISConfig& config, float sharpness, uint32_t inputViewportOriginX, uint32_t inputViewportOriginY, uint32_t inputViewportWidth, uint32_t inputViewportHeight, uint32_t inputTextureWidth, uint32_t inputTextureHeight, uint32_t outputViewportOriginX, uint32_t outputViewportOriginY, uint32_t outputViewportWidth, uint32_t outputViewportHeight, uint32_t outputTextureWidth, uint32_t outputTextureHeight, NISHDRMode hdrMode = NISHDRMode::None ); ``` Update the constant buffer whenever the input size, sharpness, or scale changes ``` NVScalerUpdateConfig(m_config, sharpness, 0, 0, inputWidth, inputHeight, inputWidth, inputHeight, 0, 0, outputWidth, outputHeight, outputWidth, outputHeight, NISHDRMode::None); updateConstBuffer(&config, csBuffer.Get()); ``` ### A simple DX11 NVScaler dispatch example ``` context->CSSetShaderResources(0, 1, input); // SRV context->CSSetShaderResource (1, 1, scalerSRV.GetAddressOf()); context->CSSetShaderResource (2, 1, usmSRV.GetAddressOf()); context->CSSetUnorderedAccessViews(0, 1, output, nullptr); context->CSSetSamplers(0, 1, linearClampSampler.GetAddressOf()); context->CSSetConstantBuffers(0, 1, csBuffer.GetAddressOf()); context->CSSetShader(NVScalerCS.Get(), nullptr, 0); context->Dispatch(UINT(std::ceil(outputWidth / float(blockWidth))), UINT(std::ceil(outputHeight / float(blockHeight))), 1); ``` ## Integration of NVSharpen If your application requires upscaling and sharpening do not use NVSharpen use NVScaler instead. Since NVScaler performs both operations, upscaling and sharpening, in one step, it performs faster and produces better image quality. ### Compile the NIS_Main.hlsl shader NIS_SCALER should be set to 0 and the optimizer isUpscaling argument should be set to false. ``` bool isUpscaling = false; // Note: NISOptimizer is optional and these values can be cached offline NISOptimizer opt(isUpscaling, NISGPUArchitecture::NVIDIA_Generic); uint32_t blockWidth = opt.GetOptimalBlockWidth(); uint32_t blockHeight = opt.GetOptimalBlockHeight(); uint32_t threadGroupSize = opt.GetOptimalThreadGroupSize(); Defines defines; defines.add("NIS_DIRSCALER", isUpscaling); defines.add("NIS_HDR_MODE", hdrMode); defines.add("NIS_BLOCK_WIDTH", blockWidth); defines.add("NIS_BLOCK_HEIGHT", blockHeight); defines.add("NIS_THREAD_GROUP_SIZE", threadGroupSize); NVSharpenCS = CompileComputeShader(device, "NIS_Main.hlsl”, &defines); ``` ### Create NVIDIA Image Scaling SDK NVSharpen configuration constant buffer ``` struct NISConfig { float kDetectRatio; float kDetectThres; float kMinContrastRatio; float kRatioNorm; ... }; NISConfig config; createConstBuffer(&config, &csBuffer); ``` ### Create Sampler ``` createLinearClampSampler(&linearClampSampler); ``` ### Update NVIDIA Image Scaling SDK NVSharpen configuration and constant buffer Use the following API call to update the NVIDIA Image Scaling SDK configuration. Since NVSharpen is a sharpening algorithm only the sharpness and input size are required. For upscaling with sharpening use NVScaler since it performs both operations at the same time. ``` void NVSharpenUpdateConfig(NISConfig& config, float sharpness, uint32_t inputViewportOriginX, uint32_t inputViewportOriginY, uint32_t inputViewportWidth, uint32_t inputViewportHeight, uint32_t inputTextureWidth, uint32_t inputTextureHeight, uint32_t outputViewportOriginX, uint32_t outputViewportOriginY, NISHDRMode hdrMode = NISHDRMode::None ); ``` Update the constant buffer whenever the input size or sharpness changes. ``` NVSharpenUpdateConfig(m_config, sharpness, 0, 0, inputWidth, inputHeight, inputWidth, inputHeight, 0, 0, NISHDRMode::None); updateConstBuffer(&config, csBuffer.Get()); ``` ### A simple DX11 NVSharpen dispatch example ``` context->CSSetShaderResources(0, 1, input); context->CSSetUnorderedAccessViews(0, 1, output, nullptr); context->CSSetSamplers(0, 1, linearClampSampler.GetAddressOf()); context->CSSetConstantBuffers(0, 1, csBuffer.GetAddressOf()); context->CSSetShader(NVSharpenCS.Get(), nullptr, 0); context->Dispatch(UINT(std::ceil(outputWidth / float(blockWidth))), UINT(std::ceil(outputHeight / float(blockHeight))), 1); ``` ### Streamline Nvidia Image Scaling Plug-in Integration Streamline is an open-source solution that facilitates the integration of the latest NVIDIA and other independent hardware vendors super resolution technologies into applications and games. For a high-level overview of NIVIDA Streamline, visit NVIDIA Developer Streamline page https://developer.nvidia.com/rtx/streamline. \ The Streamline SDK can be found here NVIDIA Streamline Github Page https://github.com/NVIDIAGameWorks/Streamline. \ For Streamline NIS plug-in integration instructions and documentation, see the Streamline NIS Programming Guide https://github.com/NVIDIAGameWorks/Streamline/blob/main/docs/ProgrammingGuideNIS.md ## Samples ### Dependencies - Visual Studio 2019 : https://visualstudio.microsoft.com/downloads/ - Windows 10 SDK : https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk - CMake 3.16 : https://cmake.org/download/ for building the Vulkan sample: - Vulkan SDK 1.2.189.2 : https://vulkan.lunarg.com/ ### Build For the DirectX11 and DirectX12 samples use the following: ``` $> cd samples $> mkdir build $> cd build $> cmake .. ``` For building the Vulkan sample add the following command line option: ``` $> cmake .. -DNIS_VK_SAMPLE=ON ``` For building the NV12 sample add the following command line option ``` $> cmake .. -DNIS_NV12_SAMPLE=ON ``` For building the Streamline sample, first checkout and build the NVIDA Streamline SDK. Specify your Visual Studio version (default vs2017) ``` $> cd samples\third_party $> setup_streamline.bat vs2019 ``` After the Streamline build process is completed then add the following command line option to the cmake command. Note that you can also build other samples by concatenating command line options ``` $> cd samples $> cmake .. -DNIS_SL_SAMPLE=ON ``` Open the solution with Visual Studio 2019. Right-click the sample project and select "Set as Startup Project" before building the project. For Linux, only the VK sample will be generated.ValveSoftware-gamescope-eb620ab/src/shaders/NVIDIAImageScaling/licence.txt000066400000000000000000000022321502457270500265350ustar00rootroot00000000000000// The MIT License(MIT) // // Copyright(c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files(the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions : // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.ValveSoftware-gamescope-eb620ab/src/shaders/alphamode.h000066400000000000000000000026361502457270500232670ustar00rootroot00000000000000#ifndef ALPHAMODE_H #define ALPHAMODE_H const int alpha_mode_premult = 0; const int alpha_mode_coverage = 1; const int alpha_mode_none = 2; const int alpha_mode_max_bits = 4; uint get_layer_alphamode(uint layerIdx) { return bitfieldExtract(u_alphaMode, int(layerIdx) * alpha_mode_max_bits, alpha_mode_max_bits); } vec4 BlendLayer( uint layerIdx, vec4 outputValue, vec4 layerColor, float opacity ) { float layerAlpha = opacity * layerColor.a; uint alphaMode = get_layer_alphamode( layerIdx ); if ( alphaMode == alpha_mode_premult ) { // wl_surfaces come with premultiplied alpha, so that's them being // premultiplied by layerColor.a. // We need to then multiply that by the layer's opacity to get to our // final premultiplied state. // For the other side of things, we need to multiply by (1.0f - (layerColor.a * opacity)) outputValue = layerColor * opacity + outputValue * (1.0f - layerAlpha); } else if ( alphaMode == alpha_mode_coverage ) // coverage for accessibility looks { outputValue = layerColor * layerAlpha + outputValue * (1.0f - layerAlpha); } else // none { outputValue = layerColor * opacity; } return outputValue; } vec3 BlendLayer( uint layerIdx, vec3 outputValue, vec4 layerColor, float opacity ) { return BlendLayer( layerIdx, vec4( outputValue, 1 ), layerColor, opacity ).rgb; } #endifValveSoftware-gamescope-eb620ab/src/shaders/blit_push_data.h000066400000000000000000000007011502457270500243060ustar00rootroot00000000000000layout(binding = 0, scalar) uniform layers_t { vec2 u_scale[VKR_MAX_LAYERS]; vec2 u_offset[VKR_MAX_LAYERS]; float u_opacity[VKR_MAX_LAYERS]; mat3x4 u_ctm[VKR_MAX_LAYERS]; uint u_borderMask; uint u_frameId; uint u_blur_radius; uint u_shaderFilter; uint u_alphaMode; // hdr float u_linearToNits; // sdr -> hdr float u_nitsToLinear; // hdr -> sdr float u_itmSdrNits; float u_itmTargetNits; }; ValveSoftware-gamescope-eb620ab/src/shaders/blur.h000066400000000000000000000360321502457270500222760ustar00rootroot00000000000000 vec4 textureCond(sampler2D layerSampler, uint layerIdx, vec2 pos, bool unnormalized) { vec2 texSize = textureSize(layerSampler, 0); vec2 coord = pos; #ifndef BLUR_DONT_SCALE coord = ((coord + u_offset[layerIdx]) * u_scale[layerIdx]); if (coord.x < 0.0f || coord.y < 0.0f || coord.x >= texSize.x || coord.y >= texSize.y) { float border = (u_borderMask & (1u << layerIdx)) != 0 ? 1.0f : 0.0f; return vec4(0.0f, 0.0f, 0.0f, border); } #endif if (!unnormalized) pos /= texSize; return textureLod(layerSampler, coord, 0.0f); } // everything except pos has to be at least spec constant vec4 gaussian_blur(sampler2D layerSampler, uint layerIdx, vec2 pos, uint radius, bool vertical, bool unnormalized) { float offsets[20]; float weights[20]; int steps; if (radius <= 1) { steps = 1; weights[0] = 0.50000000; offsets[0] = 0.01741678; } else if (radius <= 3) { steps = 2; weights[0] = 0.44907984; weights[1] = 0.05092017; offsets[0] = 0.53804874; offsets[1] = 2.06277966; } else if (radius <= 5) { steps = 3; weights[0] = 0.33022788; weights[1] = 0.15701161; weights[2] = 0.01276049; offsets[0] = 0.62183881; offsets[1] = 2.27310348; offsets[2] = 4.14653015; } else if (radius <= 7) { steps = 4; weights[0] = 0.24961469; weights[1] = 0.19246334; weights[2] = 0.05147626; weights[3] = 0.00644572; offsets[0] = 0.64434171; offsets[1] = 2.37884760; offsets[2] = 4.29111052; offsets[3] = 6.21660709; } else if (radius <= 9) { steps = 5; weights[0] = 0.19954681; weights[1] = 0.18945214; weights[2] = 0.08376212; weights[3] = 0.02321143; weights[4] = 0.00402750; offsets[0] = 0.65318614; offsets[1] = 2.42546821; offsets[2] = 4.36803484; offsets[3] = 6.31411505; offsets[4] = 8.26478577; } else if (radius <= 11) { steps = 6; weights[0] = 0.16501401; weights[1] = 0.17507112; weights[2] = 0.10112059; weights[3] = 0.04267556; weights[4] = 0.01315655; weights[5] = 0.00296217; offsets[0] = 0.65771931; offsets[1] = 2.45016599; offsets[2] = 4.41095972; offsets[3] = 6.37285233; offsets[4] = 8.33626175; offsets[5] = 10.30153465; } else if (radius <= 13) { steps = 7; weights[0] = 0.14089632; weights[1] = 0.15927219; weights[2] = 0.10714546; weights[3] = 0.05746555; weights[4] = 0.02457064; weights[5] = 0.00837465; weights[6] = 0.00227519; offsets[0] = 0.66025251; offsets[1] = 2.46415234; offsets[2] = 4.43572092; offsets[3] = 6.40770626; offsets[4] = 8.38027859; offsets[5] = 10.35359478; offsets[6] = 12.32779312; } else if (radius <= 15) { steps = 8; weights[0] = 0.12248611; weights[1] = 0.14426580; weights[2] = 0.10711376; weights[3] = 0.06708494; weights[4] = 0.03544008; weights[5] = 0.01579223; weights[6] = 0.00593551; weights[7] = 0.00188158; offsets[0] = 0.66187197; offsets[1] = 2.47315121; offsets[2] = 4.45177603; offsets[3] = 6.43057728; offsets[4] = 8.40962982; offsets[5] = 10.38900566; offsets[6] = 12.36877155; offsets[7] = 14.34898853; } else if (radius <= 17) { steps = 9; weights[0] = 0.10855028; weights[1] = 0.13135010; weights[2] = 0.10405597; weights[3] = 0.07215953; weights[4] = 0.04380336; weights[5] = 0.02327578; weights[6] = 0.01082628; weights[7] = 0.00440784; weights[8] = 0.00157086; offsets[0] = 0.66292763; offsets[1] = 2.47903848; offsets[2] = 4.46231890; offsets[3] = 6.44568348; offsets[4] = 8.42916870; offsets[5] = 10.41281033; offsets[6] = 12.39664268; offsets[7] = 14.38069725; offsets[8] = 16.36500549; } else if (radius <= 19) { steps = 10; weights[0] = 0.09721643; weights[1] = 0.11994337; weights[2] = 0.09955716; weights[3] = 0.07429117; weights[4] = 0.04983896; weights[5] = 0.03005843; weights[6] = 0.01629773; weights[7] = 0.00794417; weights[8] = 0.00348120; weights[9] = 0.00137140; offsets[0] = 0.66368324; offsets[1] = 2.48326135; offsets[2] = 4.46989584; offsets[3] = 6.45657301; offsets[4] = 8.44331169; offsets[5] = 10.43013191; offsets[6] = 12.41704941; offsets[7] = 14.40408325; offsets[8] = 16.39124870; offsets[9] = 18.37856293; } else if (radius <= 21) { steps = 11; weights[0] = 0.08818824; weights[1] = 0.11032440; weights[2] = 0.09467436; weights[3] = 0.07444362; weights[4] = 0.05363600; weights[5] = 0.03540938; weights[6] = 0.02141965; weights[7] = 0.01187237; weights[8] = 0.00602965; weights[9] = 0.00280591; weights[10] = 0.00119642; offsets[0] = 0.66422051; offsets[1] = 2.48626900; offsets[2] = 4.47529793; offsets[3] = 6.46435070; offsets[4] = 8.45343781; offsets[5] = 10.44256973; offsets[6] = 12.43175602; offsets[7] = 14.42100716; offsets[8] = 16.41033173; offsets[9] = 18.39974213; offsets[10] = 20.38924408; } else if (radius <= 23) { steps = 12; weights[0] = 0.08053473; weights[1] = 0.10183022; weights[2] = 0.08965189; weights[3] = 0.07338920; weights[4] = 0.05585918; weights[5] = 0.03953177; weights[6] = 0.02601279; weights[7] = 0.01591534; weights[8] = 0.00905383; weights[9] = 0.00478890; weights[10] = 0.00235519; weights[11] = 0.00107696; offsets[0] = 0.66463250; offsets[1] = 2.48857713; offsets[2] = 4.47944689; offsets[3] = 6.47033072; offsets[4] = 8.46123409; offsets[5] = 10.45216274; offsets[6] = 12.44312382; offsets[7] = 14.43412209; offsets[8] = 16.42516327; offsets[9] = 18.41625404; offsets[10] = 20.40739822; offsets[11] = 22.39860344; } else if (radius <= 25) { steps = 13; weights[0] = 0.07409717; weights[1] = 0.09446181; weights[2] = 0.08481805; weights[3] = 0.07161362; weights[4] = 0.05685627; weights[5] = 0.04244593; weights[6] = 0.02979672; weights[7] = 0.01966869; weights[8] = 0.01220834; weights[9] = 0.00712544; weights[10] = 0.00391057; weights[11] = 0.00201809; weights[12] = 0.00097930; offsets[0] = 0.66494846; offsets[1] = 2.49034905; offsets[2] = 4.48263311; offsets[3] = 6.47492552; offsets[4] = 8.46722984; offsets[5] = 10.45954895; offsets[6] = 12.45188808; offsets[7] = 14.44425011; offsets[8] = 16.43663788; offsets[9] = 18.42905617; offsets[10] = 20.42150688; offsets[11] = 22.41399384; offsets[12] = 24.40652084; } else if (radius <= 27) { steps = 14; weights[0] = 0.06871720; weights[1] = 0.08815804; weights[2] = 0.08036669; weights[3] = 0.06949072; weights[4] = 0.05699201; weights[5] = 0.04433407; weights[6] = 0.03271127; weights[7] = 0.02289252; weights[8] = 0.01519587; weights[9] = 0.00956738; weights[10] = 0.00571342; weights[11] = 0.00323620; weights[12] = 0.00173864; weights[13] = 0.00088596; offsets[0] = 0.66519141; offsets[1] = 2.49171162; offsets[2] = 4.48508406; offsets[3] = 6.47846127; offsets[4] = 8.47184658; offsets[5] = 10.46524143; offsets[6] = 12.45864868; offsets[7] = 14.45207024; offsets[8] = 16.44550896; offsets[9] = 18.43896484; offsets[10] = 20.43244362; offsets[11] = 22.42594528; offsets[12] = 24.41947365; offsets[13] = 26.41302872; } else if (radius <= 29) { steps = 15; weights[0] = 0.06396806; weights[1] = 0.08249077; weights[2] = 0.07613957; weights[3] = 0.06713246; weights[4] = 0.05654208; weights[5] = 0.04549127; weights[6] = 0.03496240; weights[7] = 0.02566796; weights[8] = 0.01800106; weights[9] = 0.01205929; weights[10] = 0.00771723; weights[11] = 0.00471757; weights[12] = 0.00275480; weights[13] = 0.00153666; weights[14] = 0.00081881; offsets[0] = 0.66539001; offsets[1] = 2.49282646; offsets[2] = 4.48708963; offsets[3] = 6.48135614; offsets[4] = 8.47562790; offsets[5] = 10.46990585; offsets[6] = 12.46419144; offsets[7] = 14.45848656; offsets[8] = 16.45279312; offsets[9] = 18.44711113; offsets[10] = 20.44144249; offsets[11] = 22.43579102; offsets[12] = 24.43015480; offsets[13] = 26.42453575; offsets[14] = 28.41893768; } else if (radius <= 31) { steps = 16; weights[0] = 0.05991369; weights[1] = 0.07758097; weights[2] = 0.07231852; weights[3] = 0.06476077; weights[4] = 0.05571122; weights[5] = 0.04604065; weights[6] = 0.03655175; weights[7] = 0.02787681; weights[8] = 0.02042424; weights[9] = 0.01437529; weights[10] = 0.00971975; weights[11] = 0.00631337; weights[12] = 0.00393945; weights[13] = 0.00236144; weights[14] = 0.00135983; weights[15] = 0.00075225; offsets[0] = 0.66554797; offsets[1] = 2.49371290; offsets[2] = 4.48868465; offsets[3] = 6.48365831; offsets[4] = 8.47863579; offsets[5] = 10.47361755; offsets[6] = 12.46860409; offsets[7] = 14.46359730; offsets[8] = 16.45859718; offsets[9] = 18.45360756; offsets[10] = 20.44862556; offsets[11] = 22.44365311; offsets[12] = 24.43869400; offsets[13] = 26.43374634; offsets[14] = 28.42881012; offsets[15] = 30.42388916; } else if (radius <= 33) { steps = 17; weights[0] = 0.05626789; weights[1] = 0.07311317; weights[2] = 0.06872338; weights[3] = 0.06235151; weights[4] = 0.05460383; weights[5] = 0.04615650; weights[6] = 0.03765964; weights[7] = 0.02965876; weights[8] = 0.02254568; weights[9] = 0.01654273; weights[10] = 0.01171613; weights[11] = 0.00800931; weights[12] = 0.00528492; weights[13] = 0.00336601; weights[14] = 0.00206931; weights[15] = 0.00122792; weights[16] = 0.00070331; offsets[0] = 0.66568094; offsets[1] = 2.49445939; offsets[2] = 4.49002790; offsets[3] = 6.48559809; offsets[4] = 8.48117065; offsets[5] = 10.47674561; offsets[6] = 12.47232437; offsets[7] = 14.46790791; offsets[8] = 16.46349525; offsets[9] = 18.45908928; offsets[10] = 20.45469093; offsets[11] = 22.45029831; offsets[12] = 24.44591331; offsets[13] = 26.44153595; offsets[14] = 28.43717003; offsets[15] = 30.43281174; offsets[16] = 32.42846298; } else if (radius <= 35) { steps = 18; weights[0] = 0.05310436; weights[1] = 0.06919800; weights[2] = 0.06548640; weights[3] = 0.06005197; weights[4] = 0.05336076; weights[5] = 0.04594468; weights[6] = 0.03833250; weights[7] = 0.03098971; weights[8] = 0.02427652; weights[9] = 0.01842781; weights[10] = 0.01355438; weights[11] = 0.00966059; weights[12] = 0.00667185; weights[13] = 0.00446486; weights[14] = 0.00289525; weights[15] = 0.00181922; weights[16] = 0.00110764; weights[17] = 0.00065348; offsets[0] = 0.66578931; offsets[1] = 2.49506807; offsets[2] = 4.49112320; offsets[3] = 6.48717976; offsets[4] = 8.48323727; offsets[5] = 10.47929764; offsets[6] = 12.47535992; offsets[7] = 14.47142506; offsets[8] = 16.46749496; offsets[9] = 18.46356773; offsets[10] = 20.45964622; offsets[11] = 22.45572853; offsets[12] = 24.45181656; offsets[13] = 26.44791031; offsets[14] = 28.44401169; offsets[15] = 30.44011879; offsets[16] = 32.43623352; offsets[17] = 34.43235397; } else { steps = 19; weights[0] = 0.05021843; weights[1] = 0.06559712; weights[2] = 0.06244280; weights[3] = 0.05778965; weights[4] = 0.05199815; weights[5] = 0.04548788; weights[6] = 0.03868776; weights[7] = 0.03199053; weights[8] = 0.02571813; weights[9] = 0.02010145; weights[10] = 0.01527514; weights[11] = 0.01128530; weights[12] = 0.00810608; weights[13] = 0.00566081; weights[14] = 0.00384341; weights[15] = 0.00253702; weights[16] = 0.00162818; weights[17] = 0.00101589; weights[18] = 0.00061626; offsets[0] = 0.66588259; offsets[1] = 2.49559236; offsets[2] = 4.49206638; offsets[3] = 6.48854160; offsets[4] = 8.48501778; offsets[5] = 10.48149586; offsets[6] = 12.47797489; offsets[7] = 14.47445679; offsets[8] = 16.47094154; offsets[9] = 18.46742821; offsets[10] = 20.46391869; offsets[11] = 22.46041298; offsets[12] = 24.45691109; offsets[13] = 26.45341301; offsets[14] = 28.44991875; offsets[15] = 30.44643021; offsets[16] = 32.44294739; offsets[17] = 34.43946838; offsets[18] = 36.43599701; } vec4 color = vec4(0); uint colorspace = get_layer_colorspace(layerIdx); for (int i = 0; i < steps; i++) { vec2 posOffset; if (vertical) posOffset = vec2(0, offsets[i]); else posOffset = vec2(offsets[i], 0); vec4 tmp0 = textureCond(layerSampler, layerIdx, pos - posOffset, unnormalized); tmp0.rgb = colorspace_plane_degamma_tf(tmp0.rgb, colorspace) * weights[i]; color += tmp0; vec4 tmp1 = textureCond(layerSampler, layerIdx, pos + posOffset, unnormalized); tmp1.rgb = colorspace_plane_degamma_tf(tmp1.rgb, colorspace) * weights[i]; color += tmp1; } if (vertical) { color.rgb = apply_layer_color_mgmt(color.rgb, layerIdx, colorspace); } return color; } ValveSoftware-gamescope-eb620ab/src/shaders/colorimetry.h000066400000000000000000000427001502457270500237010ustar00rootroot00000000000000///////////////////////////// // SRGB Encoding Helpers ///////////////////////////// // Go from sRGB encoding -> linear vec3 srgbToLinear(vec3 color) { bvec3 isLo = lessThanEqual(color, vec3(0.04045f)); vec3 loPart = color / 12.92f; vec3 hiPart = pow((color + 0.055f) / 1.055f, vec3(12.0f / 5.0f)); return mix(hiPart, loPart, isLo); } vec4 srgbToLinear(vec4 color) { return vec4(srgbToLinear(color.rgb), color.a); } // Go from linear -> sRGB encoding. vec3 linearToSrgb(vec3 color) { bvec3 isLo = lessThanEqual(color, vec3(0.0031308f)); vec3 loPart = color * 12.92f; vec3 hiPart = pow(color, vec3(5.0f / 12.0f)) * 1.055f - 0.055f; return mix(hiPart, loPart, isLo); } vec4 linearToSrgb(vec4 color) { return vec4(linearToSrgb(color.rgb), color.a); } ///////////////////////////// // Extra Helpers ///////////////////////////// vec3 g24ToLinear(vec3 color) { return pow(color, vec3(2.4f)); } vec4 g24ToLinear(vec4 color) { return vec4(g24ToLinear(color.rgb), color.a); } vec3 g22ToLinear(vec3 color) { return pow(color, vec3(2.2f)); } vec4 g22ToLinear(vec4 color) { return vec4(g22ToLinear(color.rgb), color.a); } ///////////////////////////// // PQ Encoding Helpers ///////////////////////////// // Converts nits -> pq and pq -> nits // Does NOT affect primaries at all. vec3 nitsToPq(vec3 nits) { vec3 y = clamp(nits / 10000.0, vec3(0.0), vec3(1.0)); const float c1 = 0.8359375; const float c2 = 18.8515625; const float c3 = 18.6875; const float m1 = 0.1593017578125; const float m2 = 78.84375; vec3 num = c1 + c2 * pow(y, vec3(m1)); vec3 den = 1.0 + c3 * pow(y, vec3(m1)); vec3 n = pow(num / den, vec3(m2)); return n; } vec3 pqToNits(vec3 pq) { const float c1 = 0.8359375; const float c2 = 18.8515625; const float c3 = 18.6875; const float oo_m1 = 1.0 / 0.1593017578125; const float oo_m2 = 1.0 / 78.84375; vec3 num = max(pow(pq, vec3(oo_m2)) - c1, vec3(0.0)); vec3 den = c2 - c3 * pow(pq, vec3(oo_m2)); return 10000.0 * pow(num / den, vec3(oo_m1)); } // does NOT change primaries, just // the pq value in nits / 80.0f! vec3 pqToScRGBEncoding(vec3 pq) { return pqToNits(pq) / 80.0f; } vec3 scRGBEncodingToPQ(vec3 scRGBEncodedValue) { return nitsToPq(scRGBEncodedValue * 80.0f); } // This is apparently defined at 80 nits... // May want to take liberties with this when displaying // on SDR though... 100 may be a better fit for most content // to match typical sRGB mastering. const float c_scRGBLightScale = 80.0f; vec3 scrgbToNits(vec3 scRGB) { return scRGB * c_scRGBLightScale; } vec3 nitsToScRGB(vec3 nits) { return nits / c_scRGBLightScale; } // nits -> linear (nits / scale) vec3 nitsToLinear(vec3 nits) { return nits * u_nitsToLinear; } // linear -> nits (linear * scale) vec3 linearToNits(vec3 linear) { return linear * u_linearToNits; } ///////////////////////////// // Primary Conversion Helpers ///////////////////////////// struct PrimaryInfo { vec2 displayPrimaryRed; vec2 displayPrimaryGreen; vec2 displayPrimaryBlue; vec2 whitePoint; }; vec3 convert_primary(vec2 xy) { float X = xy.x / xy.y; float Y = 1.0f; float Z = (1.0f - xy.x - xy.y) / xy.y; return vec3(X, Y, Z); } mat3 compute_xyz_matrix(PrimaryInfo metadata) { vec3 red = convert_primary(metadata.displayPrimaryRed); vec3 green = convert_primary(metadata.displayPrimaryGreen); vec3 blue = convert_primary(metadata.displayPrimaryBlue); vec3 white = convert_primary(metadata.whitePoint); vec3 component_scale = inverse(mat3(red, green, blue)) * white; return transpose(mat3(red * component_scale.x, green * component_scale.y, blue * component_scale.z)); } const PrimaryInfo rec709_primaries = { vec2(0.640f, 0.330f), // red vec2(0.300f, 0.600f), // green vec2(0.150f, 0.060f), // blue vec2(0.3127f, 0.3290f), // whitepoint }; /*const*/ mat3 rec709_to_xyz = compute_xyz_matrix(rec709_primaries); /*const*/ mat3 xyz_to_rec709 = inverse(rec709_to_xyz); const PrimaryInfo rec2020_primaries = { vec2(0.708f, 0.292f), // red vec2(0.170f, 0.797f), // green vec2(0.131f, 0.046f), // blue vec2(0.3127f, 0.3290f), // whitepoint }; /*const*/ mat3 rec2020_to_xyz = compute_xyz_matrix(rec2020_primaries); /*const*/ mat3 xyz_to_rec2020 = inverse(rec2020_to_xyz); vec3 convert_primaries(vec3 color, mat3 src_to_xyz, mat3 xyz_to_dst) { return color * mat3(src_to_xyz * xyz_to_dst); } // Rep. ITU-R BT.2446-1 Table 2-4 (inversed) // BT.2446 Method A inverse tone mapping (itm) vec3 bt2446a_inverse_tonemapping( vec3 color, float sdr_nits, float target_nits) { const vec3 k_bt2020 = vec3(0.262698338956556, 0.678008765772817, 0.0592928952706273); const float k_bt2020_r_helper = 1.47460332208689; // 2 - 2 * 0.262698338956556 const float k_bt2020_b_helper = 1.88141420945875; // 2 - 2 * 0.0592928952706273 //gamma const float inverse_gamma = 2.4f; const float gamma = 1.f / inverse_gamma; //RGB->R'G'B' gamma compression color = pow(color, vec3(gamma)); // Rec. ITU-R BT.2020-2 Table 4 //Y'tmo const float y_tmo = dot(color, k_bt2020); //C'b,tmo const float c_b_tmo = (color.b - y_tmo) / k_bt2020_b_helper; //C'r,tmo const float c_r_tmo = (color.r - y_tmo) / k_bt2020_r_helper; // fast path as per Rep. ITU-R BT.2446-1 Table 4 // matches the output of the inversed version for the given input if ((sdr_nits > 99.f && sdr_nits < 101.f) && (target_nits > 999.f && target_nits < 1001.f)) //avoid float issues { sdr_nits = 100.f; target_nits = 1000.f; const float a1 = 1.8712e-5; const float b1 = -2.7334e-3; const float c1 = 1.3141; const float a2 = 2.8305e-6; const float b2 = -7.4622e-4; const float c2 = 1.2328; const float yy_ = 255.0f * y_tmo; const float t = 70; float e = yy_ <= t ? a1 * pow(yy_, 2.f) + b1 * yy_ + c1 : a2 * pow(yy_, 2.f) + b2 * yy_ + c2; const float y_hdr = pow(yy_, e); float s_c = y_tmo > 0.f ? 1.075f * (y_hdr / y_tmo) : 1.f; const float c_b_hdr = c_b_tmo * s_c; const float c_r_hdr = c_r_tmo * s_c; color = vec3(clamp(y_hdr + k_bt2020_r_helper * c_r_hdr, 0.f, 1000.f), clamp(y_hdr - 0.16455312684366 * c_b_hdr - 0.57135312684366 * c_r_hdr, 0.f, 1000.f), clamp(y_hdr + k_bt2020_b_helper * c_b_hdr, 0.f, 1000.f)); color /= 1000.f; } else { // adjusted luma component (inverse) // get Y'sdr const float y_sdr = y_tmo + max(0.1f * c_r_tmo, 0.f); // Tone mapping step 3 (inverse) // get Y'c const float p_sdr = 1 + 32 * pow( sdr_nits / 10000.f , gamma); //Y'c const float y_c = log((y_sdr * (p_sdr - 1)) + 1) / log(p_sdr); //log = ln // Tone mapping step 2 (inverse) // get Y'p float y_p = 0.f; const float y_p_0 = y_c / 1.0770f; const float y_p_2 = (y_c - 0.5000f) / 0.5000f; const float _first = -2.7811f; const float _sqrt = sqrt(4.83307641 - 4.604 * y_c); const float _div = -2.302f; const float y_p_1 = (_first + _sqrt) / _div; if (y_p_0 <= 0.7399f) y_p = y_p_0; else if (y_p_1 > 0.7399f && y_p_1 < 0.9909f) y_p = y_p_1; else if (y_p_2 >= 0.9909f) y_p = y_p_2; else //y_p_1 sometimes (about 0.12% out of the full RGB range) //is less than 0.7399f or more than 0.9909f because of float inaccuracies { //error is small enough (less than 0.001) for this to be OK //ideally you would choose between y_p_0 and y_p_1 if y_p_1 < 0.7399f depending on which is closer to 0.7399f //or between y_p_1 and y_p_2 if y_p_1 > 0.9909f depending on which is closer to 0.9909f y_p = y_p_1; //this clamps it to 2 float steps above 0.7399f or 2 float steps below 0.9909f //if (y_p_1 < 0.7399f) // y_p = 0.7399001f; //else // y_p = 0.99089986f; } // Tone mapping step 1 (inverse) // get Y' const float p_hdr = 1 + 32 * pow( target_nits / 10000.f , gamma); //Y' const float y_ = (pow(p_hdr, y_p) - 1) / (p_hdr - 1); // Colour scaling function float col_scale = 0.f; if (y_ > 0.f) // avoid divison by zero col_scale = y_sdr / (1.1f * y_); // Colour difference signals (inverse) and Luma (inverse) // get R'G'B' color.b = ((c_b_tmo * k_bt2020_b_helper) / col_scale) + y_; color.r = ((c_r_tmo * k_bt2020_r_helper) / col_scale) + y_; color.g = (y_ - (k_bt2020.r * color.r + k_bt2020.b * color.b)) / k_bt2020.g; //safety color.r = clamp(color.r, 0.f, 1.f); color.g = clamp(color.g, 0.f, 1.f); color.b = clamp(color.b, 0.f, 1.f); } // R'G'B' gamma expansion color = pow(color, vec3(inverse_gamma)); // map target luminance into 10000 nits color = color * target_nits; return color; } #include "heatmap.h" // Generic helper vec3 colorspace_plane_degamma_tf(vec3 color, uint colorspace) { // matches with colorspace_to_plane_degamma_tf in drm.cpp switch (colorspace) { default: return vec3(1, 1, 0); // should never happen case colorspace_passthru: case colorspace_linear: // Using sRGB image view. Unlike DRM which doesn't get that liberty for scanout. case colorspace_scRGB: return color; case colorspace_sRGB: return srgbToLinear(color); case colorspace_pq: return pqToScRGBEncoding(color); } } vec3 colorspace_plane_regamma_tf(vec3 color, uint colorspace) { switch (colorspace) { default: return vec3(1, 1, 0); // should never happen case colorspace_passthru: case colorspace_scRGB: return color; case colorspace_linear: // Using sRGB image view. Unlike DRM which doesn't get that liberty for scanout. case colorspace_sRGB: return linearToSrgb(color); case colorspace_pq: return scRGBEncodingToPQ(color); } } vec3 colorspace_plane_shaper_tf(vec3 color, uint colorspace) { // matches with colorspace_to_plane_regamma_tf in drm.cpp switch (colorspace) { default: return vec3(0, 1, 1); // should never happen case colorspace_linear: case colorspace_sRGB: return linearToSrgb(color); case colorspace_scRGB: case colorspace_pq: return scRGBEncodingToPQ(color); } } // pre-blend doing display EOTF -> display linearized vec3 colorspace_blend_tf(vec3 color, uint eotf) { switch (eotf) { default: return color; // Note from Josh: // // We are kinda halfway between output space and not at this point // the color primaries, gamut remapping has already been performed // in display output 2.2 space, but that doesn't change the fact // that we haven't displayed it yet! // // Perform the alpha blending with sRGB linearization (like the CONTENT specifies) here // the primaries and gamut remapping transformations we performed in output 2.2 space do NOT matter. // This is more correct than using gamma 2.2 for that here. case EOTF_Gamma22: return srgbToLinear(color); case EOTF_PQ: return pqToScRGBEncoding(color); } } // post blend doing display linearized -> display EOTF vec3 colorspace_output_tf(vec3 color, uint eotf) { switch (eotf) { default: return color; // see comment in colorspace_blend_tf case EOTF_Gamma22: return linearToSrgb(color); case EOTF_PQ: return scRGBEncodingToPQ(color); } } // matches how we treat content here :) uint colorspace_to_eotf(uint colorspace) { // matches with ColorSpaceToEOTFIndex in drm.cpp switch ( colorspace ) { default: case colorspace_linear: // Not actually linear, just Linear vs sRGB image views in Vulkan. Still viewed as sRGB on the DRM side. case colorspace_sRGB: // SDR sRGB content treated as native Gamma 22 curve. No need to do sRGB -> 2.2 or whatever. return EOTF_Gamma22; case colorspace_scRGB: // Okay, so this is WEIRD right? OKAY Let me explain it to you. // The plan for scRGB content is to go from scRGB -> PQ in a SHAPER_TF // before indexing into the shaper. return EOTF_PQ; case colorspace_pq: return EOTF_PQ; } } float half_texel_scale(float x, float half_texel) { return mix(0.0f + half_texel, 1.0f - half_texel, x); } vec3 half_texel_scale(vec3 x, vec3 half_texel) { return mix(vec3(0.0f) + half_texel, vec3(1.0f) - half_texel, x); } vec3 perform_1dlut(vec3 color, sampler1D shaperLUT) { int size = textureSize(shaperLUT, 0); float offset = 0.5f / float(size); return vec3( textureLod(shaperLUT, half_texel_scale(color.r, offset), 0.0f).r, textureLod(shaperLUT, half_texel_scale(color.g, offset), 0.0f).g, textureLod(shaperLUT, half_texel_scale(color.b, offset), 0.0f).b); } vec3 perform_3dlut_native(vec3 color, sampler3D lut3D) { ivec3 size = textureSize(lut3D, 0); vec3 offset = 0.5f / vec3(float(size.x), float(size.y), float(size.z)); return textureLod(lut3D, half_texel_scale(color.rgb, offset), 0.0f).rgb; } // Adapted from: // https://github.com/AcademySoftwareFoundation/OpenColorIO/ops/lut3d/Lut3DOpGPU.cpp // License available in their repo and in our LICENSE file. vec3 perform_3dlut_tetrahedral(vec3 color, sampler3D lut3D) { ivec3 size_i = textureSize(lut3D, 0); // We only support uniform lut sizes so take .x's dim float size = float(size_i.x); float incr = 1.0f / size; vec3 outColor = color.bgr; vec3 coords = outColor.rgb * (vec3(size - 1.0f)); vec3 baseInd = floor(coords); vec3 frac = coords - baseInd; vec3 f1, f4; baseInd = (baseInd.zyx + vec3(0.5)) / vec3(size); vec3 v1 = textureLod(lut3D, baseInd, 0).rgb; vec3 nextInd = baseInd + vec3(incr); vec3 v4 = textureLod(lut3D, nextInd, 0).rgb; if (frac.r >= frac.g) { if (frac.g >= frac.b) { nextInd = baseInd + vec3(0, 0, incr); vec3 v2 = textureLod(lut3D, nextInd, 0).rgb; nextInd = baseInd + vec3(0, incr, incr); vec3 v3 = textureLod(lut3D, nextInd, 0).rgb; f1 = vec3(1.0f - frac.r); f4 = vec3(frac.b); vec3 f2 = vec3(frac.r - frac.g); vec3 f3 = vec3(frac.g - frac.b); outColor.rgb = (f2 * v2) + (f3 * v3); } else if (frac.r >= frac.b) { nextInd = baseInd + vec3(0, 0, incr); vec3 v2 = textureLod(lut3D, nextInd, 0).rgb; nextInd = baseInd + vec3(incr, 0, incr); vec3 v3 = textureLod(lut3D, nextInd, 0).rgb; f1 = vec3(1.0f - frac.r); f4 = vec3(frac.g); vec3 f2 = vec3(frac.r - frac.b); vec3 f3 = vec3(frac.b - frac.g); outColor.rgb = (f2 * v2) + (f3 * v3); } else { nextInd = baseInd + vec3(incr, 0, 0); vec3 v2 = textureLod(lut3D, nextInd, 0).rgb; nextInd = baseInd + vec3(incr, 0, incr); vec3 v3 = textureLod(lut3D, nextInd, 0).rgb; f1 = vec3(1.0f - frac.b); f4 = vec3(frac.g); vec3 f2 = vec3(frac.b - frac.r); vec3 f3 = vec3(frac.r - frac.g); outColor.rgb = (f2 * v2) + (f3 * v3); } } else { if (frac.g <= frac.b) { nextInd = baseInd + vec3(incr, 0, 0); vec3 v2 = textureLod(lut3D, nextInd, 0).rgb; nextInd = baseInd + vec3(incr, incr, 0); vec3 v3 = textureLod(lut3D, nextInd, 0).rgb; f1 = vec3(1.0f - frac.b); f4 = vec3(frac.r); vec3 f2 = vec3(frac.b - frac.g); vec3 f3 = vec3(frac.g - frac.r); outColor.rgb = (f2 * v2) + (f3 * v3); } else if (frac.r >= frac.b) { nextInd = baseInd + vec3(0, incr, 0); vec3 v2 = textureLod(lut3D, nextInd, 0).rgb; nextInd = baseInd + vec3(0, incr, incr); vec3 v3 = textureLod(lut3D, nextInd, 0).rgb; f1 = vec3(1.0f - frac.g); f4 = vec3(frac.b); vec3 f2 = vec3(frac.g - frac.r); vec3 f3 = vec3(frac.r - frac.b); outColor.rgb = (f2 * v2) + (f3 * v3); } else { nextInd = baseInd + vec3(0, incr, 0); vec3 v2 = textureLod(lut3D, nextInd, 0).rgb; nextInd = baseInd + vec3(incr, incr, 0); vec3 v3 = textureLod(lut3D, nextInd, 0).rgb; f1 = vec3(1.0f - frac.g); f4 = vec3(frac.r); vec3 f2 = vec3(frac.g - frac.b); vec3 f3 = vec3(frac.b - frac.r); outColor.rgb = (f2 * v2) + (f3 * v3); } } outColor.rgb = outColor.rgb + (f1 * v1) + (f4 * v4); return outColor.rgb; } vec3 perform_3dlut(vec3 color, sampler3D lut3D) { return perform_3dlut_tetrahedral(color, lut3D); } ValveSoftware-gamescope-eb620ab/src/shaders/composite.h000066400000000000000000000164661502457270500233450ustar00rootroot00000000000000#include "colorimetry.h" #include "shaderfilter.h" #include "alphamode.h" vec4 sampleRegular(sampler2D tex, vec2 coord, uint colorspace) { vec4 color = textureLod(tex, coord, 0); color.rgb = colorspace_plane_degamma_tf(color.rgb, colorspace); return color; } // To be considered pseudo-bandlimited, upscaling factor must be at least 2x. const float bandlimited_PI = 3.14159265359; const float bandlimited_PI_half = 0.5 * bandlimited_PI; // size: resolution of sampled texture // inv_size: inverse resolution of sampled texture // extent: Screen-space gradient of UV in texels. Typically computed as (texture resolution) / (viewport resolution). // If screen is rotated by 90 or 270 degrees, the derivatives need to be computed appropriately. // For uniform scaling, none of this matters. // extent can be multiplied to achieve LOD bias. // extent must be at least 1.0 / 256.0. vec4 sampleBandLimited(sampler2D samp, vec2 uv, vec2 size, vec2 inv_size, vec2 extent, uint colorspace, bool unnormalized) { // Josh: // Clamp to behaviour like 4x scale (0.25). // // Was defaulted to 2x before (0.5), which is 1px, but gives blurry result // on Cave Story (480p) -> 800p on Deck. // TODO: Maybe make this configurable? const float max_extent = 0.25f; // Get base pixel and phase, range [0, 1). vec2 pixel = uv * (unnormalized ? vec2(1.0f) : size) - 0.5; vec2 base_pixel = floor(pixel); vec2 phase = pixel - base_pixel; // We can resolve the filter by just sampling a single 2x2 block. // Lerp between normal sampling at LOD 0, and bandlimited pixel filter at LOD -1. vec2 shift = 0.5 + 0.5 * sin(bandlimited_PI_half * clamp((phase - 0.5) / min(extent, vec2(max_extent)), -1.0, 1.0)); uv = (base_pixel + 0.5 + shift) * (unnormalized ? vec2(1.0f) : inv_size); return sampleRegular(samp, uv, colorspace); } uint pseudo_random(uint seed) { seed ^= (seed << 13); seed ^= (seed >> 17); seed ^= (seed << 5); return seed * 1664525u + 1013904223u; } void compositing_debug(uvec2 coord) { uvec2 pos = coord; pos.x -= (u_frameId & 2) != 0 ? 128 : 0; pos.y -= (u_frameId & 1) != 0 ? 128 : 0; if (pos.x >= 40 && pos.x < 120 && pos.y >= 40 && pos.y < 120) { vec4 value = vec4(1.0f, 1.0f, 1.0f, 1.0f); if (checkDebugFlag(compositedebug_Markers_Partial)) { value = vec4(0.0f, 1.0f, 1.0f, 1.0f); } if (pos.x >= 48 && pos.x < 112 && pos.y >= 48 && pos.y < 112) { uint random = pseudo_random(u_frameId.x + (pos.x & ~0x7) + (pos.y & ~0x7) * 50); vec4 time = round(unpackUnorm4x8(random)).xyzw; if (time.x + time.y + time.z + time.w < 2.0f) value = vec4(0.0f, 0.0f, 0.0f, 1.0f); } imageStore(dst, ivec2(coord), value); } } // Takes in a scRGB/Linear encoded value and applies color management // based on the input colorspace. // // ie. call colorspace_plane_degamma_tf(color.rgb, colorspace) before // input to this function. vec3 apply_layer_color_mgmt(vec3 color, uint layer, uint colorspace) { if (colorspace == colorspace_passthru) return color; if (c_itm_enable) { color = bt2446a_inverse_tonemapping(color, u_itmSdrNits, u_itmTargetNits); colorspace = colorspace_pq; } // Shaper + 3D LUT path to match DRM. uint plane_eotf = colorspace_to_eotf(colorspace); if (layer == 0 && checkDebugFlag(compositedebug_Heatmap)) { // Debug HDR heatmap. color = hdr_heatmap(color, colorspace); plane_eotf = EOTF_Gamma22; } // The shaper TF is basically just a regamma to get into something the shaper LUT can handle. // // Despite naming, degamma + shaper TF are NOT necessarily the inverse of each other. ^^^ // This gets the color ready to go into the shaper LUT. // ie. scRGB -> PQ // // We also need to do degamma here for non-linear views to blend in linear space. // ie. PQ -> PQ would need us to manually do bilinear here. bool lut3d_enabled = textureQueryLevels(s_shaperLut[plane_eotf]) != 0; if (lut3d_enabled) { color = colorspace_plane_shaper_tf(color, colorspace); color = perform_1dlut(color, s_shaperLut[plane_eotf]); color = perform_3dlut(color, s_lut3D[plane_eotf]); color = colorspace_blend_tf(color, c_output_eotf); } return color; } vec4 sampleBilinear(sampler2D tex, vec2 coord, uint colorspace, bool unnormalized) { vec2 scale = unnormalized ? vec2(1.0) : vec2(textureSize(tex, 0)); vec2 pixCoord = coord * scale - 0.5f; vec2 originPixCoord = floor(pixCoord); vec2 gatherUV = (originPixCoord * scale + 1.0f) / scale; vec4 red = textureGather(tex, gatherUV, 0); vec4 green = textureGather(tex, gatherUV, 1); vec4 blue = textureGather(tex, gatherUV, 2); vec4 alpha = textureGather(tex, gatherUV, 3); vec4 c00 = vec4(red.w, green.w, blue.w, alpha.w); vec4 c01 = vec4(red.x, green.x, blue.x, alpha.x); vec4 c11 = vec4(red.y, green.y, blue.y, alpha.y); vec4 c10 = vec4(red.z, green.z, blue.z, alpha.z); c00.rgb = colorspace_plane_degamma_tf(c00.rgb, colorspace); c01.rgb = colorspace_plane_degamma_tf(c01.rgb, colorspace); c11.rgb = colorspace_plane_degamma_tf(c11.rgb, colorspace); c10.rgb = colorspace_plane_degamma_tf(c10.rgb, colorspace); vec2 filterWeight = pixCoord - originPixCoord; vec4 temp0 = mix(c01, c11, filterWeight.x); vec4 temp1 = mix(c00, c10, filterWeight.x); return mix(temp1, temp0, filterWeight.y); } vec4 sampleLayerEx(sampler2D layerSampler, uint offsetLayerIdx, uint colorspaceLayerIdx, vec2 uv, bool unnormalized) { vec2 coord = ((uv + u_offset[offsetLayerIdx]) * u_scale[offsetLayerIdx]); vec2 texSize = textureSize(layerSampler, 0); if (coord.x < 0.0f || coord.y < 0.0f || coord.x >= texSize.x || coord.y >= texSize.y) { float border = (u_borderMask & (1u << offsetLayerIdx)) != 0 ? 1.0f : 0.0f; if (checkDebugFlag(compositedebug_PlaneBorders)) return vec4(vec3(1.0f, 0.0f, 1.0f) * border, border); return vec4(0.0f, 0.0f, 0.0f, border); } if (!unnormalized) coord /= texSize; uint colorspace = get_layer_colorspace(colorspaceLayerIdx); vec4 color; if (get_layer_shaderfilter(offsetLayerIdx) == filter_pixel) { vec2 output_res = texSize / u_scale[offsetLayerIdx]; vec2 extent = max((texSize / output_res), vec2(1.0 / 256.0)); color = sampleBandLimited(layerSampler, coord, unnormalized ? vec2(1.0f) : texSize, unnormalized ? vec2(1.0f) : vec2(1.0f) / texSize, extent, colorspace, unnormalized); } else if (get_layer_shaderfilter(offsetLayerIdx) == filter_linear_emulated) { color = sampleBilinear(layerSampler, coord, colorspace, unnormalized); } else { color = sampleRegular(layerSampler, coord, colorspace); } // JoshA: AMDGPU applies 3x4 CTM like this, where A is 1.0, but it only affects .rgb. color.rgb = vec4(color.rgb, 1.0f) * u_ctm[colorspaceLayerIdx]; color.rgb = apply_layer_color_mgmt(color.rgb, offsetLayerIdx, colorspace); return color; } vec4 sampleLayer(sampler2D layerSampler, uint layerIdx, vec2 uv, bool unnormalized) { return sampleLayerEx(layerSampler, layerIdx, layerIdx, uv, unnormalized); } vec3 encodeOutputColor(vec3 value) { return colorspace_output_tf(value, c_output_eotf); } ValveSoftware-gamescope-eb620ab/src/shaders/cs_composite_blit.comp000066400000000000000000000025701502457270500255420ustar00rootroot00000000000000#version 450 #extension GL_GOOGLE_include_directive : require #extension GL_EXT_scalar_block_layout : require #include "descriptor_set.h" layout( local_size_x = 8, local_size_y = 8, local_size_z = 1) in; #include "blit_push_data.h" #include "composite.h" vec4 sampleLayer(uint layerIdx, vec2 uv) { if ((c_ycbcrMask & (1 << layerIdx)) != 0) return sampleLayer(s_ycbcr_samplers[layerIdx], layerIdx, uv, false); return sampleLayer(s_samplers[layerIdx], layerIdx, uv, true); } void main() { uvec2 coord = uvec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y); uvec2 outSize = imageSize(dst); if (coord.x >= outSize.x || coord.y >= outSize.y) return; vec2 uv = vec2(coord); vec4 outputValue = vec4(0.0f); if (checkDebugFlag(compositedebug_PlaneBorders)) outputValue = vec4(1.0f, 0.0f, 0.0f, 0.0f); if (c_layerCount > 0) { outputValue = sampleLayer(0, uv) * u_opacity[0]; } for (int i = 1; i < c_layerCount; i++) { vec4 layerColor = sampleLayer(i, uv); outputValue = BlendLayer( i, outputValue, layerColor, u_opacity[i] ); } outputValue.rgb = encodeOutputColor(outputValue.rgb); imageStore(dst, ivec2(coord), outputValue); // Indicator to quickly tell if we're in the compositing path or not. if (checkDebugFlag(compositedebug_Markers)) compositing_debug(coord); } ValveSoftware-gamescope-eb620ab/src/shaders/cs_composite_blur.comp000066400000000000000000000026331502457270500255540ustar00rootroot00000000000000#version 450 #extension GL_GOOGLE_include_directive : require #extension GL_EXT_scalar_block_layout : require #include "descriptor_set.h" layout( local_size_x = 8, local_size_y = 8, local_size_z = 1) in; #include "blit_push_data.h" #define BLUR_DONT_SCALE 1 #include "composite.h" #include "blur.h" vec4 sampleLayer(uint layerIdx, vec2 uv) { if ((c_ycbcrMask & (1 << layerIdx)) != 0) return sampleLayer(s_ycbcr_samplers[layerIdx], layerIdx, uv, false); return sampleLayer(s_samplers[layerIdx], layerIdx, uv, true); } void main() { uvec2 coord = uvec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y); uvec2 outSize = imageSize(dst); if (coord.x >= outSize.x || coord.y >= outSize.y) return; vec2 uv = vec2(coord); vec3 outputValue = vec3(0.0f); if (checkDebugFlag(compositedebug_PlaneBorders)) outputValue = vec3(1.0f, 0.0f, 0.0f); if (c_layerCount > 0) outputValue = gaussian_blur(s_samplers[VKR_BLUR_EXTRA_SLOT], 0, vec2(coord), u_blur_radius, true, true).rgb; for (int i = c_blur_layer_count; i < c_layerCount; i++) { vec4 layerColor = sampleLayer(i, uv); outputValue = BlendLayer( i, outputValue, layerColor, u_opacity[i] ); } outputValue = encodeOutputColor(outputValue); imageStore(dst, ivec2(coord), vec4(outputValue, 0)); if (checkDebugFlag(compositedebug_Markers)) compositing_debug(coord); } ValveSoftware-gamescope-eb620ab/src/shaders/cs_composite_blur_cond.comp000066400000000000000000000040671502457270500265620ustar00rootroot00000000000000#version 450 #extension GL_GOOGLE_include_directive : require #extension GL_EXT_scalar_block_layout : require #include "descriptor_set.h" layout( local_size_x = 8, local_size_y = 8, local_size_z = 1) in; #include "blit_push_data.h" #define BLUR_DONT_SCALE 1 #include "composite.h" #include "blur.h" vec4 sampleLayer(uint layerIdx, vec2 uv) { if ((c_ycbcrMask & (1 << layerIdx)) != 0) return sampleLayer(s_ycbcr_samplers[layerIdx], layerIdx, uv, false); return sampleLayer(s_samplers[layerIdx], layerIdx, uv, true); } void main() { uvec2 coord = uvec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y); uvec2 outSize = imageSize(dst); if (coord.x >= outSize.x || coord.y >= outSize.y) return; vec2 uv = vec2(coord); vec3 outputValue = vec3(0.0f); if (checkDebugFlag(compositedebug_PlaneBorders)) outputValue = vec3(1.0f, 0.0f, 0.0f); float finalRevAlpha = 1.0f; for (int i = c_blur_layer_count; i < c_layerCount; i++) { vec4 layerColor = sampleLayer(i, uv); float opacity = u_opacity[i]; float layerAlpha = opacity * layerColor.a; float revAlpha = (1.0f - layerAlpha); outputValue = BlendLayer( i, outputValue, layerColor, opacity ); finalRevAlpha *= revAlpha; } if (c_layerCount > 0) { if (finalRevAlpha < 0.95) { outputValue += gaussian_blur(s_samplers[VKR_BLUR_EXTRA_SLOT], 0, vec2(coord), u_blur_radius, true, true).rgb * finalRevAlpha; } else { outputValue = sampleLayer(0, uv).rgb * u_opacity[0]; for (int i = 1; i < c_blur_layer_count; i++) { vec4 layerColor = sampleLayer(i, uv); float opacity = u_opacity[i]; float layerAlpha = opacity * layerColor.a; outputValue = BlendLayer( i, outputValue, layerColor, opacity ); } } } outputValue = encodeOutputColor(outputValue); imageStore(dst, ivec2(coord), vec4(outputValue, 0)); if (checkDebugFlag(compositedebug_Markers)) compositing_debug(coord); } ValveSoftware-gamescope-eb620ab/src/shaders/cs_composite_rcas.comp000066400000000000000000000061311502457270500255350ustar00rootroot00000000000000#version 460 #extension GL_GOOGLE_include_directive : require #extension GL_EXT_scalar_block_layout : require #include "descriptor_set.h" layout( local_size_x = 64, local_size_y = 1, local_size_z = 1) in; layout(binding = 0, scalar) uniform layers_t { uvec2 u_layer0Offset; vec2 u_scale[VKR_MAX_LAYERS - 1]; vec2 u_offset[VKR_MAX_LAYERS - 1]; float u_opacity[VKR_MAX_LAYERS]; mat3x4 u_ctm[VKR_MAX_LAYERS]; uint u_borderMask; uint u_frameId; uint u_c1; uint u_shaderFilter; uint u_alphaMode; // hdr float u_linearToNits; float u_nitsToLinear; float u_itmSdrNits; float u_itmTargetNits; }; #include "composite.h" #define A_GPU 1 #define A_GLSL 1 #include "ffx_a.h" #define FSR_RCAS_F 1 vec4 FsrRcasLoadF(ivec2 p) { return texelFetch(s_samplers[0], ivec2(p), 0); } // our input is already srgb void FsrRcasInputF(inout float r, inout float g, inout float b) {} #include "ffx_fsr1.h" vec4 sampleLayer(uint layerIdx, vec2 uv) { if ((c_ycbcrMask & (1 << layerIdx)) != 0) return sampleLayerEx(s_ycbcr_samplers[layerIdx], layerIdx - 1, layerIdx, uv, false); return sampleLayerEx(s_samplers[layerIdx], layerIdx - 1, layerIdx, uv, true); } void rcasComposite(uvec2 pos) { vec3 outputValue = vec3(0.0f); if (checkDebugFlag(compositedebug_PlaneBorders)) outputValue = vec3(1.0f, 0.0f, 0.0f); if (c_layerCount > 0) { // this is actually signed, underflow will be filtered out by the branch below uvec2 rcasPos = pos + u_layer0Offset; uvec2 layer0Extent = uvec2(textureSize(s_samplers[0], 0)); if (all(lessThan(rcasPos, layer0Extent))) { FsrRcasF(outputValue.r, outputValue.g, outputValue.b, rcasPos, u_c1.xxxx); uint colorspace = get_layer_colorspace(0); if (colorspace == colorspace_linear) { // We don't use an sRGB view for FSR due to the spaces RCAS works in. colorspace = colorspace_sRGB; } outputValue.rgb = colorspace_plane_degamma_tf(outputValue.rgb, colorspace); outputValue.rgb = (vec4(outputValue.rgb, 1.0f) * u_ctm[0]).rgb; outputValue.rgb = apply_layer_color_mgmt(outputValue.rgb, 0, colorspace); outputValue *= u_opacity[0]; } } if (c_layerCount > 1) { vec2 uv = vec2(pos); for (int i = 1; i < c_layerCount; i++) { vec4 layerColor = sampleLayer(i, uv); outputValue = BlendLayer( i, outputValue, layerColor, u_opacity[i] ); } } outputValue = encodeOutputColor(outputValue); imageStore(dst, ivec2(pos), vec4(outputValue, 0)); if (checkDebugFlag(compositedebug_Markers)) compositing_debug(pos); } void main() { // AMD recommends to use this swizzle and to process 4 pixel per invocation // for better cache utilisation uvec2 pos = ARmp8x8(gl_LocalInvocationID.x) + uvec2(gl_WorkGroupID.x << 4u, gl_WorkGroupID.y << 4u); rcasComposite(pos); pos.x += 8u; rcasComposite(pos); pos.y += 8u; rcasComposite(pos); pos.x -= 8u; rcasComposite(pos); } ValveSoftware-gamescope-eb620ab/src/shaders/cs_easu.comp000066400000000000000000000021141502457270500234550ustar00rootroot00000000000000#version 460 #extension GL_GOOGLE_include_directive : require #extension GL_EXT_scalar_block_layout : require #include "descriptor_set.h" layout( local_size_x = 64, local_size_y = 1, local_size_z = 1) in; layout(binding = 0, scalar) uniform layers_t { uvec4 c1, c2, c3, c4; }; #define A_GPU 1 #define A_GLSL 1 #include "ffx_a.h" #define FSR_EASU_F 1 AF4 FsrEasuRF(AF2 p){return AF4(textureGather(s_samplers[0], p, 0));} AF4 FsrEasuGF(AF2 p){return AF4(textureGather(s_samplers[0], p, 1));} AF4 FsrEasuBF(AF2 p){return AF4(textureGather(s_samplers[0], p, 2));} #include "ffx_fsr1.h" void easuPass(uvec2 pos) { vec3 color; FsrEasuF(color, pos, c1, c2, c3, c4); imageStore(dst, ivec2(pos), vec4(color, 1)); } void main() { // AMD recommends to use this swizzle and to process 4 pixel per invocation // for better cache utilisation uvec2 pos = ARmp8x8(gl_LocalInvocationID.x) + uvec2(gl_WorkGroupID.x << 4u, gl_WorkGroupID.y << 4u); easuPass(pos); pos.x += 8u; easuPass(pos); pos.y += 8u; easuPass(pos); pos.x -= 8u; easuPass(pos); } ValveSoftware-gamescope-eb620ab/src/shaders/cs_easu_fp16.comp000066400000000000000000000023021502457270500243100ustar00rootroot00000000000000#version 460 #extension GL_GOOGLE_include_directive : require #extension GL_EXT_shader_explicit_arithmetic_types_float16 : require #extension GL_EXT_scalar_block_layout : require #include "descriptor_set.h" layout( local_size_x = 64, local_size_y = 1, local_size_z = 1) in; layout(binding = 0, scalar) uniform layers_t { uvec4 c1, c2, c3, c4; }; #define A_GPU 1 #define A_GLSL 1 #define A_HALF 1 #include "ffx_a.h" #define FSR_EASU_H 1 f16vec4 FsrEasuRH(vec2 p) {return f16vec4(textureGather(s_samplers[0], p, 0));} f16vec4 FsrEasuGH(vec2 p) {return f16vec4(textureGather(s_samplers[0], p, 1));} f16vec4 FsrEasuBH(vec2 p) {return f16vec4(textureGather(s_samplers[0], p, 2));} #include "ffx_fsr1.h" void easuPass(uvec2 pos) { f16vec3 color; FsrEasuH(color, pos, c1, c2, c3, c4); imageStore(dst, ivec2(pos), vec4(color, 1)); } void main() { // AMD recommends to use this swizzle and to process 4 pixel per invocation // for better cache utilisation uvec2 pos = ARmp8x8(gl_LocalInvocationID.x) + uvec2(gl_WorkGroupID.x << 4u, gl_WorkGroupID.y << 4u); easuPass(pos); pos.x += 8u; easuPass(pos); pos.y += 8u; easuPass(pos); pos.x -= 8u; easuPass(pos); } ValveSoftware-gamescope-eb620ab/src/shaders/cs_gaussian_blur_horizontal.comp000066400000000000000000000033651502457270500276400ustar00rootroot00000000000000#version 460 #extension GL_GOOGLE_include_directive : require #extension GL_EXT_scalar_block_layout : require #include "descriptor_set.h" layout( local_size_x = 8, local_size_y = 8, local_size_z = 1) in; layout(binding = 0, scalar) uniform layers_t { vec2 u_scale[VKR_MAX_LAYERS]; vec2 u_offset[VKR_MAX_LAYERS]; float u_opacity[VKR_MAX_LAYERS]; mat3x4 u_ctm[VKR_MAX_LAYERS]; uint u_borderMask; uint u_frameId; uint u_blur_radius; uint u_shaderFilter; uint u_alphaMode; // hdr float u_linearToNits; float u_nitsToLinear; float u_itmSdrNits; float u_itmTargetNits; }; #include "composite.h" #include "blur.h" void main() { uvec2 coord = uvec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y); vec2 pos = coord; vec3 outputValue = vec3(0); if (c_layerCount > 0) { if ((c_ycbcrMask & 1) != 0) outputValue = gaussian_blur(s_ycbcr_samplers[0], 0, pos, u_blur_radius, false, false).rgb * u_opacity[0]; else outputValue = gaussian_blur(s_samplers[0], 0, pos, u_blur_radius, false, true).rgb * u_opacity[0]; } for (int i = 1; i < c_layerCount; i++) { vec4 layerColor; // YCBCR technically has incorrect blending here but... meh. if ((c_ycbcrMask & (1 << i)) != 0) layerColor = gaussian_blur(s_ycbcr_samplers[i], i, pos, u_blur_radius, false, false); else layerColor = gaussian_blur(s_samplers[i], i, pos, u_blur_radius, false, true); outputValue = BlendLayer( i, outputValue, layerColor, u_opacity[i] ); } uint colorspace = get_layer_colorspace(0); outputValue = colorspace_plane_regamma_tf(outputValue, colorspace); imageStore(dst, ivec2(coord), vec4(outputValue, 0)); } ValveSoftware-gamescope-eb620ab/src/shaders/cs_nis.comp000066400000000000000000000030151502457270500233120ustar00rootroot00000000000000#version 450 #extension GL_GOOGLE_include_directive : enable #extension GL_EXT_scalar_block_layout : require #define NIS_GLSL 1 #define NIS_SCALER 1 #include "descriptor_set.h" layout(binding = 0, scalar) uniform const_buffer { float kDetectRatio; float kDetectThres; float kMinContrastRatio; float kRatioNorm; float kContrastBoost; float kEps; float kSharpStartY; float kSharpScaleY; float kSharpStrengthMin; float kSharpStrengthScale; float kSharpLimitMin; float kSharpLimitScale; float kScaleX; float kScaleY; float kDstNormX; float kDstNormY; float kSrcNormX; float kSrcNormY; uint kInputViewportOriginX; uint kInputViewportOriginY; uint kInputViewportWidth; uint kInputViewportHeight; uint kOutputViewportOriginX; uint kOutputViewportOriginY; uint kOutputViewportWidth; uint kOutputViewportHeight; float reserved0; float reserved1; }; // These are the names the NIS shader uses to access the data needed to do the upscaling #define in_texture s_samplers[0] #define out_texture dst #define coef_scaler s_samplers[VKR_NIS_COEF_SCALER_SLOT] #define coef_usm s_samplers[VKR_NIS_COEF_USM_SLOT] // Gamescope is using combined image samplers so no need to specify a sampler #define sampler2D(x, sampler) (x) #include "NVIDIAImageScaling/NIS/NIS_Scaler.h" layout(local_size_x=NIS_THREAD_GROUP_SIZE, local_size_y = 1, local_size_z = 1) in; void main() { NVScaler(gl_WorkGroupID.xy, gl_LocalInvocationID.x); }ValveSoftware-gamescope-eb620ab/src/shaders/cs_nis_fp16.comp000066400000000000000000000031631502457270500241520ustar00rootroot00000000000000#version 450 #extension GL_GOOGLE_include_directive : enable #extension GL_EXT_shader_explicit_arithmetic_types_float16 : require #extension GL_EXT_scalar_block_layout : require #define NIS_GLSL 1 #define NIS_USE_HALF_PRECISION 1 #define NIS_SCALER 1 #include "descriptor_set.h" layout(binding = 0, scalar) uniform const_buffer { float kDetectRatio; float kDetectThres; float kMinContrastRatio; float kRatioNorm; float kContrastBoost; float kEps; float kSharpStartY; float kSharpScaleY; float kSharpStrengthMin; float kSharpStrengthScale; float kSharpLimitMin; float kSharpLimitScale; float kScaleX; float kScaleY; float kDstNormX; float kDstNormY; float kSrcNormX; float kSrcNormY; uint kInputViewportOriginX; uint kInputViewportOriginY; uint kInputViewportWidth; uint kInputViewportHeight; uint kOutputViewportOriginX; uint kOutputViewportOriginY; uint kOutputViewportWidth; uint kOutputViewportHeight; float reserved0; float reserved1; }; // These are the names the NIS shader uses to access the data needed to do the upscaling #define in_texture s_samplers[0] #define out_texture dst #define coef_scaler s_samplers[VKR_NIS_COEF_SCALER_SLOT] #define coef_usm s_samplers[VKR_NIS_COEF_USM_SLOT] // Gamescope is using combined image samplers so no need to specify a sampler #define sampler2D(x, sampler) (x) #include "NVIDIAImageScaling/NIS/NIS_Scaler.h" layout(local_size_x=NIS_THREAD_GROUP_SIZE, local_size_y = 1, local_size_z = 1) in; void main() { NVScaler(gl_WorkGroupID.xy, gl_LocalInvocationID.x); }ValveSoftware-gamescope-eb620ab/src/shaders/cs_rgb_to_nv12.comp000066400000000000000000000041411502457270500246440ustar00rootroot00000000000000#version 450 #extension GL_GOOGLE_include_directive : require #extension GL_EXT_scalar_block_layout : require #include "descriptor_set.h" layout( local_size_x = 8, local_size_y = 8, local_size_z = 1) in; // NV12 Format is: // YYYYYYYYYYYYYYY... // YYYYYYYYYYYYYYY... // UVUVUVUVUVUVUVU... const uint u_frameId = 0; const uint u_shaderFilter = filter_linear_emulated; const uint u_alphaMode = 0; const float u_linearToNits = 400.0f; const float u_nitsToLinear = 1.0f / 100.0f; const float u_itmSdrNits = 100.f; const float u_itmTargetNits = 1000.f; layout(binding = 0, scalar) uniform layers_t { vec2 u_scale[1]; vec2 u_offset[1]; float u_opacity[1]; mat3x4 u_ctm[1]; mat3x4 u_outputCTM; uint u_borderMask; uvec2 u_halfExtent; }; #include "composite.h" vec4 sampleLayer(uint layerIdx, vec2 uv) { return sampleLayer(s_samplers[layerIdx], layerIdx, uv, true); } vec3 applyColorMatrix(vec3 rgb, mat3x4 matrix) { return vec4(linearToSrgb(rgb), 1.0f) * matrix; } void main() { ivec3 thread_id = ivec3(gl_GlobalInvocationID); // todo: fix if (all(lessThan(thread_id.xy, ivec2(u_halfExtent.x, u_halfExtent.y)))) { vec2 offset_table[4] = { vec2(0, 0), vec2(1, 0), vec2(0, 1), vec2(1, 1), }; ivec2 chroma_uv = thread_id.xy; vec2 luma_uv = vec2(thread_id.xy * 2) + vec2(0.5f, 0.5f); vec3 color[4] = { sampleLayer(0, vec2(luma_uv.x + offset_table[0].x, luma_uv.y + offset_table[0].y)).rgb, sampleLayer(0, vec2(luma_uv.x + offset_table[1].x, luma_uv.y + offset_table[1].y)).rgb, sampleLayer(0, vec2(luma_uv.x + offset_table[2].x, luma_uv.y + offset_table[2].y)).rgb, sampleLayer(0, vec2(luma_uv.x + offset_table[3].x, luma_uv.y + offset_table[3].y)).rgb, }; vec3 avg_color = (color[0] + color[1] + color[2] + color[3]) / 4.0f; vec2 uv = applyColorMatrix(avg_color, u_outputCTM).yz; imageStore(dst_chroma, chroma_uv, vec4(uv, 0.0f, 1.0f)); for (int i = 0; i < 4; i++) { float y = applyColorMatrix(color[i], u_outputCTM).x; imageStore(dst_luma, ivec2(luma_uv + offset_table[i]), vec4(y, 0.0f, 0.0f, 1.0f)); } } } ValveSoftware-gamescope-eb620ab/src/shaders/descriptor_set.h000066400000000000000000000044121502457270500243600ustar00rootroot00000000000000#include "descriptor_set_constants.h" layout(constant_id = 0) const int c_layerCount = 1; layout(constant_id = 1) const uint c_ycbcrMask = 0; layout(constant_id = 2) const uint c_compositing_debug = 0; layout(constant_id = 3) const int c_blur_layer_count = 0; layout(constant_id = 4) const uint c_colorspaceMask = 0; layout(constant_id = 5) const uint c_output_eotf = 0; layout(constant_id = 7) const bool c_itm_enable = false; const int colorspace_linear = 0; const int colorspace_sRGB = 1; const int colorspace_scRGB = 2; const int colorspace_pq = 3; const int colorspace_passthru = 4; const int colorspace_max_bits = 3; const int filter_linear_emulated = 0; const int filter_nearest = 1; const int filter_fsr = 2; const int filter_nis = 3; const int filter_pixel = 4; const int filter_from_view = 255; const int EOTF_Gamma22 = 0; const int EOTF_PQ = 1; const int EOTF_Count = 2; // These can be changed but also modify the ones in renderervulkan.hpp const uint compositedebug_Markers = 1u << 0; const uint compositedebug_PlaneBorders = 1u << 1; const uint compositedebug_Heatmap = 1u << 2; const uint compositedebug_Heatmap_MSWCG = 1u << 3; // If compositedebug_Heatmap is set, use the MS WCG heatmap instead of Lilium const uint compositedebug_Heatmap_Hard = 1u << 4; // If compositedebug_Heatmap is set, use a heatmap with specialized hard flagging const uint compositedebug_Markers_Partial = 1u << 5; // If compositedebug_Heatmap is set, use a heatmap with specialized hard flagging //const uint compositedebug_Tonemap_Reinhard = 1u << 7; // Use Reinhard tonemapping instead of Uncharted. bool checkDebugFlag(uint flag) { return (c_compositing_debug & flag) != 0; } uint get_layer_colorspace(uint layerIdx) { return bitfieldExtract(c_colorspaceMask, int(layerIdx) * colorspace_max_bits, colorspace_max_bits); } layout(binding = 1, rgba8) writeonly uniform image2D dst; // alias layout(binding = 1, rgba8) writeonly uniform image2D dst_luma; layout(binding = 2, rgba8) writeonly uniform image2D dst_chroma; layout(binding = 3) uniform sampler2D s_samplers[VKR_SAMPLER_SLOTS]; layout(binding = 4) uniform sampler2D s_ycbcr_samplers[VKR_SAMPLER_SLOTS]; layout(binding = 5) uniform sampler1D s_shaperLut[VKR_LUT3D_COUNT]; layout(binding = 6) uniform sampler3D s_lut3D[VKR_LUT3D_COUNT]; ValveSoftware-gamescope-eb620ab/src/shaders/descriptor_set_constants.h000066400000000000000000000006101502457270500264500ustar00rootroot00000000000000#ifndef DESCRIPTOR_SET_CONSTANTS_H_ #define DESCRIPTOR_SET_CONSTANTS_H_ #define VKR_TARGET_SLOTS 2u #define VKR_SAMPLER_SLOTS 16u #define VKR_MAX_LAYERS 6u #define VKR_BLUR_EXTRA_SLOT VKR_MAX_LAYERS #define VKR_NIS_COEF_SCALER_SLOT (VKR_BLUR_EXTRA_SLOT + 1u) #define VKR_NIS_COEF_USM_SLOT (VKR_NIS_COEF_SCALER_SLOT + 1u) #define VKR_LUT3D_COUNT 2 // Must match EOTF_Count #endif ValveSoftware-gamescope-eb620ab/src/shaders/ffx_a.h000066400000000000000000005430521502457270500224220ustar00rootroot00000000000000//============================================================================================================================== // // [A] SHADER PORTABILITY 1.20210629 // //============================================================================================================================== // FidelityFX Super Resolution Sample // // Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files(the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions : // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. //------------------------------------------------------------------------------------------------------------------------------ // MIT LICENSE // =========== // Copyright (c) 2014 Michal Drobot (for concepts used in "FLOAT APPROXIMATIONS"). // ----------- // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation // files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, // modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // ----------- // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the // Software. // ----------- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. //------------------------------------------------------------------------------------------------------------------------------ // ABOUT // ===== // Common central point for high-level shading language and C portability for various shader headers. //------------------------------------------------------------------------------------------------------------------------------ // DEFINES // ======= // A_CPU ..... Include the CPU related code. // A_GPU ..... Include the GPU related code. // A_GLSL .... Using GLSL. // A_HLSL .... Using HLSL. // A_HLSL_6_2 Using HLSL 6.2 with new 'uint16_t' and related types (requires '-enable-16bit-types'). // A_NO_16_BIT_CAST Don't use instructions that are not availabe in SPIR-V (needed for running A_HLSL_6_2 on Vulkan) // A_GCC ..... Using a GCC compatible compiler (else assume MSVC compatible compiler by default). // ======= // A_BYTE .... Support 8-bit integer. // A_HALF .... Support 16-bit integer and floating point. // A_LONG .... Support 64-bit integer. // A_DUBL .... Support 64-bit floating point. // ======= // A_WAVE .... Support wave-wide operations. //------------------------------------------------------------------------------------------------------------------------------ // To get #include "ffx_a.h" working in GLSL use '#extension GL_GOOGLE_include_directive:require'. //------------------------------------------------------------------------------------------------------------------------------ // SIMPLIFIED TYPE SYSTEM // ====================== // - All ints will be unsigned with exception of when signed is required. // - Type naming simplified and shortened "A<#components>", // - H = 16-bit float (half) // - F = 32-bit float (float) // - D = 64-bit float (double) // - P = 1-bit integer (predicate, not using bool because 'B' is used for byte) // - B = 8-bit integer (byte) // - W = 16-bit integer (word) // - U = 32-bit integer (unsigned) // - L = 64-bit integer (long) // - Using "AS<#components>" for signed when required. //------------------------------------------------------------------------------------------------------------------------------ // TODO // ==== // - Make sure 'ALerp*(a,b,m)' does 'b*m+(-a*m+a)' (2 ops). //------------------------------------------------------------------------------------------------------------------------------ // CHANGE LOG // ========== // 20200914 - Expanded wave ops and prx code. // 20200713 - Added [ZOL] section, fixed serious bugs in sRGB and Rec.709 color conversion code, etc. //============================================================================================================================== //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // COMMON //============================================================================================================================== #define A_2PI 6.28318530718 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // // // CPU // // //============================================================================================================================== #ifdef A_CPU // Supporting user defined overrides. #ifndef A_RESTRICT #define A_RESTRICT __restrict #endif //------------------------------------------------------------------------------------------------------------------------------ #ifndef A_STATIC #define A_STATIC static inline #endif //------------------------------------------------------------------------------------------------------------------------------ // Same types across CPU and GPU. // Predicate uses 32-bit integer (C friendly bool). typedef uint32_t AP1; typedef float AF1; typedef double AD1; typedef uint8_t AB1; typedef uint16_t AW1; typedef uint32_t AU1; typedef uint64_t AL1; typedef int8_t ASB1; typedef int16_t ASW1; typedef int32_t ASU1; typedef int64_t ASL1; //------------------------------------------------------------------------------------------------------------------------------ #define AD1_(a) ((AD1)(a)) #define AF1_(a) ((AF1)(a)) #define AL1_(a) ((AL1)(a)) #define AU1_(a) ((AU1)(a)) //------------------------------------------------------------------------------------------------------------------------------ #define ASL1_(a) ((ASL1)(a)) #define ASU1_(a) ((ASU1)(a)) //------------------------------------------------------------------------------------------------------------------------------ A_STATIC AU1 AU1_AF1(AF1 a){union{AF1 f;AU1 u;}bits;bits.f=a;return bits.u;} //------------------------------------------------------------------------------------------------------------------------------ #define A_TRUE 1 #define A_FALSE 0 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // // CPU/GPU PORTING // //------------------------------------------------------------------------------------------------------------------------------ // Get CPU and GPU to share all setup code, without duplicate code paths. // This uses a lower-case prefix for special vector constructs. // - In C restrict pointers are used. // - In the shading language, in/inout/out arguments are used. // This depends on the ability to access a vector value in both languages via array syntax (aka color[2]). //============================================================================================================================== //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // VECTOR ARGUMENT/RETURN/INITIALIZATION PORTABILITY //============================================================================================================================== #define retAD2 AD1 * #define retAD3 AD1 * #define retAD4 AD1 * #define retAF2 AF1 * #define retAF3 AF1 * #define retAF4 AF1 * #define retAL2 AL1 * #define retAL3 AL1 * #define retAL4 AL1 * #define retAU2 AU1 * #define retAU3 AU1 * #define retAU4 AU1 * //------------------------------------------------------------------------------------------------------------------------------ #define inAD2 AD1 *A_RESTRICT #define inAD3 AD1 *A_RESTRICT #define inAD4 AD1 *A_RESTRICT #define inAF2 AF1 *A_RESTRICT #define inAF3 AF1 *A_RESTRICT #define inAF4 AF1 *A_RESTRICT #define inAL2 AL1 *A_RESTRICT #define inAL3 AL1 *A_RESTRICT #define inAL4 AL1 *A_RESTRICT #define inAU2 AU1 *A_RESTRICT #define inAU3 AU1 *A_RESTRICT #define inAU4 AU1 *A_RESTRICT //------------------------------------------------------------------------------------------------------------------------------ #define inoutAD2 AD1 *A_RESTRICT #define inoutAD3 AD1 *A_RESTRICT #define inoutAD4 AD1 *A_RESTRICT #define inoutAF2 AF1 *A_RESTRICT #define inoutAF3 AF1 *A_RESTRICT #define inoutAF4 AF1 *A_RESTRICT #define inoutAL2 AL1 *A_RESTRICT #define inoutAL3 AL1 *A_RESTRICT #define inoutAL4 AL1 *A_RESTRICT #define inoutAU2 AU1 *A_RESTRICT #define inoutAU3 AU1 *A_RESTRICT #define inoutAU4 AU1 *A_RESTRICT //------------------------------------------------------------------------------------------------------------------------------ #define outAD2 AD1 *A_RESTRICT #define outAD3 AD1 *A_RESTRICT #define outAD4 AD1 *A_RESTRICT #define outAF2 AF1 *A_RESTRICT #define outAF3 AF1 *A_RESTRICT #define outAF4 AF1 *A_RESTRICT #define outAL2 AL1 *A_RESTRICT #define outAL3 AL1 *A_RESTRICT #define outAL4 AL1 *A_RESTRICT #define outAU2 AU1 *A_RESTRICT #define outAU3 AU1 *A_RESTRICT #define outAU4 AU1 *A_RESTRICT //------------------------------------------------------------------------------------------------------------------------------ #define varAD2(x) AD1 x[2] #define varAD3(x) AD1 x[3] #define varAD4(x) AD1 x[4] #define varAF2(x) AF1 x[2] #define varAF3(x) AF1 x[3] #define varAF4(x) AF1 x[4] #define varAL2(x) AL1 x[2] #define varAL3(x) AL1 x[3] #define varAL4(x) AL1 x[4] #define varAU2(x) AU1 x[2] #define varAU3(x) AU1 x[3] #define varAU4(x) AU1 x[4] //------------------------------------------------------------------------------------------------------------------------------ #define initAD2(x,y) {x,y} #define initAD3(x,y,z) {x,y,z} #define initAD4(x,y,z,w) {x,y,z,w} #define initAF2(x,y) {x,y} #define initAF3(x,y,z) {x,y,z} #define initAF4(x,y,z,w) {x,y,z,w} #define initAL2(x,y) {x,y} #define initAL3(x,y,z) {x,y,z} #define initAL4(x,y,z,w) {x,y,z,w} #define initAU2(x,y) {x,y} #define initAU3(x,y,z) {x,y,z} #define initAU4(x,y,z,w) {x,y,z,w} //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // SCALAR RETURN OPS //------------------------------------------------------------------------------------------------------------------------------ // TODO // ==== // - Replace transcendentals with manual versions. //============================================================================================================================== #ifdef A_GCC A_STATIC AD1 AAbsD1(AD1 a){return __builtin_fabs(a);} A_STATIC AF1 AAbsF1(AF1 a){return __builtin_fabsf(a);} A_STATIC AU1 AAbsSU1(AU1 a){return AU1_(__builtin_abs(ASU1_(a)));} A_STATIC AL1 AAbsSL1(AL1 a){return AL1_(__builtin_llabs(ASL1_(a)));} #else A_STATIC AD1 AAbsD1(AD1 a){return fabs(a);} A_STATIC AF1 AAbsF1(AF1 a){return fabsf(a);} A_STATIC AU1 AAbsSU1(AU1 a){return AU1_(abs(ASU1_(a)));} A_STATIC AL1 AAbsSL1(AL1 a){return AL1_(labs((long)ASL1_(a)));} #endif //------------------------------------------------------------------------------------------------------------------------------ #ifdef A_GCC A_STATIC AD1 ACosD1(AD1 a){return __builtin_cos(a);} A_STATIC AF1 ACosF1(AF1 a){return __builtin_cosf(a);} #else A_STATIC AD1 ACosD1(AD1 a){return cos(a);} A_STATIC AF1 ACosF1(AF1 a){return cosf(a);} #endif //------------------------------------------------------------------------------------------------------------------------------ A_STATIC AD1 ADotD2(inAD2 a,inAD2 b){return a[0]*b[0]+a[1]*b[1];} A_STATIC AD1 ADotD3(inAD3 a,inAD3 b){return a[0]*b[0]+a[1]*b[1]+a[2]*b[2];} A_STATIC AD1 ADotD4(inAD4 a,inAD4 b){return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]+a[3]*b[3];} A_STATIC AF1 ADotF2(inAF2 a,inAF2 b){return a[0]*b[0]+a[1]*b[1];} A_STATIC AF1 ADotF3(inAF3 a,inAF3 b){return a[0]*b[0]+a[1]*b[1]+a[2]*b[2];} A_STATIC AF1 ADotF4(inAF4 a,inAF4 b){return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]+a[3]*b[3];} //------------------------------------------------------------------------------------------------------------------------------ #ifdef A_GCC A_STATIC AD1 AExp2D1(AD1 a){return __builtin_exp2(a);} A_STATIC AF1 AExp2F1(AF1 a){return __builtin_exp2f(a);} #else A_STATIC AD1 AExp2D1(AD1 a){return exp2(a);} A_STATIC AF1 AExp2F1(AF1 a){return exp2f(a);} #endif //------------------------------------------------------------------------------------------------------------------------------ #ifdef A_GCC A_STATIC AD1 AFloorD1(AD1 a){return __builtin_floor(a);} A_STATIC AF1 AFloorF1(AF1 a){return __builtin_floorf(a);} #else A_STATIC AD1 AFloorD1(AD1 a){return floor(a);} A_STATIC AF1 AFloorF1(AF1 a){return floorf(a);} #endif //------------------------------------------------------------------------------------------------------------------------------ A_STATIC AD1 ALerpD1(AD1 a,AD1 b,AD1 c){return b*c+(-a*c+a);} A_STATIC AF1 ALerpF1(AF1 a,AF1 b,AF1 c){return b*c+(-a*c+a);} //------------------------------------------------------------------------------------------------------------------------------ #ifdef A_GCC A_STATIC AD1 ALog2D1(AD1 a){return __builtin_log2(a);} A_STATIC AF1 ALog2F1(AF1 a){return __builtin_log2f(a);} #else A_STATIC AD1 ALog2D1(AD1 a){return log2(a);} A_STATIC AF1 ALog2F1(AF1 a){return log2f(a);} #endif //------------------------------------------------------------------------------------------------------------------------------ A_STATIC AD1 AMaxD1(AD1 a,AD1 b){return a>b?a:b;} A_STATIC AF1 AMaxF1(AF1 a,AF1 b){return a>b?a:b;} A_STATIC AL1 AMaxL1(AL1 a,AL1 b){return a>b?a:b;} A_STATIC AU1 AMaxU1(AU1 a,AU1 b){return a>b?a:b;} //------------------------------------------------------------------------------------------------------------------------------ // These follow the convention that A integer types don't have signage, until they are operated on. A_STATIC AL1 AMaxSL1(AL1 a,AL1 b){return (ASL1_(a)>ASL1_(b))?a:b;} A_STATIC AU1 AMaxSU1(AU1 a,AU1 b){return (ASU1_(a)>ASU1_(b))?a:b;} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC AD1 AMinD1(AD1 a,AD1 b){return a>ASL1_(b));} A_STATIC AU1 AShrSU1(AU1 a,AU1 b){return AU1_(ASU1_(a)>>ASU1_(b));} //------------------------------------------------------------------------------------------------------------------------------ #ifdef A_GCC A_STATIC AD1 ASinD1(AD1 a){return __builtin_sin(a);} A_STATIC AF1 ASinF1(AF1 a){return __builtin_sinf(a);} #else A_STATIC AD1 ASinD1(AD1 a){return sin(a);} A_STATIC AF1 ASinF1(AF1 a){return sinf(a);} #endif //------------------------------------------------------------------------------------------------------------------------------ #ifdef A_GCC A_STATIC AD1 ASqrtD1(AD1 a){return __builtin_sqrt(a);} A_STATIC AF1 ASqrtF1(AF1 a){return __builtin_sqrtf(a);} #else A_STATIC AD1 ASqrtD1(AD1 a){return sqrt(a);} A_STATIC AF1 ASqrtF1(AF1 a){return sqrtf(a);} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // SCALAR RETURN OPS - DEPENDENT //============================================================================================================================== A_STATIC AD1 AClampD1(AD1 x,AD1 n,AD1 m){return AMaxD1(n,AMinD1(x,m));} A_STATIC AF1 AClampF1(AF1 x,AF1 n,AF1 m){return AMaxF1(n,AMinF1(x,m));} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC AD1 AFractD1(AD1 a){return a-AFloorD1(a);} A_STATIC AF1 AFractF1(AF1 a){return a-AFloorF1(a);} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC AD1 APowD1(AD1 a,AD1 b){return AExp2D1(b*ALog2D1(a));} A_STATIC AF1 APowF1(AF1 a,AF1 b){return AExp2F1(b*ALog2F1(a));} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC AD1 ARsqD1(AD1 a){return ARcpD1(ASqrtD1(a));} A_STATIC AF1 ARsqF1(AF1 a){return ARcpF1(ASqrtF1(a));} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC AD1 ASatD1(AD1 a){return AMinD1(1.0,AMaxD1(0.0,a));} A_STATIC AF1 ASatF1(AF1 a){return AMinF1(1.0f,AMaxF1(0.0f,a));} //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // VECTOR OPS //------------------------------------------------------------------------------------------------------------------------------ // These are added as needed for production or prototyping, so not necessarily a complete set. // They follow a convention of taking in a destination and also returning the destination value to increase utility. //============================================================================================================================== A_STATIC retAD2 opAAbsD2(outAD2 d,inAD2 a){d[0]=AAbsD1(a[0]);d[1]=AAbsD1(a[1]);return d;} A_STATIC retAD3 opAAbsD3(outAD3 d,inAD3 a){d[0]=AAbsD1(a[0]);d[1]=AAbsD1(a[1]);d[2]=AAbsD1(a[2]);return d;} A_STATIC retAD4 opAAbsD4(outAD4 d,inAD4 a){d[0]=AAbsD1(a[0]);d[1]=AAbsD1(a[1]);d[2]=AAbsD1(a[2]);d[3]=AAbsD1(a[3]);return d;} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC retAF2 opAAbsF2(outAF2 d,inAF2 a){d[0]=AAbsF1(a[0]);d[1]=AAbsF1(a[1]);return d;} A_STATIC retAF3 opAAbsF3(outAF3 d,inAF3 a){d[0]=AAbsF1(a[0]);d[1]=AAbsF1(a[1]);d[2]=AAbsF1(a[2]);return d;} A_STATIC retAF4 opAAbsF4(outAF4 d,inAF4 a){d[0]=AAbsF1(a[0]);d[1]=AAbsF1(a[1]);d[2]=AAbsF1(a[2]);d[3]=AAbsF1(a[3]);return d;} //============================================================================================================================== A_STATIC retAD2 opAAddD2(outAD2 d,inAD2 a,inAD2 b){d[0]=a[0]+b[0];d[1]=a[1]+b[1];return d;} A_STATIC retAD3 opAAddD3(outAD3 d,inAD3 a,inAD3 b){d[0]=a[0]+b[0];d[1]=a[1]+b[1];d[2]=a[2]+b[2];return d;} A_STATIC retAD4 opAAddD4(outAD4 d,inAD4 a,inAD4 b){d[0]=a[0]+b[0];d[1]=a[1]+b[1];d[2]=a[2]+b[2];d[3]=a[3]+b[3];return d;} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC retAF2 opAAddF2(outAF2 d,inAF2 a,inAF2 b){d[0]=a[0]+b[0];d[1]=a[1]+b[1];return d;} A_STATIC retAF3 opAAddF3(outAF3 d,inAF3 a,inAF3 b){d[0]=a[0]+b[0];d[1]=a[1]+b[1];d[2]=a[2]+b[2];return d;} A_STATIC retAF4 opAAddF4(outAF4 d,inAF4 a,inAF4 b){d[0]=a[0]+b[0];d[1]=a[1]+b[1];d[2]=a[2]+b[2];d[3]=a[3]+b[3];return d;} //============================================================================================================================== A_STATIC retAD2 opAAddOneD2(outAD2 d,inAD2 a,AD1 b){d[0]=a[0]+b;d[1]=a[1]+b;return d;} A_STATIC retAD3 opAAddOneD3(outAD3 d,inAD3 a,AD1 b){d[0]=a[0]+b;d[1]=a[1]+b;d[2]=a[2]+b;return d;} A_STATIC retAD4 opAAddOneD4(outAD4 d,inAD4 a,AD1 b){d[0]=a[0]+b;d[1]=a[1]+b;d[2]=a[2]+b;d[3]=a[3]+b;return d;} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC retAF2 opAAddOneF2(outAF2 d,inAF2 a,AF1 b){d[0]=a[0]+b;d[1]=a[1]+b;return d;} A_STATIC retAF3 opAAddOneF3(outAF3 d,inAF3 a,AF1 b){d[0]=a[0]+b;d[1]=a[1]+b;d[2]=a[2]+b;return d;} A_STATIC retAF4 opAAddOneF4(outAF4 d,inAF4 a,AF1 b){d[0]=a[0]+b;d[1]=a[1]+b;d[2]=a[2]+b;d[3]=a[3]+b;return d;} //============================================================================================================================== A_STATIC retAD2 opACpyD2(outAD2 d,inAD2 a){d[0]=a[0];d[1]=a[1];return d;} A_STATIC retAD3 opACpyD3(outAD3 d,inAD3 a){d[0]=a[0];d[1]=a[1];d[2]=a[2];return d;} A_STATIC retAD4 opACpyD4(outAD4 d,inAD4 a){d[0]=a[0];d[1]=a[1];d[2]=a[2];d[3]=a[3];return d;} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC retAF2 opACpyF2(outAF2 d,inAF2 a){d[0]=a[0];d[1]=a[1];return d;} A_STATIC retAF3 opACpyF3(outAF3 d,inAF3 a){d[0]=a[0];d[1]=a[1];d[2]=a[2];return d;} A_STATIC retAF4 opACpyF4(outAF4 d,inAF4 a){d[0]=a[0];d[1]=a[1];d[2]=a[2];d[3]=a[3];return d;} //============================================================================================================================== A_STATIC retAD2 opALerpD2(outAD2 d,inAD2 a,inAD2 b,inAD2 c){d[0]=ALerpD1(a[0],b[0],c[0]);d[1]=ALerpD1(a[1],b[1],c[1]);return d;} A_STATIC retAD3 opALerpD3(outAD3 d,inAD3 a,inAD3 b,inAD3 c){d[0]=ALerpD1(a[0],b[0],c[0]);d[1]=ALerpD1(a[1],b[1],c[1]);d[2]=ALerpD1(a[2],b[2],c[2]);return d;} A_STATIC retAD4 opALerpD4(outAD4 d,inAD4 a,inAD4 b,inAD4 c){d[0]=ALerpD1(a[0],b[0],c[0]);d[1]=ALerpD1(a[1],b[1],c[1]);d[2]=ALerpD1(a[2],b[2],c[2]);d[3]=ALerpD1(a[3],b[3],c[3]);return d;} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC retAF2 opALerpF2(outAF2 d,inAF2 a,inAF2 b,inAF2 c){d[0]=ALerpF1(a[0],b[0],c[0]);d[1]=ALerpF1(a[1],b[1],c[1]);return d;} A_STATIC retAF3 opALerpF3(outAF3 d,inAF3 a,inAF3 b,inAF3 c){d[0]=ALerpF1(a[0],b[0],c[0]);d[1]=ALerpF1(a[1],b[1],c[1]);d[2]=ALerpF1(a[2],b[2],c[2]);return d;} A_STATIC retAF4 opALerpF4(outAF4 d,inAF4 a,inAF4 b,inAF4 c){d[0]=ALerpF1(a[0],b[0],c[0]);d[1]=ALerpF1(a[1],b[1],c[1]);d[2]=ALerpF1(a[2],b[2],c[2]);d[3]=ALerpF1(a[3],b[3],c[3]);return d;} //============================================================================================================================== A_STATIC retAD2 opALerpOneD2(outAD2 d,inAD2 a,inAD2 b,AD1 c){d[0]=ALerpD1(a[0],b[0],c);d[1]=ALerpD1(a[1],b[1],c);return d;} A_STATIC retAD3 opALerpOneD3(outAD3 d,inAD3 a,inAD3 b,AD1 c){d[0]=ALerpD1(a[0],b[0],c);d[1]=ALerpD1(a[1],b[1],c);d[2]=ALerpD1(a[2],b[2],c);return d;} A_STATIC retAD4 opALerpOneD4(outAD4 d,inAD4 a,inAD4 b,AD1 c){d[0]=ALerpD1(a[0],b[0],c);d[1]=ALerpD1(a[1],b[1],c);d[2]=ALerpD1(a[2],b[2],c);d[3]=ALerpD1(a[3],b[3],c);return d;} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC retAF2 opALerpOneF2(outAF2 d,inAF2 a,inAF2 b,AF1 c){d[0]=ALerpF1(a[0],b[0],c);d[1]=ALerpF1(a[1],b[1],c);return d;} A_STATIC retAF3 opALerpOneF3(outAF3 d,inAF3 a,inAF3 b,AF1 c){d[0]=ALerpF1(a[0],b[0],c);d[1]=ALerpF1(a[1],b[1],c);d[2]=ALerpF1(a[2],b[2],c);return d;} A_STATIC retAF4 opALerpOneF4(outAF4 d,inAF4 a,inAF4 b,AF1 c){d[0]=ALerpF1(a[0],b[0],c);d[1]=ALerpF1(a[1],b[1],c);d[2]=ALerpF1(a[2],b[2],c);d[3]=ALerpF1(a[3],b[3],c);return d;} //============================================================================================================================== A_STATIC retAD2 opAMaxD2(outAD2 d,inAD2 a,inAD2 b){d[0]=AMaxD1(a[0],b[0]);d[1]=AMaxD1(a[1],b[1]);return d;} A_STATIC retAD3 opAMaxD3(outAD3 d,inAD3 a,inAD3 b){d[0]=AMaxD1(a[0],b[0]);d[1]=AMaxD1(a[1],b[1]);d[2]=AMaxD1(a[2],b[2]);return d;} A_STATIC retAD4 opAMaxD4(outAD4 d,inAD4 a,inAD4 b){d[0]=AMaxD1(a[0],b[0]);d[1]=AMaxD1(a[1],b[1]);d[2]=AMaxD1(a[2],b[2]);d[3]=AMaxD1(a[3],b[3]);return d;} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC retAF2 opAMaxF2(outAF2 d,inAF2 a,inAF2 b){d[0]=AMaxF1(a[0],b[0]);d[1]=AMaxF1(a[1],b[1]);return d;} A_STATIC retAF3 opAMaxF3(outAF3 d,inAF3 a,inAF3 b){d[0]=AMaxF1(a[0],b[0]);d[1]=AMaxF1(a[1],b[1]);d[2]=AMaxF1(a[2],b[2]);return d;} A_STATIC retAF4 opAMaxF4(outAF4 d,inAF4 a,inAF4 b){d[0]=AMaxF1(a[0],b[0]);d[1]=AMaxF1(a[1],b[1]);d[2]=AMaxF1(a[2],b[2]);d[3]=AMaxF1(a[3],b[3]);return d;} //============================================================================================================================== A_STATIC retAD2 opAMinD2(outAD2 d,inAD2 a,inAD2 b){d[0]=AMinD1(a[0],b[0]);d[1]=AMinD1(a[1],b[1]);return d;} A_STATIC retAD3 opAMinD3(outAD3 d,inAD3 a,inAD3 b){d[0]=AMinD1(a[0],b[0]);d[1]=AMinD1(a[1],b[1]);d[2]=AMinD1(a[2],b[2]);return d;} A_STATIC retAD4 opAMinD4(outAD4 d,inAD4 a,inAD4 b){d[0]=AMinD1(a[0],b[0]);d[1]=AMinD1(a[1],b[1]);d[2]=AMinD1(a[2],b[2]);d[3]=AMinD1(a[3],b[3]);return d;} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC retAF2 opAMinF2(outAF2 d,inAF2 a,inAF2 b){d[0]=AMinF1(a[0],b[0]);d[1]=AMinF1(a[1],b[1]);return d;} A_STATIC retAF3 opAMinF3(outAF3 d,inAF3 a,inAF3 b){d[0]=AMinF1(a[0],b[0]);d[1]=AMinF1(a[1],b[1]);d[2]=AMinF1(a[2],b[2]);return d;} A_STATIC retAF4 opAMinF4(outAF4 d,inAF4 a,inAF4 b){d[0]=AMinF1(a[0],b[0]);d[1]=AMinF1(a[1],b[1]);d[2]=AMinF1(a[2],b[2]);d[3]=AMinF1(a[3],b[3]);return d;} //============================================================================================================================== A_STATIC retAD2 opAMulD2(outAD2 d,inAD2 a,inAD2 b){d[0]=a[0]*b[0];d[1]=a[1]*b[1];return d;} A_STATIC retAD3 opAMulD3(outAD3 d,inAD3 a,inAD3 b){d[0]=a[0]*b[0];d[1]=a[1]*b[1];d[2]=a[2]*b[2];return d;} A_STATIC retAD4 opAMulD4(outAD4 d,inAD4 a,inAD4 b){d[0]=a[0]*b[0];d[1]=a[1]*b[1];d[2]=a[2]*b[2];d[3]=a[3]*b[3];return d;} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC retAF2 opAMulF2(outAF2 d,inAF2 a,inAF2 b){d[0]=a[0]*b[0];d[1]=a[1]*b[1];return d;} A_STATIC retAF3 opAMulF3(outAF3 d,inAF3 a,inAF3 b){d[0]=a[0]*b[0];d[1]=a[1]*b[1];d[2]=a[2]*b[2];return d;} A_STATIC retAF4 opAMulF4(outAF4 d,inAF4 a,inAF4 b){d[0]=a[0]*b[0];d[1]=a[1]*b[1];d[2]=a[2]*b[2];d[3]=a[3]*b[3];return d;} //============================================================================================================================== A_STATIC retAD2 opAMulOneD2(outAD2 d,inAD2 a,AD1 b){d[0]=a[0]*b;d[1]=a[1]*b;return d;} A_STATIC retAD3 opAMulOneD3(outAD3 d,inAD3 a,AD1 b){d[0]=a[0]*b;d[1]=a[1]*b;d[2]=a[2]*b;return d;} A_STATIC retAD4 opAMulOneD4(outAD4 d,inAD4 a,AD1 b){d[0]=a[0]*b;d[1]=a[1]*b;d[2]=a[2]*b;d[3]=a[3]*b;return d;} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC retAF2 opAMulOneF2(outAF2 d,inAF2 a,AF1 b){d[0]=a[0]*b;d[1]=a[1]*b;return d;} A_STATIC retAF3 opAMulOneF3(outAF3 d,inAF3 a,AF1 b){d[0]=a[0]*b;d[1]=a[1]*b;d[2]=a[2]*b;return d;} A_STATIC retAF4 opAMulOneF4(outAF4 d,inAF4 a,AF1 b){d[0]=a[0]*b;d[1]=a[1]*b;d[2]=a[2]*b;d[3]=a[3]*b;return d;} //============================================================================================================================== A_STATIC retAD2 opANegD2(outAD2 d,inAD2 a){d[0]=-a[0];d[1]=-a[1];return d;} A_STATIC retAD3 opANegD3(outAD3 d,inAD3 a){d[0]=-a[0];d[1]=-a[1];d[2]=-a[2];return d;} A_STATIC retAD4 opANegD4(outAD4 d,inAD4 a){d[0]=-a[0];d[1]=-a[1];d[2]=-a[2];d[3]=-a[3];return d;} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC retAF2 opANegF2(outAF2 d,inAF2 a){d[0]=-a[0];d[1]=-a[1];return d;} A_STATIC retAF3 opANegF3(outAF3 d,inAF3 a){d[0]=-a[0];d[1]=-a[1];d[2]=-a[2];return d;} A_STATIC retAF4 opANegF4(outAF4 d,inAF4 a){d[0]=-a[0];d[1]=-a[1];d[2]=-a[2];d[3]=-a[3];return d;} //============================================================================================================================== A_STATIC retAD2 opARcpD2(outAD2 d,inAD2 a){d[0]=ARcpD1(a[0]);d[1]=ARcpD1(a[1]);return d;} A_STATIC retAD3 opARcpD3(outAD3 d,inAD3 a){d[0]=ARcpD1(a[0]);d[1]=ARcpD1(a[1]);d[2]=ARcpD1(a[2]);return d;} A_STATIC retAD4 opARcpD4(outAD4 d,inAD4 a){d[0]=ARcpD1(a[0]);d[1]=ARcpD1(a[1]);d[2]=ARcpD1(a[2]);d[3]=ARcpD1(a[3]);return d;} //------------------------------------------------------------------------------------------------------------------------------ A_STATIC retAF2 opARcpF2(outAF2 d,inAF2 a){d[0]=ARcpF1(a[0]);d[1]=ARcpF1(a[1]);return d;} A_STATIC retAF3 opARcpF3(outAF3 d,inAF3 a){d[0]=ARcpF1(a[0]);d[1]=ARcpF1(a[1]);d[2]=ARcpF1(a[2]);return d;} A_STATIC retAF4 opARcpF4(outAF4 d,inAF4 a){d[0]=ARcpF1(a[0]);d[1]=ARcpF1(a[1]);d[2]=ARcpF1(a[2]);d[3]=ARcpF1(a[3]);return d;} //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // HALF FLOAT PACKING //============================================================================================================================== // Convert float to half (in lower 16-bits of output). // Same fast technique as documented here: ftp://ftp.fox-toolkit.org/pub/fasthalffloatconversion.pdf // Supports denormals. // Conversion rules are to make computations possibly "safer" on the GPU, // -INF & -NaN -> -65504 // +INF & +NaN -> +65504 A_STATIC AU1 AU1_AH1_AF1(AF1 f){ static AW1 base[512]={ 0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000, 0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000, 0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000, 0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000, 0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000, 0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000, 0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0001,0x0002,0x0004,0x0008,0x0010,0x0020,0x0040,0x0080,0x0100, 0x0200,0x0400,0x0800,0x0c00,0x1000,0x1400,0x1800,0x1c00,0x2000,0x2400,0x2800,0x2c00,0x3000,0x3400,0x3800,0x3c00, 0x4000,0x4400,0x4800,0x4c00,0x5000,0x5400,0x5800,0x5c00,0x6000,0x6400,0x6800,0x6c00,0x7000,0x7400,0x7800,0x7bff, 0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff, 0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff, 0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff, 0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff, 0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff, 0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff, 0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff,0x7bff, 0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000, 0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000, 0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000, 0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000, 0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000, 0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000, 0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8000,0x8001,0x8002,0x8004,0x8008,0x8010,0x8020,0x8040,0x8080,0x8100, 0x8200,0x8400,0x8800,0x8c00,0x9000,0x9400,0x9800,0x9c00,0xa000,0xa400,0xa800,0xac00,0xb000,0xb400,0xb800,0xbc00, 0xc000,0xc400,0xc800,0xcc00,0xd000,0xd400,0xd800,0xdc00,0xe000,0xe400,0xe800,0xec00,0xf000,0xf400,0xf800,0xfbff, 0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff, 0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff, 0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff, 0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff, 0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff, 0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff, 0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff,0xfbff}; static AB1 shift[512]={ 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x17,0x16,0x15,0x14,0x13,0x12,0x11,0x10,0x0f, 0x0e,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d, 0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x17,0x16,0x15,0x14,0x13,0x12,0x11,0x10,0x0f, 0x0e,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d, 0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x0d,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18}; union{AF1 f;AU1 u;}bits;bits.f=f;AU1 u=bits.u;AU1 i=u>>23;return (AU1)(base[i])+((u&0x7fffff)>>shift[i]);} //------------------------------------------------------------------------------------------------------------------------------ // Used to output packed constant. A_STATIC AU1 AU1_AH2_AF2(inAF2 a){return AU1_AH1_AF1(a[0])+(AU1_AH1_AF1(a[1])<<16);} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // // // GLSL // // //============================================================================================================================== #if defined(A_GLSL) && defined(A_GPU) #ifndef A_SKIP_EXT #ifdef A_HALF #extension GL_EXT_shader_16bit_storage:require #extension GL_EXT_shader_explicit_arithmetic_types:require #endif //------------------------------------------------------------------------------------------------------------------------------ #ifdef A_LONG #extension GL_ARB_gpu_shader_int64:require #extension GL_NV_shader_atomic_int64:require #endif //------------------------------------------------------------------------------------------------------------------------------ #ifdef A_WAVE #extension GL_KHR_shader_subgroup_arithmetic:require #extension GL_KHR_shader_subgroup_ballot:require #extension GL_KHR_shader_subgroup_quad:require #extension GL_KHR_shader_subgroup_shuffle:require #endif #endif //============================================================================================================================== #define AP1 bool #define AP2 bvec2 #define AP3 bvec3 #define AP4 bvec4 //------------------------------------------------------------------------------------------------------------------------------ #define AF1 float #define AF2 vec2 #define AF3 vec3 #define AF4 vec4 //------------------------------------------------------------------------------------------------------------------------------ #define AU1 uint #define AU2 uvec2 #define AU3 uvec3 #define AU4 uvec4 //------------------------------------------------------------------------------------------------------------------------------ #define ASU1 int #define ASU2 ivec2 #define ASU3 ivec3 #define ASU4 ivec4 //============================================================================================================================== #define AF1_AU1(x) uintBitsToFloat(AU1(x)) #define AF2_AU2(x) uintBitsToFloat(AU2(x)) #define AF3_AU3(x) uintBitsToFloat(AU3(x)) #define AF4_AU4(x) uintBitsToFloat(AU4(x)) //------------------------------------------------------------------------------------------------------------------------------ #define AU1_AF1(x) floatBitsToUint(AF1(x)) #define AU2_AF2(x) floatBitsToUint(AF2(x)) #define AU3_AF3(x) floatBitsToUint(AF3(x)) #define AU4_AF4(x) floatBitsToUint(AF4(x)) //------------------------------------------------------------------------------------------------------------------------------ AU1 AU1_AH1_AF1_x(AF1 a){return packHalf2x16(AF2(a,0.0));} #define AU1_AH1_AF1(a) AU1_AH1_AF1_x(AF1(a)) //------------------------------------------------------------------------------------------------------------------------------ #define AU1_AH2_AF2 packHalf2x16 #define AU1_AW2Unorm_AF2 packUnorm2x16 #define AU1_AB4Unorm_AF4 packUnorm4x8 //------------------------------------------------------------------------------------------------------------------------------ #define AF2_AH2_AU1 unpackHalf2x16 #define AF2_AW2Unorm_AU1 unpackUnorm2x16 #define AF4_AB4Unorm_AU1 unpackUnorm4x8 //============================================================================================================================== AF1 AF1_x(AF1 a){return AF1(a);} AF2 AF2_x(AF1 a){return AF2(a,a);} AF3 AF3_x(AF1 a){return AF3(a,a,a);} AF4 AF4_x(AF1 a){return AF4(a,a,a,a);} #define AF1_(a) AF1_x(AF1(a)) #define AF2_(a) AF2_x(AF1(a)) #define AF3_(a) AF3_x(AF1(a)) #define AF4_(a) AF4_x(AF1(a)) //------------------------------------------------------------------------------------------------------------------------------ AU1 AU1_x(AU1 a){return AU1(a);} AU2 AU2_x(AU1 a){return AU2(a,a);} AU3 AU3_x(AU1 a){return AU3(a,a,a);} AU4 AU4_x(AU1 a){return AU4(a,a,a,a);} #define AU1_(a) AU1_x(AU1(a)) #define AU2_(a) AU2_x(AU1(a)) #define AU3_(a) AU3_x(AU1(a)) #define AU4_(a) AU4_x(AU1(a)) //============================================================================================================================== AU1 AAbsSU1(AU1 a){return AU1(abs(ASU1(a)));} AU2 AAbsSU2(AU2 a){return AU2(abs(ASU2(a)));} AU3 AAbsSU3(AU3 a){return AU3(abs(ASU3(a)));} AU4 AAbsSU4(AU4 a){return AU4(abs(ASU4(a)));} //------------------------------------------------------------------------------------------------------------------------------ AU1 ABfe(AU1 src,AU1 off,AU1 bits){return bitfieldExtract(src,ASU1(off),ASU1(bits));} AU1 ABfi(AU1 src,AU1 ins,AU1 mask){return (ins&mask)|(src&(~mask));} // Proxy for V_BFI_B32 where the 'mask' is set as 'bits', 'mask=(1<>ASU1(b));} AU2 AShrSU2(AU2 a,AU2 b){return AU2(ASU2(a)>>ASU2(b));} AU3 AShrSU3(AU3 a,AU3 b){return AU3(ASU3(a)>>ASU3(b));} AU4 AShrSU4(AU4 a,AU4 b){return AU4(ASU4(a)>>ASU4(b));} //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // GLSL BYTE //============================================================================================================================== #ifdef A_BYTE #define AB1 uint8_t #define AB2 u8vec2 #define AB3 u8vec3 #define AB4 u8vec4 //------------------------------------------------------------------------------------------------------------------------------ #define ASB1 int8_t #define ASB2 i8vec2 #define ASB3 i8vec3 #define ASB4 i8vec4 //------------------------------------------------------------------------------------------------------------------------------ AB1 AB1_x(AB1 a){return AB1(a);} AB2 AB2_x(AB1 a){return AB2(a,a);} AB3 AB3_x(AB1 a){return AB3(a,a,a);} AB4 AB4_x(AB1 a){return AB4(a,a,a,a);} #define AB1_(a) AB1_x(AB1(a)) #define AB2_(a) AB2_x(AB1(a)) #define AB3_(a) AB3_x(AB1(a)) #define AB4_(a) AB4_x(AB1(a)) #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // GLSL HALF //============================================================================================================================== #ifdef A_HALF #define AH1 float16_t #define AH2 f16vec2 #define AH3 f16vec3 #define AH4 f16vec4 //------------------------------------------------------------------------------------------------------------------------------ #define AW1 uint16_t #define AW2 u16vec2 #define AW3 u16vec3 #define AW4 u16vec4 //------------------------------------------------------------------------------------------------------------------------------ #define ASW1 int16_t #define ASW2 i16vec2 #define ASW3 i16vec3 #define ASW4 i16vec4 //============================================================================================================================== #define AH2_AU1(x) unpackFloat2x16(AU1(x)) AH4 AH4_AU2_x(AU2 x){return AH4(unpackFloat2x16(x.x),unpackFloat2x16(x.y));} #define AH4_AU2(x) AH4_AU2_x(AU2(x)) #define AW2_AU1(x) unpackUint2x16(AU1(x)) #define AW4_AU2(x) unpackUint4x16(pack64(AU2(x))) //------------------------------------------------------------------------------------------------------------------------------ #define AU1_AH2(x) packFloat2x16(AH2(x)) AU2 AU2_AH4_x(AH4 x){return AU2(packFloat2x16(x.xy),packFloat2x16(x.zw));} #define AU2_AH4(x) AU2_AH4_x(AH4(x)) #define AU1_AW2(x) packUint2x16(AW2(x)) #define AU2_AW4(x) unpack32(packUint4x16(AW4(x))) //============================================================================================================================== #define AW1_AH1(x) halfBitsToUint16(AH1(x)) #define AW2_AH2(x) halfBitsToUint16(AH2(x)) #define AW3_AH3(x) halfBitsToUint16(AH3(x)) #define AW4_AH4(x) halfBitsToUint16(AH4(x)) //------------------------------------------------------------------------------------------------------------------------------ #define AH1_AW1(x) uint16BitsToHalf(AW1(x)) #define AH2_AW2(x) uint16BitsToHalf(AW2(x)) #define AH3_AW3(x) uint16BitsToHalf(AW3(x)) #define AH4_AW4(x) uint16BitsToHalf(AW4(x)) //============================================================================================================================== AH1 AH1_x(AH1 a){return AH1(a);} AH2 AH2_x(AH1 a){return AH2(a,a);} AH3 AH3_x(AH1 a){return AH3(a,a,a);} AH4 AH4_x(AH1 a){return AH4(a,a,a,a);} #define AH1_(a) AH1_x(AH1(a)) #define AH2_(a) AH2_x(AH1(a)) #define AH3_(a) AH3_x(AH1(a)) #define AH4_(a) AH4_x(AH1(a)) //------------------------------------------------------------------------------------------------------------------------------ AW1 AW1_x(AW1 a){return AW1(a);} AW2 AW2_x(AW1 a){return AW2(a,a);} AW3 AW3_x(AW1 a){return AW3(a,a,a);} AW4 AW4_x(AW1 a){return AW4(a,a,a,a);} #define AW1_(a) AW1_x(AW1(a)) #define AW2_(a) AW2_x(AW1(a)) #define AW3_(a) AW3_x(AW1(a)) #define AW4_(a) AW4_x(AW1(a)) //============================================================================================================================== AW1 AAbsSW1(AW1 a){return AW1(abs(ASW1(a)));} AW2 AAbsSW2(AW2 a){return AW2(abs(ASW2(a)));} AW3 AAbsSW3(AW3 a){return AW3(abs(ASW3(a)));} AW4 AAbsSW4(AW4 a){return AW4(abs(ASW4(a)));} //------------------------------------------------------------------------------------------------------------------------------ AH1 AClampH1(AH1 x,AH1 n,AH1 m){return clamp(x,n,m);} AH2 AClampH2(AH2 x,AH2 n,AH2 m){return clamp(x,n,m);} AH3 AClampH3(AH3 x,AH3 n,AH3 m){return clamp(x,n,m);} AH4 AClampH4(AH4 x,AH4 n,AH4 m){return clamp(x,n,m);} //------------------------------------------------------------------------------------------------------------------------------ AH1 AFractH1(AH1 x){return fract(x);} AH2 AFractH2(AH2 x){return fract(x);} AH3 AFractH3(AH3 x){return fract(x);} AH4 AFractH4(AH4 x){return fract(x);} //------------------------------------------------------------------------------------------------------------------------------ AH1 ALerpH1(AH1 x,AH1 y,AH1 a){return mix(x,y,a);} AH2 ALerpH2(AH2 x,AH2 y,AH2 a){return mix(x,y,a);} AH3 ALerpH3(AH3 x,AH3 y,AH3 a){return mix(x,y,a);} AH4 ALerpH4(AH4 x,AH4 y,AH4 a){return mix(x,y,a);} //------------------------------------------------------------------------------------------------------------------------------ // No packed version of max3. AH1 AMax3H1(AH1 x,AH1 y,AH1 z){return max(x,max(y,z));} AH2 AMax3H2(AH2 x,AH2 y,AH2 z){return max(x,max(y,z));} AH3 AMax3H3(AH3 x,AH3 y,AH3 z){return max(x,max(y,z));} AH4 AMax3H4(AH4 x,AH4 y,AH4 z){return max(x,max(y,z));} //------------------------------------------------------------------------------------------------------------------------------ AW1 AMaxSW1(AW1 a,AW1 b){return AW1(max(ASU1(a),ASU1(b)));} AW2 AMaxSW2(AW2 a,AW2 b){return AW2(max(ASU2(a),ASU2(b)));} AW3 AMaxSW3(AW3 a,AW3 b){return AW3(max(ASU3(a),ASU3(b)));} AW4 AMaxSW4(AW4 a,AW4 b){return AW4(max(ASU4(a),ASU4(b)));} //------------------------------------------------------------------------------------------------------------------------------ // No packed version of min3. AH1 AMin3H1(AH1 x,AH1 y,AH1 z){return min(x,min(y,z));} AH2 AMin3H2(AH2 x,AH2 y,AH2 z){return min(x,min(y,z));} AH3 AMin3H3(AH3 x,AH3 y,AH3 z){return min(x,min(y,z));} AH4 AMin3H4(AH4 x,AH4 y,AH4 z){return min(x,min(y,z));} //------------------------------------------------------------------------------------------------------------------------------ AW1 AMinSW1(AW1 a,AW1 b){return AW1(min(ASU1(a),ASU1(b)));} AW2 AMinSW2(AW2 a,AW2 b){return AW2(min(ASU2(a),ASU2(b)));} AW3 AMinSW3(AW3 a,AW3 b){return AW3(min(ASU3(a),ASU3(b)));} AW4 AMinSW4(AW4 a,AW4 b){return AW4(min(ASU4(a),ASU4(b)));} //------------------------------------------------------------------------------------------------------------------------------ AH1 ARcpH1(AH1 x){return AH1_(1.0)/x;} AH2 ARcpH2(AH2 x){return AH2_(1.0)/x;} AH3 ARcpH3(AH3 x){return AH3_(1.0)/x;} AH4 ARcpH4(AH4 x){return AH4_(1.0)/x;} //------------------------------------------------------------------------------------------------------------------------------ AH1 ARsqH1(AH1 x){return AH1_(1.0)/sqrt(x);} AH2 ARsqH2(AH2 x){return AH2_(1.0)/sqrt(x);} AH3 ARsqH3(AH3 x){return AH3_(1.0)/sqrt(x);} AH4 ARsqH4(AH4 x){return AH4_(1.0)/sqrt(x);} //------------------------------------------------------------------------------------------------------------------------------ AH1 ASatH1(AH1 x){return clamp(x,AH1_(0.0),AH1_(1.0));} AH2 ASatH2(AH2 x){return clamp(x,AH2_(0.0),AH2_(1.0));} AH3 ASatH3(AH3 x){return clamp(x,AH3_(0.0),AH3_(1.0));} AH4 ASatH4(AH4 x){return clamp(x,AH4_(0.0),AH4_(1.0));} //------------------------------------------------------------------------------------------------------------------------------ AW1 AShrSW1(AW1 a,AW1 b){return AW1(ASW1(a)>>ASW1(b));} AW2 AShrSW2(AW2 a,AW2 b){return AW2(ASW2(a)>>ASW2(b));} AW3 AShrSW3(AW3 a,AW3 b){return AW3(ASW3(a)>>ASW3(b));} AW4 AShrSW4(AW4 a,AW4 b){return AW4(ASW4(a)>>ASW4(b));} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // GLSL DOUBLE //============================================================================================================================== #ifdef A_DUBL #define AD1 double #define AD2 dvec2 #define AD3 dvec3 #define AD4 dvec4 //------------------------------------------------------------------------------------------------------------------------------ AD1 AD1_x(AD1 a){return AD1(a);} AD2 AD2_x(AD1 a){return AD2(a,a);} AD3 AD3_x(AD1 a){return AD3(a,a,a);} AD4 AD4_x(AD1 a){return AD4(a,a,a,a);} #define AD1_(a) AD1_x(AD1(a)) #define AD2_(a) AD2_x(AD1(a)) #define AD3_(a) AD3_x(AD1(a)) #define AD4_(a) AD4_x(AD1(a)) //============================================================================================================================== AD1 AFractD1(AD1 x){return fract(x);} AD2 AFractD2(AD2 x){return fract(x);} AD3 AFractD3(AD3 x){return fract(x);} AD4 AFractD4(AD4 x){return fract(x);} //------------------------------------------------------------------------------------------------------------------------------ AD1 ALerpD1(AD1 x,AD1 y,AD1 a){return mix(x,y,a);} AD2 ALerpD2(AD2 x,AD2 y,AD2 a){return mix(x,y,a);} AD3 ALerpD3(AD3 x,AD3 y,AD3 a){return mix(x,y,a);} AD4 ALerpD4(AD4 x,AD4 y,AD4 a){return mix(x,y,a);} //------------------------------------------------------------------------------------------------------------------------------ AD1 ARcpD1(AD1 x){return AD1_(1.0)/x;} AD2 ARcpD2(AD2 x){return AD2_(1.0)/x;} AD3 ARcpD3(AD3 x){return AD3_(1.0)/x;} AD4 ARcpD4(AD4 x){return AD4_(1.0)/x;} //------------------------------------------------------------------------------------------------------------------------------ AD1 ARsqD1(AD1 x){return AD1_(1.0)/sqrt(x);} AD2 ARsqD2(AD2 x){return AD2_(1.0)/sqrt(x);} AD3 ARsqD3(AD3 x){return AD3_(1.0)/sqrt(x);} AD4 ARsqD4(AD4 x){return AD4_(1.0)/sqrt(x);} //------------------------------------------------------------------------------------------------------------------------------ AD1 ASatD1(AD1 x){return clamp(x,AD1_(0.0),AD1_(1.0));} AD2 ASatD2(AD2 x){return clamp(x,AD2_(0.0),AD2_(1.0));} AD3 ASatD3(AD3 x){return clamp(x,AD3_(0.0),AD3_(1.0));} AD4 ASatD4(AD4 x){return clamp(x,AD4_(0.0),AD4_(1.0));} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // GLSL LONG //============================================================================================================================== #ifdef A_LONG #define AL1 uint64_t #define AL2 u64vec2 #define AL3 u64vec3 #define AL4 u64vec4 //------------------------------------------------------------------------------------------------------------------------------ #define ASL1 int64_t #define ASL2 i64vec2 #define ASL3 i64vec3 #define ASL4 i64vec4 //------------------------------------------------------------------------------------------------------------------------------ #define AL1_AU2(x) packUint2x32(AU2(x)) #define AU2_AL1(x) unpackUint2x32(AL1(x)) //------------------------------------------------------------------------------------------------------------------------------ AL1 AL1_x(AL1 a){return AL1(a);} AL2 AL2_x(AL1 a){return AL2(a,a);} AL3 AL3_x(AL1 a){return AL3(a,a,a);} AL4 AL4_x(AL1 a){return AL4(a,a,a,a);} #define AL1_(a) AL1_x(AL1(a)) #define AL2_(a) AL2_x(AL1(a)) #define AL3_(a) AL3_x(AL1(a)) #define AL4_(a) AL4_x(AL1(a)) //============================================================================================================================== AL1 AAbsSL1(AL1 a){return AL1(abs(ASL1(a)));} AL2 AAbsSL2(AL2 a){return AL2(abs(ASL2(a)));} AL3 AAbsSL3(AL3 a){return AL3(abs(ASL3(a)));} AL4 AAbsSL4(AL4 a){return AL4(abs(ASL4(a)));} //------------------------------------------------------------------------------------------------------------------------------ AL1 AMaxSL1(AL1 a,AL1 b){return AL1(max(ASU1(a),ASU1(b)));} AL2 AMaxSL2(AL2 a,AL2 b){return AL2(max(ASU2(a),ASU2(b)));} AL3 AMaxSL3(AL3 a,AL3 b){return AL3(max(ASU3(a),ASU3(b)));} AL4 AMaxSL4(AL4 a,AL4 b){return AL4(max(ASU4(a),ASU4(b)));} //------------------------------------------------------------------------------------------------------------------------------ AL1 AMinSL1(AL1 a,AL1 b){return AL1(min(ASU1(a),ASU1(b)));} AL2 AMinSL2(AL2 a,AL2 b){return AL2(min(ASU2(a),ASU2(b)));} AL3 AMinSL3(AL3 a,AL3 b){return AL3(min(ASU3(a),ASU3(b)));} AL4 AMinSL4(AL4 a,AL4 b){return AL4(min(ASU4(a),ASU4(b)));} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // WAVE OPERATIONS //============================================================================================================================== #ifdef A_WAVE // Where 'x' must be a compile time literal. AF1 AWaveXorF1(AF1 v,AU1 x){return subgroupShuffleXor(v,x);} AF2 AWaveXorF2(AF2 v,AU1 x){return subgroupShuffleXor(v,x);} AF3 AWaveXorF3(AF3 v,AU1 x){return subgroupShuffleXor(v,x);} AF4 AWaveXorF4(AF4 v,AU1 x){return subgroupShuffleXor(v,x);} AU1 AWaveXorU1(AU1 v,AU1 x){return subgroupShuffleXor(v,x);} AU2 AWaveXorU2(AU2 v,AU1 x){return subgroupShuffleXor(v,x);} AU3 AWaveXorU3(AU3 v,AU1 x){return subgroupShuffleXor(v,x);} AU4 AWaveXorU4(AU4 v,AU1 x){return subgroupShuffleXor(v,x);} //------------------------------------------------------------------------------------------------------------------------------ #ifdef A_HALF AH2 AWaveXorH2(AH2 v,AU1 x){return AH2_AU1(subgroupShuffleXor(AU1_AH2(v),x));} AH4 AWaveXorH4(AH4 v,AU1 x){return AH4_AU2(subgroupShuffleXor(AU2_AH4(v),x));} AW2 AWaveXorW2(AW2 v,AU1 x){return AW2_AU1(subgroupShuffleXor(AU1_AW2(v),x));} AW4 AWaveXorW4(AW4 v,AU1 x){return AW4_AU2(subgroupShuffleXor(AU2_AW4(v),x));} #endif #endif //============================================================================================================================== #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // // // HLSL // // //============================================================================================================================== #if defined(A_HLSL) && defined(A_GPU) #ifdef A_HLSL_6_2 #define AP1 bool #define AP2 bool2 #define AP3 bool3 #define AP4 bool4 //------------------------------------------------------------------------------------------------------------------------------ #define AF1 float32_t #define AF2 float32_t2 #define AF3 float32_t3 #define AF4 float32_t4 //------------------------------------------------------------------------------------------------------------------------------ #define AU1 uint32_t #define AU2 uint32_t2 #define AU3 uint32_t3 #define AU4 uint32_t4 //------------------------------------------------------------------------------------------------------------------------------ #define ASU1 int32_t #define ASU2 int32_t2 #define ASU3 int32_t3 #define ASU4 int32_t4 #else #define AP1 bool #define AP2 bool2 #define AP3 bool3 #define AP4 bool4 //------------------------------------------------------------------------------------------------------------------------------ #define AF1 float #define AF2 float2 #define AF3 float3 #define AF4 float4 //------------------------------------------------------------------------------------------------------------------------------ #define AU1 uint #define AU2 uint2 #define AU3 uint3 #define AU4 uint4 //------------------------------------------------------------------------------------------------------------------------------ #define ASU1 int #define ASU2 int2 #define ASU3 int3 #define ASU4 int4 #endif //============================================================================================================================== #define AF1_AU1(x) asfloat(AU1(x)) #define AF2_AU2(x) asfloat(AU2(x)) #define AF3_AU3(x) asfloat(AU3(x)) #define AF4_AU4(x) asfloat(AU4(x)) //------------------------------------------------------------------------------------------------------------------------------ #define AU1_AF1(x) asuint(AF1(x)) #define AU2_AF2(x) asuint(AF2(x)) #define AU3_AF3(x) asuint(AF3(x)) #define AU4_AF4(x) asuint(AF4(x)) //------------------------------------------------------------------------------------------------------------------------------ AU1 AU1_AH1_AF1_x(AF1 a){return f32tof16(a);} #define AU1_AH1_AF1(a) AU1_AH1_AF1_x(AF1(a)) //------------------------------------------------------------------------------------------------------------------------------ AU1 AU1_AH2_AF2_x(AF2 a){return f32tof16(a.x)|(f32tof16(a.y)<<16);} #define AU1_AH2_AF2(a) AU1_AH2_AF2_x(AF2(a)) #define AU1_AB4Unorm_AF4(x) D3DCOLORtoUBYTE4(AF4(x)) //------------------------------------------------------------------------------------------------------------------------------ AF2 AF2_AH2_AU1_x(AU1 x){return AF2(f16tof32(x&0xFFFF),f16tof32(x>>16));} #define AF2_AH2_AU1(x) AF2_AH2_AU1_x(AU1(x)) //============================================================================================================================== AF1 AF1_x(AF1 a){return AF1(a);} AF2 AF2_x(AF1 a){return AF2(a,a);} AF3 AF3_x(AF1 a){return AF3(a,a,a);} AF4 AF4_x(AF1 a){return AF4(a,a,a,a);} #define AF1_(a) AF1_x(AF1(a)) #define AF2_(a) AF2_x(AF1(a)) #define AF3_(a) AF3_x(AF1(a)) #define AF4_(a) AF4_x(AF1(a)) //------------------------------------------------------------------------------------------------------------------------------ AU1 AU1_x(AU1 a){return AU1(a);} AU2 AU2_x(AU1 a){return AU2(a,a);} AU3 AU3_x(AU1 a){return AU3(a,a,a);} AU4 AU4_x(AU1 a){return AU4(a,a,a,a);} #define AU1_(a) AU1_x(AU1(a)) #define AU2_(a) AU2_x(AU1(a)) #define AU3_(a) AU3_x(AU1(a)) #define AU4_(a) AU4_x(AU1(a)) //============================================================================================================================== AU1 AAbsSU1(AU1 a){return AU1(abs(ASU1(a)));} AU2 AAbsSU2(AU2 a){return AU2(abs(ASU2(a)));} AU3 AAbsSU3(AU3 a){return AU3(abs(ASU3(a)));} AU4 AAbsSU4(AU4 a){return AU4(abs(ASU4(a)));} //------------------------------------------------------------------------------------------------------------------------------ AU1 ABfe(AU1 src,AU1 off,AU1 bits){AU1 mask=(1u<>off)&mask;} AU1 ABfi(AU1 src,AU1 ins,AU1 mask){return (ins&mask)|(src&(~mask));} AU1 ABfiM(AU1 src,AU1 ins,AU1 bits){AU1 mask=(1u<>ASU1(b));} AU2 AShrSU2(AU2 a,AU2 b){return AU2(ASU2(a)>>ASU2(b));} AU3 AShrSU3(AU3 a,AU3 b){return AU3(ASU3(a)>>ASU3(b));} AU4 AShrSU4(AU4 a,AU4 b){return AU4(ASU4(a)>>ASU4(b));} //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // HLSL BYTE //============================================================================================================================== #ifdef A_BYTE #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // HLSL HALF //============================================================================================================================== #ifdef A_HALF #ifdef A_HLSL_6_2 #define AH1 float16_t #define AH2 float16_t2 #define AH3 float16_t3 #define AH4 float16_t4 //------------------------------------------------------------------------------------------------------------------------------ #define AW1 uint16_t #define AW2 uint16_t2 #define AW3 uint16_t3 #define AW4 uint16_t4 //------------------------------------------------------------------------------------------------------------------------------ #define ASW1 int16_t #define ASW2 int16_t2 #define ASW3 int16_t3 #define ASW4 int16_t4 #else #define AH1 min16float #define AH2 min16float2 #define AH3 min16float3 #define AH4 min16float4 //------------------------------------------------------------------------------------------------------------------------------ #define AW1 min16uint #define AW2 min16uint2 #define AW3 min16uint3 #define AW4 min16uint4 //------------------------------------------------------------------------------------------------------------------------------ #define ASW1 min16int #define ASW2 min16int2 #define ASW3 min16int3 #define ASW4 min16int4 #endif //============================================================================================================================== // Need to use manual unpack to get optimal execution (don't use packed types in buffers directly). // Unpack requires this pattern: https://gpuopen.com/first-steps-implementing-fp16/ AH2 AH2_AU1_x(AU1 x){AF2 t=f16tof32(AU2(x&0xFFFF,x>>16));return AH2(t);} AH4 AH4_AU2_x(AU2 x){return AH4(AH2_AU1_x(x.x),AH2_AU1_x(x.y));} AW2 AW2_AU1_x(AU1 x){AU2 t=AU2(x&0xFFFF,x>>16);return AW2(t);} AW4 AW4_AU2_x(AU2 x){return AW4(AW2_AU1_x(x.x),AW2_AU1_x(x.y));} #define AH2_AU1(x) AH2_AU1_x(AU1(x)) #define AH4_AU2(x) AH4_AU2_x(AU2(x)) #define AW2_AU1(x) AW2_AU1_x(AU1(x)) #define AW4_AU2(x) AW4_AU2_x(AU2(x)) //------------------------------------------------------------------------------------------------------------------------------ AU1 AU1_AH2_x(AH2 x){return f32tof16(x.x)+(f32tof16(x.y)<<16);} AU2 AU2_AH4_x(AH4 x){return AU2(AU1_AH2_x(x.xy),AU1_AH2_x(x.zw));} AU1 AU1_AW2_x(AW2 x){return AU1(x.x)+(AU1(x.y)<<16);} AU2 AU2_AW4_x(AW4 x){return AU2(AU1_AW2_x(x.xy),AU1_AW2_x(x.zw));} #define AU1_AH2(x) AU1_AH2_x(AH2(x)) #define AU2_AH4(x) AU2_AH4_x(AH4(x)) #define AU1_AW2(x) AU1_AW2_x(AW2(x)) #define AU2_AW4(x) AU2_AW4_x(AW4(x)) //============================================================================================================================== #if defined(A_HLSL_6_2) && !defined(A_NO_16_BIT_CAST) #define AW1_AH1(x) asuint16(x) #define AW2_AH2(x) asuint16(x) #define AW3_AH3(x) asuint16(x) #define AW4_AH4(x) asuint16(x) #else #define AW1_AH1(a) AW1(f32tof16(AF1(a))) #define AW2_AH2(a) AW2(AW1_AH1((a).x),AW1_AH1((a).y)) #define AW3_AH3(a) AW3(AW1_AH1((a).x),AW1_AH1((a).y),AW1_AH1((a).z)) #define AW4_AH4(a) AW4(AW1_AH1((a).x),AW1_AH1((a).y),AW1_AH1((a).z),AW1_AH1((a).w)) #endif //------------------------------------------------------------------------------------------------------------------------------ #if defined(A_HLSL_6_2) && !defined(A_NO_16_BIT_CAST) #define AH1_AW1(x) asfloat16(x) #define AH2_AW2(x) asfloat16(x) #define AH3_AW3(x) asfloat16(x) #define AH4_AW4(x) asfloat16(x) #else #define AH1_AW1(a) AH1(f16tof32(AU1(a))) #define AH2_AW2(a) AH2(AH1_AW1((a).x),AH1_AW1((a).y)) #define AH3_AW3(a) AH3(AH1_AW1((a).x),AH1_AW1((a).y),AH1_AW1((a).z)) #define AH4_AW4(a) AH4(AH1_AW1((a).x),AH1_AW1((a).y),AH1_AW1((a).z),AH1_AW1((a).w)) #endif //============================================================================================================================== AH1 AH1_x(AH1 a){return AH1(a);} AH2 AH2_x(AH1 a){return AH2(a,a);} AH3 AH3_x(AH1 a){return AH3(a,a,a);} AH4 AH4_x(AH1 a){return AH4(a,a,a,a);} #define AH1_(a) AH1_x(AH1(a)) #define AH2_(a) AH2_x(AH1(a)) #define AH3_(a) AH3_x(AH1(a)) #define AH4_(a) AH4_x(AH1(a)) //------------------------------------------------------------------------------------------------------------------------------ AW1 AW1_x(AW1 a){return AW1(a);} AW2 AW2_x(AW1 a){return AW2(a,a);} AW3 AW3_x(AW1 a){return AW3(a,a,a);} AW4 AW4_x(AW1 a){return AW4(a,a,a,a);} #define AW1_(a) AW1_x(AW1(a)) #define AW2_(a) AW2_x(AW1(a)) #define AW3_(a) AW3_x(AW1(a)) #define AW4_(a) AW4_x(AW1(a)) //============================================================================================================================== AW1 AAbsSW1(AW1 a){return AW1(abs(ASW1(a)));} AW2 AAbsSW2(AW2 a){return AW2(abs(ASW2(a)));} AW3 AAbsSW3(AW3 a){return AW3(abs(ASW3(a)));} AW4 AAbsSW4(AW4 a){return AW4(abs(ASW4(a)));} //------------------------------------------------------------------------------------------------------------------------------ AH1 AClampH1(AH1 x,AH1 n,AH1 m){return max(n,min(x,m));} AH2 AClampH2(AH2 x,AH2 n,AH2 m){return max(n,min(x,m));} AH3 AClampH3(AH3 x,AH3 n,AH3 m){return max(n,min(x,m));} AH4 AClampH4(AH4 x,AH4 n,AH4 m){return max(n,min(x,m));} //------------------------------------------------------------------------------------------------------------------------------ // V_FRACT_F16 (note DX frac() is different). AH1 AFractH1(AH1 x){return x-floor(x);} AH2 AFractH2(AH2 x){return x-floor(x);} AH3 AFractH3(AH3 x){return x-floor(x);} AH4 AFractH4(AH4 x){return x-floor(x);} //------------------------------------------------------------------------------------------------------------------------------ AH1 ALerpH1(AH1 x,AH1 y,AH1 a){return lerp(x,y,a);} AH2 ALerpH2(AH2 x,AH2 y,AH2 a){return lerp(x,y,a);} AH3 ALerpH3(AH3 x,AH3 y,AH3 a){return lerp(x,y,a);} AH4 ALerpH4(AH4 x,AH4 y,AH4 a){return lerp(x,y,a);} //------------------------------------------------------------------------------------------------------------------------------ AH1 AMax3H1(AH1 x,AH1 y,AH1 z){return max(x,max(y,z));} AH2 AMax3H2(AH2 x,AH2 y,AH2 z){return max(x,max(y,z));} AH3 AMax3H3(AH3 x,AH3 y,AH3 z){return max(x,max(y,z));} AH4 AMax3H4(AH4 x,AH4 y,AH4 z){return max(x,max(y,z));} //------------------------------------------------------------------------------------------------------------------------------ AW1 AMaxSW1(AW1 a,AW1 b){return AW1(max(ASU1(a),ASU1(b)));} AW2 AMaxSW2(AW2 a,AW2 b){return AW2(max(ASU2(a),ASU2(b)));} AW3 AMaxSW3(AW3 a,AW3 b){return AW3(max(ASU3(a),ASU3(b)));} AW4 AMaxSW4(AW4 a,AW4 b){return AW4(max(ASU4(a),ASU4(b)));} //------------------------------------------------------------------------------------------------------------------------------ AH1 AMin3H1(AH1 x,AH1 y,AH1 z){return min(x,min(y,z));} AH2 AMin3H2(AH2 x,AH2 y,AH2 z){return min(x,min(y,z));} AH3 AMin3H3(AH3 x,AH3 y,AH3 z){return min(x,min(y,z));} AH4 AMin3H4(AH4 x,AH4 y,AH4 z){return min(x,min(y,z));} //------------------------------------------------------------------------------------------------------------------------------ AW1 AMinSW1(AW1 a,AW1 b){return AW1(min(ASU1(a),ASU1(b)));} AW2 AMinSW2(AW2 a,AW2 b){return AW2(min(ASU2(a),ASU2(b)));} AW3 AMinSW3(AW3 a,AW3 b){return AW3(min(ASU3(a),ASU3(b)));} AW4 AMinSW4(AW4 a,AW4 b){return AW4(min(ASU4(a),ASU4(b)));} //------------------------------------------------------------------------------------------------------------------------------ AH1 ARcpH1(AH1 x){return rcp(x);} AH2 ARcpH2(AH2 x){return rcp(x);} AH3 ARcpH3(AH3 x){return rcp(x);} AH4 ARcpH4(AH4 x){return rcp(x);} //------------------------------------------------------------------------------------------------------------------------------ AH1 ARsqH1(AH1 x){return rsqrt(x);} AH2 ARsqH2(AH2 x){return rsqrt(x);} AH3 ARsqH3(AH3 x){return rsqrt(x);} AH4 ARsqH4(AH4 x){return rsqrt(x);} //------------------------------------------------------------------------------------------------------------------------------ AH1 ASatH1(AH1 x){return saturate(x);} AH2 ASatH2(AH2 x){return saturate(x);} AH3 ASatH3(AH3 x){return saturate(x);} AH4 ASatH4(AH4 x){return saturate(x);} //------------------------------------------------------------------------------------------------------------------------------ AW1 AShrSW1(AW1 a,AW1 b){return AW1(ASW1(a)>>ASW1(b));} AW2 AShrSW2(AW2 a,AW2 b){return AW2(ASW2(a)>>ASW2(b));} AW3 AShrSW3(AW3 a,AW3 b){return AW3(ASW3(a)>>ASW3(b));} AW4 AShrSW4(AW4 a,AW4 b){return AW4(ASW4(a)>>ASW4(b));} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // HLSL DOUBLE //============================================================================================================================== #ifdef A_DUBL #ifdef A_HLSL_6_2 #define AD1 float64_t #define AD2 float64_t2 #define AD3 float64_t3 #define AD4 float64_t4 #else #define AD1 double #define AD2 double2 #define AD3 double3 #define AD4 double4 #endif //------------------------------------------------------------------------------------------------------------------------------ AD1 AD1_x(AD1 a){return AD1(a);} AD2 AD2_x(AD1 a){return AD2(a,a);} AD3 AD3_x(AD1 a){return AD3(a,a,a);} AD4 AD4_x(AD1 a){return AD4(a,a,a,a);} #define AD1_(a) AD1_x(AD1(a)) #define AD2_(a) AD2_x(AD1(a)) #define AD3_(a) AD3_x(AD1(a)) #define AD4_(a) AD4_x(AD1(a)) //============================================================================================================================== AD1 AFractD1(AD1 a){return a-floor(a);} AD2 AFractD2(AD2 a){return a-floor(a);} AD3 AFractD3(AD3 a){return a-floor(a);} AD4 AFractD4(AD4 a){return a-floor(a);} //------------------------------------------------------------------------------------------------------------------------------ AD1 ALerpD1(AD1 x,AD1 y,AD1 a){return lerp(x,y,a);} AD2 ALerpD2(AD2 x,AD2 y,AD2 a){return lerp(x,y,a);} AD3 ALerpD3(AD3 x,AD3 y,AD3 a){return lerp(x,y,a);} AD4 ALerpD4(AD4 x,AD4 y,AD4 a){return lerp(x,y,a);} //------------------------------------------------------------------------------------------------------------------------------ AD1 ARcpD1(AD1 x){return rcp(x);} AD2 ARcpD2(AD2 x){return rcp(x);} AD3 ARcpD3(AD3 x){return rcp(x);} AD4 ARcpD4(AD4 x){return rcp(x);} //------------------------------------------------------------------------------------------------------------------------------ AD1 ARsqD1(AD1 x){return rsqrt(x);} AD2 ARsqD2(AD2 x){return rsqrt(x);} AD3 ARsqD3(AD3 x){return rsqrt(x);} AD4 ARsqD4(AD4 x){return rsqrt(x);} //------------------------------------------------------------------------------------------------------------------------------ AD1 ASatD1(AD1 x){return saturate(x);} AD2 ASatD2(AD2 x){return saturate(x);} AD3 ASatD3(AD3 x){return saturate(x);} AD4 ASatD4(AD4 x){return saturate(x);} #endif //============================================================================================================================== // HLSL WAVE //============================================================================================================================== #ifdef A_WAVE // Where 'x' must be a compile time literal. AF1 AWaveXorF1(AF1 v,AU1 x){return WaveReadLaneAt(v,WaveGetLaneIndex()^x);} AF2 AWaveXorF2(AF2 v,AU1 x){return WaveReadLaneAt(v,WaveGetLaneIndex()^x);} AF3 AWaveXorF3(AF3 v,AU1 x){return WaveReadLaneAt(v,WaveGetLaneIndex()^x);} AF4 AWaveXorF4(AF4 v,AU1 x){return WaveReadLaneAt(v,WaveGetLaneIndex()^x);} AU1 AWaveXorU1(AU1 v,AU1 x){return WaveReadLaneAt(v,WaveGetLaneIndex()^x);} AU2 AWaveXorU1(AU2 v,AU1 x){return WaveReadLaneAt(v,WaveGetLaneIndex()^x);} AU3 AWaveXorU1(AU3 v,AU1 x){return WaveReadLaneAt(v,WaveGetLaneIndex()^x);} AU4 AWaveXorU1(AU4 v,AU1 x){return WaveReadLaneAt(v,WaveGetLaneIndex()^x);} //------------------------------------------------------------------------------------------------------------------------------ #ifdef A_HALF AH2 AWaveXorH2(AH2 v,AU1 x){return AH2_AU1(WaveReadLaneAt(AU1_AH2(v),WaveGetLaneIndex()^x));} AH4 AWaveXorH4(AH4 v,AU1 x){return AH4_AU2(WaveReadLaneAt(AU2_AH4(v),WaveGetLaneIndex()^x));} AW2 AWaveXorW2(AW2 v,AU1 x){return AW2_AU1(WaveReadLaneAt(AU1_AW2(v),WaveGetLaneIndex()^x));} AW4 AWaveXorW4(AW4 v,AU1 x){return AW4_AU1(WaveReadLaneAt(AU1_AW4(v),WaveGetLaneIndex()^x));} #endif #endif //============================================================================================================================== #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // // // GPU COMMON // // //============================================================================================================================== #ifdef A_GPU // Negative and positive infinity. #define A_INFP_F AF1_AU1(0x7f800000u) #define A_INFN_F AF1_AU1(0xff800000u) //------------------------------------------------------------------------------------------------------------------------------ // Copy sign from 's' to positive 'd'. AF1 ACpySgnF1(AF1 d,AF1 s){return AF1_AU1(AU1_AF1(d)|(AU1_AF1(s)&AU1_(0x80000000u)));} AF2 ACpySgnF2(AF2 d,AF2 s){return AF2_AU2(AU2_AF2(d)|(AU2_AF2(s)&AU2_(0x80000000u)));} AF3 ACpySgnF3(AF3 d,AF3 s){return AF3_AU3(AU3_AF3(d)|(AU3_AF3(s)&AU3_(0x80000000u)));} AF4 ACpySgnF4(AF4 d,AF4 s){return AF4_AU4(AU4_AF4(d)|(AU4_AF4(s)&AU4_(0x80000000u)));} //------------------------------------------------------------------------------------------------------------------------------ // Single operation to return (useful to create a mask to use in lerp for branch free logic), // m=NaN := 0 // m>=0 := 0 // m<0 := 1 // Uses the following useful floating point logic, // saturate(+a*(-INF)==-INF) := 0 // saturate( 0*(-INF)== NaN) := 0 // saturate(-a*(-INF)==+INF) := 1 AF1 ASignedF1(AF1 m){return ASatF1(m*AF1_(A_INFN_F));} AF2 ASignedF2(AF2 m){return ASatF2(m*AF2_(A_INFN_F));} AF3 ASignedF3(AF3 m){return ASatF3(m*AF3_(A_INFN_F));} AF4 ASignedF4(AF4 m){return ASatF4(m*AF4_(A_INFN_F));} //------------------------------------------------------------------------------------------------------------------------------ AF1 AGtZeroF1(AF1 m){return ASatF1(m*AF1_(A_INFP_F));} AF2 AGtZeroF2(AF2 m){return ASatF2(m*AF2_(A_INFP_F));} AF3 AGtZeroF3(AF3 m){return ASatF3(m*AF3_(A_INFP_F));} AF4 AGtZeroF4(AF4 m){return ASatF4(m*AF4_(A_INFP_F));} //============================================================================================================================== #ifdef A_HALF #ifdef A_HLSL_6_2 #define A_INFP_H AH1_AW1((uint16_t)0x7c00u) #define A_INFN_H AH1_AW1((uint16_t)0xfc00u) #else #define A_INFP_H AH1_AW1(0x7c00u) #define A_INFN_H AH1_AW1(0xfc00u) #endif //------------------------------------------------------------------------------------------------------------------------------ AH1 ACpySgnH1(AH1 d,AH1 s){return AH1_AW1(AW1_AH1(d)|(AW1_AH1(s)&AW1_(0x8000u)));} AH2 ACpySgnH2(AH2 d,AH2 s){return AH2_AW2(AW2_AH2(d)|(AW2_AH2(s)&AW2_(0x8000u)));} AH3 ACpySgnH3(AH3 d,AH3 s){return AH3_AW3(AW3_AH3(d)|(AW3_AH3(s)&AW3_(0x8000u)));} AH4 ACpySgnH4(AH4 d,AH4 s){return AH4_AW4(AW4_AH4(d)|(AW4_AH4(s)&AW4_(0x8000u)));} //------------------------------------------------------------------------------------------------------------------------------ AH1 ASignedH1(AH1 m){return ASatH1(m*AH1_(A_INFN_H));} AH2 ASignedH2(AH2 m){return ASatH2(m*AH2_(A_INFN_H));} AH3 ASignedH3(AH3 m){return ASatH3(m*AH3_(A_INFN_H));} AH4 ASignedH4(AH4 m){return ASatH4(m*AH4_(A_INFN_H));} //------------------------------------------------------------------------------------------------------------------------------ AH1 AGtZeroH1(AH1 m){return ASatH1(m*AH1_(A_INFP_H));} AH2 AGtZeroH2(AH2 m){return ASatH2(m*AH2_(A_INFP_H));} AH3 AGtZeroH3(AH3 m){return ASatH3(m*AH3_(A_INFP_H));} AH4 AGtZeroH4(AH4 m){return ASatH4(m*AH4_(A_INFP_H));} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // [FIS] FLOAT INTEGER SORTABLE //------------------------------------------------------------------------------------------------------------------------------ // Float to integer sortable. // - If sign bit=0, flip the sign bit (positives). // - If sign bit=1, flip all bits (negatives). // Integer sortable to float. // - If sign bit=1, flip the sign bit (positives). // - If sign bit=0, flip all bits (negatives). // Has nice side effects. // - Larger integers are more positive values. // - Float zero is mapped to center of integers (so clear to integer zero is a nice default for atomic max usage). // Burns 3 ops for conversion {shift,or,xor}. //============================================================================================================================== AU1 AFisToU1(AU1 x){return x^(( AShrSU1(x,AU1_(31)))|AU1_(0x80000000));} AU1 AFisFromU1(AU1 x){return x^((~AShrSU1(x,AU1_(31)))|AU1_(0x80000000));} //------------------------------------------------------------------------------------------------------------------------------ // Just adjust high 16-bit value (useful when upper part of 32-bit word is a 16-bit float value). AU1 AFisToHiU1(AU1 x){return x^(( AShrSU1(x,AU1_(15)))|AU1_(0x80000000));} AU1 AFisFromHiU1(AU1 x){return x^((~AShrSU1(x,AU1_(15)))|AU1_(0x80000000));} //------------------------------------------------------------------------------------------------------------------------------ #ifdef A_HALF AW1 AFisToW1(AW1 x){return x^(( AShrSW1(x,AW1_(15)))|AW1_(0x8000));} AW1 AFisFromW1(AW1 x){return x^((~AShrSW1(x,AW1_(15)))|AW1_(0x8000));} //------------------------------------------------------------------------------------------------------------------------------ AW2 AFisToW2(AW2 x){return x^(( AShrSW2(x,AW2_(15)))|AW2_(0x8000));} AW2 AFisFromW2(AW2 x){return x^((~AShrSW2(x,AW2_(15)))|AW2_(0x8000));} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // [PERM] V_PERM_B32 //------------------------------------------------------------------------------------------------------------------------------ // Support for V_PERM_B32 started in the 3rd generation of GCN. //------------------------------------------------------------------------------------------------------------------------------ // yyyyxxxx - The 'i' input. // 76543210 // ======== // HGFEDCBA - Naming on permutation. //------------------------------------------------------------------------------------------------------------------------------ // TODO // ==== // - Make sure compiler optimizes this. //============================================================================================================================== #ifdef A_HALF AU1 APerm0E0A(AU2 i){return((i.x )&0xffu)|((i.y<<16)&0xff0000u);} AU1 APerm0F0B(AU2 i){return((i.x>> 8)&0xffu)|((i.y<< 8)&0xff0000u);} AU1 APerm0G0C(AU2 i){return((i.x>>16)&0xffu)|((i.y )&0xff0000u);} AU1 APerm0H0D(AU2 i){return((i.x>>24)&0xffu)|((i.y>> 8)&0xff0000u);} //------------------------------------------------------------------------------------------------------------------------------ AU1 APermHGFA(AU2 i){return((i.x )&0x000000ffu)|(i.y&0xffffff00u);} AU1 APermHGFC(AU2 i){return((i.x>>16)&0x000000ffu)|(i.y&0xffffff00u);} AU1 APermHGAE(AU2 i){return((i.x<< 8)&0x0000ff00u)|(i.y&0xffff00ffu);} AU1 APermHGCE(AU2 i){return((i.x>> 8)&0x0000ff00u)|(i.y&0xffff00ffu);} AU1 APermHAFE(AU2 i){return((i.x<<16)&0x00ff0000u)|(i.y&0xff00ffffu);} AU1 APermHCFE(AU2 i){return((i.x )&0x00ff0000u)|(i.y&0xff00ffffu);} AU1 APermAGFE(AU2 i){return((i.x<<24)&0xff000000u)|(i.y&0x00ffffffu);} AU1 APermCGFE(AU2 i){return((i.x<< 8)&0xff000000u)|(i.y&0x00ffffffu);} //------------------------------------------------------------------------------------------------------------------------------ AU1 APermGCEA(AU2 i){return((i.x)&0x00ff00ffu)|((i.y<<8)&0xff00ff00u);} AU1 APermGECA(AU2 i){return(((i.x)&0xffu)|((i.x>>8)&0xff00u)|((i.y<<16)&0xff0000u)|((i.y<<8)&0xff000000u));} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // [BUC] BYTE UNSIGNED CONVERSION //------------------------------------------------------------------------------------------------------------------------------ // Designed to use the optimal conversion, enables the scaling to possibly be factored into other computation. // Works on a range of {0 to A_BUC_<32,16>}, for <32-bit, and 16-bit> respectively. //------------------------------------------------------------------------------------------------------------------------------ // OPCODE NOTES // ============ // GCN does not do UNORM or SNORM for bytes in opcodes. // - V_CVT_F32_UBYTE{0,1,2,3} - Unsigned byte to float. // - V_CVT_PKACC_U8_F32 - Float to unsigned byte (does bit-field insert into 32-bit integer). // V_PERM_B32 does byte packing with ability to zero fill bytes as well. // - Can pull out byte values from two sources, and zero fill upper 8-bits of packed hi and lo. //------------------------------------------------------------------------------------------------------------------------------ // BYTE : FLOAT - ABuc{0,1,2,3}{To,From}U1() - Designed for V_CVT_F32_UBYTE* and V_CVT_PKACCUM_U8_F32 ops. // ==== ===== // 0 : 0 // 1 : 1 // ... // 255 : 255 // : 256 (just outside the encoding range) //------------------------------------------------------------------------------------------------------------------------------ // BYTE : FLOAT - ABuc{0,1,2,3}{To,From}U2() - Designed for 16-bit denormal tricks and V_PERM_B32. // ==== ===== // 0 : 0 // 1 : 1/512 // 2 : 1/256 // ... // 64 : 1/8 // 128 : 1/4 // 255 : 255/512 // : 1/2 (just outside the encoding range) //------------------------------------------------------------------------------------------------------------------------------ // OPTIMAL IMPLEMENTATIONS ON AMD ARCHITECTURES // ============================================ // r=ABuc0FromU1(i) // V_CVT_F32_UBYTE0 r,i // -------------------------------------------- // r=ABuc0ToU1(d,i) // V_CVT_PKACCUM_U8_F32 r,i,0,d // -------------------------------------------- // d=ABuc0FromU2(i) // Where 'k0' is an SGPR with 0x0E0A // Where 'k1' is an SGPR with {32768.0} packed into the lower 16-bits // V_PERM_B32 d,i.x,i.y,k0 // V_PK_FMA_F16 d,d,k1.x,0 // -------------------------------------------- // r=ABuc0ToU2(d,i) // Where 'k0' is an SGPR with {1.0/32768.0} packed into the lower 16-bits // Where 'k1' is an SGPR with 0x???? // Where 'k2' is an SGPR with 0x???? // V_PK_FMA_F16 i,i,k0.x,0 // V_PERM_B32 r.x,i,i,k1 // V_PERM_B32 r.y,i,i,k2 //============================================================================================================================== // Peak range for 32-bit and 16-bit operations. #define A_BUC_32 (255.0) #define A_BUC_16 (255.0/512.0) //============================================================================================================================== #if 1 // Designed to be one V_CVT_PKACCUM_U8_F32. // The extra min is required to pattern match to V_CVT_PKACCUM_U8_F32. AU1 ABuc0ToU1(AU1 d,AF1 i){return (d&0xffffff00u)|((min(AU1(i),255u) )&(0x000000ffu));} AU1 ABuc1ToU1(AU1 d,AF1 i){return (d&0xffff00ffu)|((min(AU1(i),255u)<< 8)&(0x0000ff00u));} AU1 ABuc2ToU1(AU1 d,AF1 i){return (d&0xff00ffffu)|((min(AU1(i),255u)<<16)&(0x00ff0000u));} AU1 ABuc3ToU1(AU1 d,AF1 i){return (d&0x00ffffffu)|((min(AU1(i),255u)<<24)&(0xff000000u));} //------------------------------------------------------------------------------------------------------------------------------ // Designed to be one V_CVT_F32_UBYTE*. AF1 ABuc0FromU1(AU1 i){return AF1((i )&255u);} AF1 ABuc1FromU1(AU1 i){return AF1((i>> 8)&255u);} AF1 ABuc2FromU1(AU1 i){return AF1((i>>16)&255u);} AF1 ABuc3FromU1(AU1 i){return AF1((i>>24)&255u);} #endif //============================================================================================================================== #ifdef A_HALF // Takes {x0,x1} and {y0,y1} and builds {{x0,y0},{x1,y1}}. AW2 ABuc01ToW2(AH2 x,AH2 y){x*=AH2_(1.0/32768.0);y*=AH2_(1.0/32768.0); return AW2_AU1(APermGCEA(AU2(AU1_AW2(AW2_AH2(x)),AU1_AW2(AW2_AH2(y)))));} //------------------------------------------------------------------------------------------------------------------------------ // Designed for 3 ops to do SOA to AOS and conversion. AU2 ABuc0ToU2(AU2 d,AH2 i){AU1 b=AU1_AW2(AW2_AH2(i*AH2_(1.0/32768.0))); return AU2(APermHGFA(AU2(d.x,b)),APermHGFC(AU2(d.y,b)));} AU2 ABuc1ToU2(AU2 d,AH2 i){AU1 b=AU1_AW2(AW2_AH2(i*AH2_(1.0/32768.0))); return AU2(APermHGAE(AU2(d.x,b)),APermHGCE(AU2(d.y,b)));} AU2 ABuc2ToU2(AU2 d,AH2 i){AU1 b=AU1_AW2(AW2_AH2(i*AH2_(1.0/32768.0))); return AU2(APermHAFE(AU2(d.x,b)),APermHCFE(AU2(d.y,b)));} AU2 ABuc3ToU2(AU2 d,AH2 i){AU1 b=AU1_AW2(AW2_AH2(i*AH2_(1.0/32768.0))); return AU2(APermAGFE(AU2(d.x,b)),APermCGFE(AU2(d.y,b)));} //------------------------------------------------------------------------------------------------------------------------------ // Designed for 2 ops to do both AOS to SOA, and conversion. AH2 ABuc0FromU2(AU2 i){return AH2_AW2(AW2_AU1(APerm0E0A(i)))*AH2_(32768.0);} AH2 ABuc1FromU2(AU2 i){return AH2_AW2(AW2_AU1(APerm0F0B(i)))*AH2_(32768.0);} AH2 ABuc2FromU2(AU2 i){return AH2_AW2(AW2_AU1(APerm0G0C(i)))*AH2_(32768.0);} AH2 ABuc3FromU2(AU2 i){return AH2_AW2(AW2_AU1(APerm0H0D(i)))*AH2_(32768.0);} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // [BSC] BYTE SIGNED CONVERSION //------------------------------------------------------------------------------------------------------------------------------ // Similar to [BUC]. // Works on a range of {-/+ A_BSC_<32,16>}, for <32-bit, and 16-bit> respectively. //------------------------------------------------------------------------------------------------------------------------------ // ENCODING (without zero-based encoding) // ======== // 0 = unused (can be used to mean something else) // 1 = lowest value // 128 = exact zero center (zero based encoding // 255 = highest value //------------------------------------------------------------------------------------------------------------------------------ // Zero-based [Zb] flips the MSB bit of the byte (making 128 "exact zero" actually zero). // This is useful if there is a desire for cleared values to decode as zero. //------------------------------------------------------------------------------------------------------------------------------ // BYTE : FLOAT - ABsc{0,1,2,3}{To,From}U2() - Designed for 16-bit denormal tricks and V_PERM_B32. // ==== ===== // 0 : -127/512 (unused) // 1 : -126/512 // 2 : -125/512 // ... // 128 : 0 // ... // 255 : 127/512 // : 1/4 (just outside the encoding range) //============================================================================================================================== // Peak range for 32-bit and 16-bit operations. #define A_BSC_32 (127.0) #define A_BSC_16 (127.0/512.0) //============================================================================================================================== #if 1 AU1 ABsc0ToU1(AU1 d,AF1 i){return (d&0xffffff00u)|((min(AU1(i+128.0),255u) )&(0x000000ffu));} AU1 ABsc1ToU1(AU1 d,AF1 i){return (d&0xffff00ffu)|((min(AU1(i+128.0),255u)<< 8)&(0x0000ff00u));} AU1 ABsc2ToU1(AU1 d,AF1 i){return (d&0xff00ffffu)|((min(AU1(i+128.0),255u)<<16)&(0x00ff0000u));} AU1 ABsc3ToU1(AU1 d,AF1 i){return (d&0x00ffffffu)|((min(AU1(i+128.0),255u)<<24)&(0xff000000u));} //------------------------------------------------------------------------------------------------------------------------------ AU1 ABsc0ToZbU1(AU1 d,AF1 i){return ((d&0xffffff00u)|((min(AU1(trunc(i)+128.0),255u) )&(0x000000ffu)))^0x00000080u;} AU1 ABsc1ToZbU1(AU1 d,AF1 i){return ((d&0xffff00ffu)|((min(AU1(trunc(i)+128.0),255u)<< 8)&(0x0000ff00u)))^0x00008000u;} AU1 ABsc2ToZbU1(AU1 d,AF1 i){return ((d&0xff00ffffu)|((min(AU1(trunc(i)+128.0),255u)<<16)&(0x00ff0000u)))^0x00800000u;} AU1 ABsc3ToZbU1(AU1 d,AF1 i){return ((d&0x00ffffffu)|((min(AU1(trunc(i)+128.0),255u)<<24)&(0xff000000u)))^0x80000000u;} //------------------------------------------------------------------------------------------------------------------------------ AF1 ABsc0FromU1(AU1 i){return AF1((i )&255u)-128.0;} AF1 ABsc1FromU1(AU1 i){return AF1((i>> 8)&255u)-128.0;} AF1 ABsc2FromU1(AU1 i){return AF1((i>>16)&255u)-128.0;} AF1 ABsc3FromU1(AU1 i){return AF1((i>>24)&255u)-128.0;} //------------------------------------------------------------------------------------------------------------------------------ AF1 ABsc0FromZbU1(AU1 i){return AF1(((i )&255u)^0x80u)-128.0;} AF1 ABsc1FromZbU1(AU1 i){return AF1(((i>> 8)&255u)^0x80u)-128.0;} AF1 ABsc2FromZbU1(AU1 i){return AF1(((i>>16)&255u)^0x80u)-128.0;} AF1 ABsc3FromZbU1(AU1 i){return AF1(((i>>24)&255u)^0x80u)-128.0;} #endif //============================================================================================================================== #ifdef A_HALF // Takes {x0,x1} and {y0,y1} and builds {{x0,y0},{x1,y1}}. AW2 ABsc01ToW2(AH2 x,AH2 y){x=x*AH2_(1.0/32768.0)+AH2_(0.25/32768.0);y=y*AH2_(1.0/32768.0)+AH2_(0.25/32768.0); return AW2_AU1(APermGCEA(AU2(AU1_AW2(AW2_AH2(x)),AU1_AW2(AW2_AH2(y)))));} //------------------------------------------------------------------------------------------------------------------------------ AU2 ABsc0ToU2(AU2 d,AH2 i){AU1 b=AU1_AW2(AW2_AH2(i*AH2_(1.0/32768.0)+AH2_(0.25/32768.0))); return AU2(APermHGFA(AU2(d.x,b)),APermHGFC(AU2(d.y,b)));} AU2 ABsc1ToU2(AU2 d,AH2 i){AU1 b=AU1_AW2(AW2_AH2(i*AH2_(1.0/32768.0)+AH2_(0.25/32768.0))); return AU2(APermHGAE(AU2(d.x,b)),APermHGCE(AU2(d.y,b)));} AU2 ABsc2ToU2(AU2 d,AH2 i){AU1 b=AU1_AW2(AW2_AH2(i*AH2_(1.0/32768.0)+AH2_(0.25/32768.0))); return AU2(APermHAFE(AU2(d.x,b)),APermHCFE(AU2(d.y,b)));} AU2 ABsc3ToU2(AU2 d,AH2 i){AU1 b=AU1_AW2(AW2_AH2(i*AH2_(1.0/32768.0)+AH2_(0.25/32768.0))); return AU2(APermAGFE(AU2(d.x,b)),APermCGFE(AU2(d.y,b)));} //------------------------------------------------------------------------------------------------------------------------------ AU2 ABsc0ToZbU2(AU2 d,AH2 i){AU1 b=AU1_AW2(AW2_AH2(i*AH2_(1.0/32768.0)+AH2_(0.25/32768.0)))^0x00800080u; return AU2(APermHGFA(AU2(d.x,b)),APermHGFC(AU2(d.y,b)));} AU2 ABsc1ToZbU2(AU2 d,AH2 i){AU1 b=AU1_AW2(AW2_AH2(i*AH2_(1.0/32768.0)+AH2_(0.25/32768.0)))^0x00800080u; return AU2(APermHGAE(AU2(d.x,b)),APermHGCE(AU2(d.y,b)));} AU2 ABsc2ToZbU2(AU2 d,AH2 i){AU1 b=AU1_AW2(AW2_AH2(i*AH2_(1.0/32768.0)+AH2_(0.25/32768.0)))^0x00800080u; return AU2(APermHAFE(AU2(d.x,b)),APermHCFE(AU2(d.y,b)));} AU2 ABsc3ToZbU2(AU2 d,AH2 i){AU1 b=AU1_AW2(AW2_AH2(i*AH2_(1.0/32768.0)+AH2_(0.25/32768.0)))^0x00800080u; return AU2(APermAGFE(AU2(d.x,b)),APermCGFE(AU2(d.y,b)));} //------------------------------------------------------------------------------------------------------------------------------ AH2 ABsc0FromU2(AU2 i){return AH2_AW2(AW2_AU1(APerm0E0A(i)))*AH2_(32768.0)-AH2_(0.25);} AH2 ABsc1FromU2(AU2 i){return AH2_AW2(AW2_AU1(APerm0F0B(i)))*AH2_(32768.0)-AH2_(0.25);} AH2 ABsc2FromU2(AU2 i){return AH2_AW2(AW2_AU1(APerm0G0C(i)))*AH2_(32768.0)-AH2_(0.25);} AH2 ABsc3FromU2(AU2 i){return AH2_AW2(AW2_AU1(APerm0H0D(i)))*AH2_(32768.0)-AH2_(0.25);} //------------------------------------------------------------------------------------------------------------------------------ AH2 ABsc0FromZbU2(AU2 i){return AH2_AW2(AW2_AU1(APerm0E0A(i)^0x00800080u))*AH2_(32768.0)-AH2_(0.25);} AH2 ABsc1FromZbU2(AU2 i){return AH2_AW2(AW2_AU1(APerm0F0B(i)^0x00800080u))*AH2_(32768.0)-AH2_(0.25);} AH2 ABsc2FromZbU2(AU2 i){return AH2_AW2(AW2_AU1(APerm0G0C(i)^0x00800080u))*AH2_(32768.0)-AH2_(0.25);} AH2 ABsc3FromZbU2(AU2 i){return AH2_AW2(AW2_AU1(APerm0H0D(i)^0x00800080u))*AH2_(32768.0)-AH2_(0.25);} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // HALF APPROXIMATIONS //------------------------------------------------------------------------------------------------------------------------------ // These support only positive inputs. // Did not see value yet in specialization for range. // Using quick testing, ended up mostly getting the same "best" approximation for various ranges. // With hardware that can co-execute transcendentals, the value in approximations could be less than expected. // However from a latency perspective, if execution of a transcendental is 4 clk, with no packed support, -> 8 clk total. // And co-execution would require a compiler interleaving a lot of independent work for packed usage. //------------------------------------------------------------------------------------------------------------------------------ // The one Newton Raphson iteration form of rsq() was skipped (requires 6 ops total). // Same with sqrt(), as this could be x*rsq() (7 ops). //============================================================================================================================== #ifdef A_HALF // Minimize squared error across full positive range, 2 ops. // The 0x1de2 based approximation maps {0 to 1} input maps to < 1 output. AH1 APrxLoSqrtH1(AH1 a){return AH1_AW1((AW1_AH1(a)>>AW1_(1))+AW1_(0x1de2));} AH2 APrxLoSqrtH2(AH2 a){return AH2_AW2((AW2_AH2(a)>>AW2_(1))+AW2_(0x1de2));} AH3 APrxLoSqrtH3(AH3 a){return AH3_AW3((AW3_AH3(a)>>AW3_(1))+AW3_(0x1de2));} AH4 APrxLoSqrtH4(AH4 a){return AH4_AW4((AW4_AH4(a)>>AW4_(1))+AW4_(0x1de2));} //------------------------------------------------------------------------------------------------------------------------------ // Lower precision estimation, 1 op. // Minimize squared error across {smallest normal to 16384.0}. AH1 APrxLoRcpH1(AH1 a){return AH1_AW1(AW1_(0x7784)-AW1_AH1(a));} AH2 APrxLoRcpH2(AH2 a){return AH2_AW2(AW2_(0x7784)-AW2_AH2(a));} AH3 APrxLoRcpH3(AH3 a){return AH3_AW3(AW3_(0x7784)-AW3_AH3(a));} AH4 APrxLoRcpH4(AH4 a){return AH4_AW4(AW4_(0x7784)-AW4_AH4(a));} //------------------------------------------------------------------------------------------------------------------------------ // Medium precision estimation, one Newton Raphson iteration, 3 ops. AH1 APrxMedRcpH1(AH1 a){AH1 b=AH1_AW1(AW1_(0x778d)-AW1_AH1(a));return b*(-b*a+AH1_(2.0));} AH2 APrxMedRcpH2(AH2 a){AH2 b=AH2_AW2(AW2_(0x778d)-AW2_AH2(a));return b*(-b*a+AH2_(2.0));} AH3 APrxMedRcpH3(AH3 a){AH3 b=AH3_AW3(AW3_(0x778d)-AW3_AH3(a));return b*(-b*a+AH3_(2.0));} AH4 APrxMedRcpH4(AH4 a){AH4 b=AH4_AW4(AW4_(0x778d)-AW4_AH4(a));return b*(-b*a+AH4_(2.0));} //------------------------------------------------------------------------------------------------------------------------------ // Minimize squared error across {smallest normal to 16384.0}, 2 ops. AH1 APrxLoRsqH1(AH1 a){return AH1_AW1(AW1_(0x59a3)-(AW1_AH1(a)>>AW1_(1)));} AH2 APrxLoRsqH2(AH2 a){return AH2_AW2(AW2_(0x59a3)-(AW2_AH2(a)>>AW2_(1)));} AH3 APrxLoRsqH3(AH3 a){return AH3_AW3(AW3_(0x59a3)-(AW3_AH3(a)>>AW3_(1)));} AH4 APrxLoRsqH4(AH4 a){return AH4_AW4(AW4_(0x59a3)-(AW4_AH4(a)>>AW4_(1)));} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // FLOAT APPROXIMATIONS //------------------------------------------------------------------------------------------------------------------------------ // Michal Drobot has an excellent presentation on these: "Low Level Optimizations For GCN", // - Idea dates back to SGI, then to Quake 3, etc. // - https://michaldrobot.files.wordpress.com/2014/05/gcn_alu_opt_digitaldragons2014.pdf // - sqrt(x)=rsqrt(x)*x // - rcp(x)=rsqrt(x)*rsqrt(x) for positive x // - https://github.com/michaldrobot/ShaderFastLibs/blob/master/ShaderFastMathLib.h //------------------------------------------------------------------------------------------------------------------------------ // These below are from perhaps less complete searching for optimal. // Used FP16 normal range for testing with +4096 32-bit step size for sampling error. // So these match up well with the half approximations. //============================================================================================================================== AF1 APrxLoSqrtF1(AF1 a){return AF1_AU1((AU1_AF1(a)>>AU1_(1))+AU1_(0x1fbc4639));} AF1 APrxLoRcpF1(AF1 a){return AF1_AU1(AU1_(0x7ef07ebb)-AU1_AF1(a));} AF1 APrxMedRcpF1(AF1 a){AF1 b=AF1_AU1(AU1_(0x7ef19fff)-AU1_AF1(a));return b*(-b*a+AF1_(2.0));} AF1 APrxLoRsqF1(AF1 a){return AF1_AU1(AU1_(0x5f347d74)-(AU1_AF1(a)>>AU1_(1)));} //------------------------------------------------------------------------------------------------------------------------------ AF2 APrxLoSqrtF2(AF2 a){return AF2_AU2((AU2_AF2(a)>>AU2_(1))+AU2_(0x1fbc4639));} AF2 APrxLoRcpF2(AF2 a){return AF2_AU2(AU2_(0x7ef07ebb)-AU2_AF2(a));} AF2 APrxMedRcpF2(AF2 a){AF2 b=AF2_AU2(AU2_(0x7ef19fff)-AU2_AF2(a));return b*(-b*a+AF2_(2.0));} AF2 APrxLoRsqF2(AF2 a){return AF2_AU2(AU2_(0x5f347d74)-(AU2_AF2(a)>>AU2_(1)));} //------------------------------------------------------------------------------------------------------------------------------ AF3 APrxLoSqrtF3(AF3 a){return AF3_AU3((AU3_AF3(a)>>AU3_(1))+AU3_(0x1fbc4639));} AF3 APrxLoRcpF3(AF3 a){return AF3_AU3(AU3_(0x7ef07ebb)-AU3_AF3(a));} AF3 APrxMedRcpF3(AF3 a){AF3 b=AF3_AU3(AU3_(0x7ef19fff)-AU3_AF3(a));return b*(-b*a+AF3_(2.0));} AF3 APrxLoRsqF3(AF3 a){return AF3_AU3(AU3_(0x5f347d74)-(AU3_AF3(a)>>AU3_(1)));} //------------------------------------------------------------------------------------------------------------------------------ AF4 APrxLoSqrtF4(AF4 a){return AF4_AU4((AU4_AF4(a)>>AU4_(1))+AU4_(0x1fbc4639));} AF4 APrxLoRcpF4(AF4 a){return AF4_AU4(AU4_(0x7ef07ebb)-AU4_AF4(a));} AF4 APrxMedRcpF4(AF4 a){AF4 b=AF4_AU4(AU4_(0x7ef19fff)-AU4_AF4(a));return b*(-b*a+AF4_(2.0));} AF4 APrxLoRsqF4(AF4 a){return AF4_AU4(AU4_(0x5f347d74)-(AU4_AF4(a)>>AU4_(1)));} //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // PQ APPROXIMATIONS //------------------------------------------------------------------------------------------------------------------------------ // PQ is very close to x^(1/8). The functions below Use the fast float approximation method to do // PQ<~>Gamma2 (4th power and fast 4th root) and PQ<~>Linear (8th power and fast 8th root). Maximum error is ~0.2%. //============================================================================================================================== // Helpers AF1 Quart(AF1 a) { a = a * a; return a * a;} AF1 Oct(AF1 a) { a = a * a; a = a * a; return a * a; } AF2 Quart(AF2 a) { a = a * a; return a * a; } AF2 Oct(AF2 a) { a = a * a; a = a * a; return a * a; } AF3 Quart(AF3 a) { a = a * a; return a * a; } AF3 Oct(AF3 a) { a = a * a; a = a * a; return a * a; } AF4 Quart(AF4 a) { a = a * a; return a * a; } AF4 Oct(AF4 a) { a = a * a; a = a * a; return a * a; } //------------------------------------------------------------------------------------------------------------------------------ AF1 APrxPQToGamma2(AF1 a) { return Quart(a); } AF1 APrxPQToLinear(AF1 a) { return Oct(a); } AF1 APrxLoGamma2ToPQ(AF1 a) { return AF1_AU1((AU1_AF1(a) >> AU1_(2)) + AU1_(0x2F9A4E46)); } AF1 APrxMedGamma2ToPQ(AF1 a) { AF1 b = AF1_AU1((AU1_AF1(a) >> AU1_(2)) + AU1_(0x2F9A4E46)); AF1 b4 = Quart(b); return b - b * (b4 - a) / (AF1_(4.0) * b4); } AF1 APrxHighGamma2ToPQ(AF1 a) { return sqrt(sqrt(a)); } AF1 APrxLoLinearToPQ(AF1 a) { return AF1_AU1((AU1_AF1(a) >> AU1_(3)) + AU1_(0x378D8723)); } AF1 APrxMedLinearToPQ(AF1 a) { AF1 b = AF1_AU1((AU1_AF1(a) >> AU1_(3)) + AU1_(0x378D8723)); AF1 b8 = Oct(b); return b - b * (b8 - a) / (AF1_(8.0) * b8); } AF1 APrxHighLinearToPQ(AF1 a) { return sqrt(sqrt(sqrt(a))); } //------------------------------------------------------------------------------------------------------------------------------ AF2 APrxPQToGamma2(AF2 a) { return Quart(a); } AF2 APrxPQToLinear(AF2 a) { return Oct(a); } AF2 APrxLoGamma2ToPQ(AF2 a) { return AF2_AU2((AU2_AF2(a) >> AU2_(2)) + AU2_(0x2F9A4E46)); } AF2 APrxMedGamma2ToPQ(AF2 a) { AF2 b = AF2_AU2((AU2_AF2(a) >> AU2_(2)) + AU2_(0x2F9A4E46)); AF2 b4 = Quart(b); return b - b * (b4 - a) / (AF1_(4.0) * b4); } AF2 APrxHighGamma2ToPQ(AF2 a) { return sqrt(sqrt(a)); } AF2 APrxLoLinearToPQ(AF2 a) { return AF2_AU2((AU2_AF2(a) >> AU2_(3)) + AU2_(0x378D8723)); } AF2 APrxMedLinearToPQ(AF2 a) { AF2 b = AF2_AU2((AU2_AF2(a) >> AU2_(3)) + AU2_(0x378D8723)); AF2 b8 = Oct(b); return b - b * (b8 - a) / (AF1_(8.0) * b8); } AF2 APrxHighLinearToPQ(AF2 a) { return sqrt(sqrt(sqrt(a))); } //------------------------------------------------------------------------------------------------------------------------------ AF3 APrxPQToGamma2(AF3 a) { return Quart(a); } AF3 APrxPQToLinear(AF3 a) { return Oct(a); } AF3 APrxLoGamma2ToPQ(AF3 a) { return AF3_AU3((AU3_AF3(a) >> AU3_(2)) + AU3_(0x2F9A4E46)); } AF3 APrxMedGamma2ToPQ(AF3 a) { AF3 b = AF3_AU3((AU3_AF3(a) >> AU3_(2)) + AU3_(0x2F9A4E46)); AF3 b4 = Quart(b); return b - b * (b4 - a) / (AF1_(4.0) * b4); } AF3 APrxHighGamma2ToPQ(AF3 a) { return sqrt(sqrt(a)); } AF3 APrxLoLinearToPQ(AF3 a) { return AF3_AU3((AU3_AF3(a) >> AU3_(3)) + AU3_(0x378D8723)); } AF3 APrxMedLinearToPQ(AF3 a) { AF3 b = AF3_AU3((AU3_AF3(a) >> AU3_(3)) + AU3_(0x378D8723)); AF3 b8 = Oct(b); return b - b * (b8 - a) / (AF1_(8.0) * b8); } AF3 APrxHighLinearToPQ(AF3 a) { return sqrt(sqrt(sqrt(a))); } //------------------------------------------------------------------------------------------------------------------------------ AF4 APrxPQToGamma2(AF4 a) { return Quart(a); } AF4 APrxPQToLinear(AF4 a) { return Oct(a); } AF4 APrxLoGamma2ToPQ(AF4 a) { return AF4_AU4((AU4_AF4(a) >> AU4_(2)) + AU4_(0x2F9A4E46)); } AF4 APrxMedGamma2ToPQ(AF4 a) { AF4 b = AF4_AU4((AU4_AF4(a) >> AU4_(2)) + AU4_(0x2F9A4E46)); AF4 b4 = Quart(b); return b - b * (b4 - a) / (AF1_(4.0) * b4); } AF4 APrxHighGamma2ToPQ(AF4 a) { return sqrt(sqrt(a)); } AF4 APrxLoLinearToPQ(AF4 a) { return AF4_AU4((AU4_AF4(a) >> AU4_(3)) + AU4_(0x378D8723)); } AF4 APrxMedLinearToPQ(AF4 a) { AF4 b = AF4_AU4((AU4_AF4(a) >> AU4_(3)) + AU4_(0x378D8723)); AF4 b8 = Oct(b); return b - b * (b8 - a) / (AF1_(8.0) * b8); } AF4 APrxHighLinearToPQ(AF4 a) { return sqrt(sqrt(sqrt(a))); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // PARABOLIC SIN & COS //------------------------------------------------------------------------------------------------------------------------------ // Approximate answers to transcendental questions. //------------------------------------------------------------------------------------------------------------------------------ //============================================================================================================================== #if 1 // Valid input range is {-1 to 1} representing {0 to 2 pi}. // Output range is {-1/4 to 1/4} representing {-1 to 1}. AF1 APSinF1(AF1 x){return x*abs(x)-x;} // MAD. AF2 APSinF2(AF2 x){return x*abs(x)-x;} AF1 APCosF1(AF1 x){x=AFractF1(x*AF1_(0.5)+AF1_(0.75));x=x*AF1_(2.0)-AF1_(1.0);return APSinF1(x);} // 3x MAD, FRACT AF2 APCosF2(AF2 x){x=AFractF2(x*AF2_(0.5)+AF2_(0.75));x=x*AF2_(2.0)-AF2_(1.0);return APSinF2(x);} AF2 APSinCosF1(AF1 x){AF1 y=AFractF1(x*AF1_(0.5)+AF1_(0.75));y=y*AF1_(2.0)-AF1_(1.0);return APSinF2(AF2(x,y));} #endif //------------------------------------------------------------------------------------------------------------------------------ #ifdef A_HALF // For a packed {sin,cos} pair, // - Native takes 16 clocks and 4 issue slots (no packed transcendentals). // - Parabolic takes 8 clocks and 8 issue slots (only fract is non-packed). AH1 APSinH1(AH1 x){return x*abs(x)-x;} AH2 APSinH2(AH2 x){return x*abs(x)-x;} // AND,FMA AH1 APCosH1(AH1 x){x=AFractH1(x*AH1_(0.5)+AH1_(0.75));x=x*AH1_(2.0)-AH1_(1.0);return APSinH1(x);} AH2 APCosH2(AH2 x){x=AFractH2(x*AH2_(0.5)+AH2_(0.75));x=x*AH2_(2.0)-AH2_(1.0);return APSinH2(x);} // 3x FMA, 2xFRACT, AND AH2 APSinCosH1(AH1 x){AH1 y=AFractH1(x*AH1_(0.5)+AH1_(0.75));y=y*AH1_(2.0)-AH1_(1.0);return APSinH2(AH2(x,y));} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // [ZOL] ZERO ONE LOGIC //------------------------------------------------------------------------------------------------------------------------------ // Conditional free logic designed for easy 16-bit packing, and backwards porting to 32-bit. //------------------------------------------------------------------------------------------------------------------------------ // 0 := false // 1 := true //------------------------------------------------------------------------------------------------------------------------------ // AndNot(x,y) -> !(x&y) .... One op. // AndOr(x,y,z) -> (x&y)|z ... One op. // GtZero(x) -> x>0.0 ..... One op. // Sel(x,y,z) -> x?y:z ..... Two ops, has no precision loss. // Signed(x) -> x<0.0 ..... One op. // ZeroPass(x,y) -> x?0:y ..... Two ops, 'y' is a pass through safe for aliasing as integer. //------------------------------------------------------------------------------------------------------------------------------ // OPTIMIZATION NOTES // ================== // - On Vega to use 2 constants in a packed op, pass in as one AW2 or one AH2 'k.xy' and use as 'k.xx' and 'k.yy'. // For example 'a.xy*k.xx+k.yy'. //============================================================================================================================== #if 1 AU1 AZolAndU1(AU1 x,AU1 y){return min(x,y);} AU2 AZolAndU2(AU2 x,AU2 y){return min(x,y);} AU3 AZolAndU3(AU3 x,AU3 y){return min(x,y);} AU4 AZolAndU4(AU4 x,AU4 y){return min(x,y);} //------------------------------------------------------------------------------------------------------------------------------ AU1 AZolNotU1(AU1 x){return x^AU1_(1);} AU2 AZolNotU2(AU2 x){return x^AU2_(1);} AU3 AZolNotU3(AU3 x){return x^AU3_(1);} AU4 AZolNotU4(AU4 x){return x^AU4_(1);} //------------------------------------------------------------------------------------------------------------------------------ AU1 AZolOrU1(AU1 x,AU1 y){return max(x,y);} AU2 AZolOrU2(AU2 x,AU2 y){return max(x,y);} AU3 AZolOrU3(AU3 x,AU3 y){return max(x,y);} AU4 AZolOrU4(AU4 x,AU4 y){return max(x,y);} //============================================================================================================================== AU1 AZolF1ToU1(AF1 x){return AU1(x);} AU2 AZolF2ToU2(AF2 x){return AU2(x);} AU3 AZolF3ToU3(AF3 x){return AU3(x);} AU4 AZolF4ToU4(AF4 x){return AU4(x);} //------------------------------------------------------------------------------------------------------------------------------ // 2 ops, denormals don't work in 32-bit on PC (and if they are enabled, OMOD is disabled). AU1 AZolNotF1ToU1(AF1 x){return AU1(AF1_(1.0)-x);} AU2 AZolNotF2ToU2(AF2 x){return AU2(AF2_(1.0)-x);} AU3 AZolNotF3ToU3(AF3 x){return AU3(AF3_(1.0)-x);} AU4 AZolNotF4ToU4(AF4 x){return AU4(AF4_(1.0)-x);} //------------------------------------------------------------------------------------------------------------------------------ AF1 AZolU1ToF1(AU1 x){return AF1(x);} AF2 AZolU2ToF2(AU2 x){return AF2(x);} AF3 AZolU3ToF3(AU3 x){return AF3(x);} AF4 AZolU4ToF4(AU4 x){return AF4(x);} //============================================================================================================================== AF1 AZolAndF1(AF1 x,AF1 y){return min(x,y);} AF2 AZolAndF2(AF2 x,AF2 y){return min(x,y);} AF3 AZolAndF3(AF3 x,AF3 y){return min(x,y);} AF4 AZolAndF4(AF4 x,AF4 y){return min(x,y);} //------------------------------------------------------------------------------------------------------------------------------ AF1 ASolAndNotF1(AF1 x,AF1 y){return (-x)*y+AF1_(1.0);} AF2 ASolAndNotF2(AF2 x,AF2 y){return (-x)*y+AF2_(1.0);} AF3 ASolAndNotF3(AF3 x,AF3 y){return (-x)*y+AF3_(1.0);} AF4 ASolAndNotF4(AF4 x,AF4 y){return (-x)*y+AF4_(1.0);} //------------------------------------------------------------------------------------------------------------------------------ AF1 AZolAndOrF1(AF1 x,AF1 y,AF1 z){return ASatF1(x*y+z);} AF2 AZolAndOrF2(AF2 x,AF2 y,AF2 z){return ASatF2(x*y+z);} AF3 AZolAndOrF3(AF3 x,AF3 y,AF3 z){return ASatF3(x*y+z);} AF4 AZolAndOrF4(AF4 x,AF4 y,AF4 z){return ASatF4(x*y+z);} //------------------------------------------------------------------------------------------------------------------------------ AF1 AZolGtZeroF1(AF1 x){return ASatF1(x*AF1_(A_INFP_F));} AF2 AZolGtZeroF2(AF2 x){return ASatF2(x*AF2_(A_INFP_F));} AF3 AZolGtZeroF3(AF3 x){return ASatF3(x*AF3_(A_INFP_F));} AF4 AZolGtZeroF4(AF4 x){return ASatF4(x*AF4_(A_INFP_F));} //------------------------------------------------------------------------------------------------------------------------------ AF1 AZolNotF1(AF1 x){return AF1_(1.0)-x;} AF2 AZolNotF2(AF2 x){return AF2_(1.0)-x;} AF3 AZolNotF3(AF3 x){return AF3_(1.0)-x;} AF4 AZolNotF4(AF4 x){return AF4_(1.0)-x;} //------------------------------------------------------------------------------------------------------------------------------ AF1 AZolOrF1(AF1 x,AF1 y){return max(x,y);} AF2 AZolOrF2(AF2 x,AF2 y){return max(x,y);} AF3 AZolOrF3(AF3 x,AF3 y){return max(x,y);} AF4 AZolOrF4(AF4 x,AF4 y){return max(x,y);} //------------------------------------------------------------------------------------------------------------------------------ AF1 AZolSelF1(AF1 x,AF1 y,AF1 z){AF1 r=(-x)*z+z;return x*y+r;} AF2 AZolSelF2(AF2 x,AF2 y,AF2 z){AF2 r=(-x)*z+z;return x*y+r;} AF3 AZolSelF3(AF3 x,AF3 y,AF3 z){AF3 r=(-x)*z+z;return x*y+r;} AF4 AZolSelF4(AF4 x,AF4 y,AF4 z){AF4 r=(-x)*z+z;return x*y+r;} //------------------------------------------------------------------------------------------------------------------------------ AF1 AZolSignedF1(AF1 x){return ASatF1(x*AF1_(A_INFN_F));} AF2 AZolSignedF2(AF2 x){return ASatF2(x*AF2_(A_INFN_F));} AF3 AZolSignedF3(AF3 x){return ASatF3(x*AF3_(A_INFN_F));} AF4 AZolSignedF4(AF4 x){return ASatF4(x*AF4_(A_INFN_F));} //------------------------------------------------------------------------------------------------------------------------------ AF1 AZolZeroPassF1(AF1 x,AF1 y){return AF1_AU1((AU1_AF1(x)!=AU1_(0))?AU1_(0):AU1_AF1(y));} AF2 AZolZeroPassF2(AF2 x,AF2 y){return AF2_AU2((AU2_AF2(x)!=AU2_(0))?AU2_(0):AU2_AF2(y));} AF3 AZolZeroPassF3(AF3 x,AF3 y){return AF3_AU3((AU3_AF3(x)!=AU3_(0))?AU3_(0):AU3_AF3(y));} AF4 AZolZeroPassF4(AF4 x,AF4 y){return AF4_AU4((AU4_AF4(x)!=AU4_(0))?AU4_(0):AU4_AF4(y));} #endif //============================================================================================================================== #ifdef A_HALF AW1 AZolAndW1(AW1 x,AW1 y){return min(x,y);} AW2 AZolAndW2(AW2 x,AW2 y){return min(x,y);} AW3 AZolAndW3(AW3 x,AW3 y){return min(x,y);} AW4 AZolAndW4(AW4 x,AW4 y){return min(x,y);} //------------------------------------------------------------------------------------------------------------------------------ AW1 AZolNotW1(AW1 x){return x^AW1_(1);} AW2 AZolNotW2(AW2 x){return x^AW2_(1);} AW3 AZolNotW3(AW3 x){return x^AW3_(1);} AW4 AZolNotW4(AW4 x){return x^AW4_(1);} //------------------------------------------------------------------------------------------------------------------------------ AW1 AZolOrW1(AW1 x,AW1 y){return max(x,y);} AW2 AZolOrW2(AW2 x,AW2 y){return max(x,y);} AW3 AZolOrW3(AW3 x,AW3 y){return max(x,y);} AW4 AZolOrW4(AW4 x,AW4 y){return max(x,y);} //============================================================================================================================== // Uses denormal trick. AW1 AZolH1ToW1(AH1 x){return AW1_AH1(x*AH1_AW1(AW1_(1)));} AW2 AZolH2ToW2(AH2 x){return AW2_AH2(x*AH2_AW2(AW2_(1)));} AW3 AZolH3ToW3(AH3 x){return AW3_AH3(x*AH3_AW3(AW3_(1)));} AW4 AZolH4ToW4(AH4 x){return AW4_AH4(x*AH4_AW4(AW4_(1)));} //------------------------------------------------------------------------------------------------------------------------------ // AMD arch lacks a packed conversion opcode. AH1 AZolW1ToH1(AW1 x){return AH1_AW1(x*AW1_AH1(AH1_(1.0)));} AH2 AZolW2ToH2(AW2 x){return AH2_AW2(x*AW2_AH2(AH2_(1.0)));} AH3 AZolW1ToH3(AW3 x){return AH3_AW3(x*AW3_AH3(AH3_(1.0)));} AH4 AZolW2ToH4(AW4 x){return AH4_AW4(x*AW4_AH4(AH4_(1.0)));} //============================================================================================================================== AH1 AZolAndH1(AH1 x,AH1 y){return min(x,y);} AH2 AZolAndH2(AH2 x,AH2 y){return min(x,y);} AH3 AZolAndH3(AH3 x,AH3 y){return min(x,y);} AH4 AZolAndH4(AH4 x,AH4 y){return min(x,y);} //------------------------------------------------------------------------------------------------------------------------------ AH1 ASolAndNotH1(AH1 x,AH1 y){return (-x)*y+AH1_(1.0);} AH2 ASolAndNotH2(AH2 x,AH2 y){return (-x)*y+AH2_(1.0);} AH3 ASolAndNotH3(AH3 x,AH3 y){return (-x)*y+AH3_(1.0);} AH4 ASolAndNotH4(AH4 x,AH4 y){return (-x)*y+AH4_(1.0);} //------------------------------------------------------------------------------------------------------------------------------ AH1 AZolAndOrH1(AH1 x,AH1 y,AH1 z){return ASatH1(x*y+z);} AH2 AZolAndOrH2(AH2 x,AH2 y,AH2 z){return ASatH2(x*y+z);} AH3 AZolAndOrH3(AH3 x,AH3 y,AH3 z){return ASatH3(x*y+z);} AH4 AZolAndOrH4(AH4 x,AH4 y,AH4 z){return ASatH4(x*y+z);} //------------------------------------------------------------------------------------------------------------------------------ AH1 AZolGtZeroH1(AH1 x){return ASatH1(x*AH1_(A_INFP_H));} AH2 AZolGtZeroH2(AH2 x){return ASatH2(x*AH2_(A_INFP_H));} AH3 AZolGtZeroH3(AH3 x){return ASatH3(x*AH3_(A_INFP_H));} AH4 AZolGtZeroH4(AH4 x){return ASatH4(x*AH4_(A_INFP_H));} //------------------------------------------------------------------------------------------------------------------------------ AH1 AZolNotH1(AH1 x){return AH1_(1.0)-x;} AH2 AZolNotH2(AH2 x){return AH2_(1.0)-x;} AH3 AZolNotH3(AH3 x){return AH3_(1.0)-x;} AH4 AZolNotH4(AH4 x){return AH4_(1.0)-x;} //------------------------------------------------------------------------------------------------------------------------------ AH1 AZolOrH1(AH1 x,AH1 y){return max(x,y);} AH2 AZolOrH2(AH2 x,AH2 y){return max(x,y);} AH3 AZolOrH3(AH3 x,AH3 y){return max(x,y);} AH4 AZolOrH4(AH4 x,AH4 y){return max(x,y);} //------------------------------------------------------------------------------------------------------------------------------ AH1 AZolSelH1(AH1 x,AH1 y,AH1 z){AH1 r=(-x)*z+z;return x*y+r;} AH2 AZolSelH2(AH2 x,AH2 y,AH2 z){AH2 r=(-x)*z+z;return x*y+r;} AH3 AZolSelH3(AH3 x,AH3 y,AH3 z){AH3 r=(-x)*z+z;return x*y+r;} AH4 AZolSelH4(AH4 x,AH4 y,AH4 z){AH4 r=(-x)*z+z;return x*y+r;} //------------------------------------------------------------------------------------------------------------------------------ AH1 AZolSignedH1(AH1 x){return ASatH1(x*AH1_(A_INFN_H));} AH2 AZolSignedH2(AH2 x){return ASatH2(x*AH2_(A_INFN_H));} AH3 AZolSignedH3(AH3 x){return ASatH3(x*AH3_(A_INFN_H));} AH4 AZolSignedH4(AH4 x){return ASatH4(x*AH4_(A_INFN_H));} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // COLOR CONVERSIONS //------------------------------------------------------------------------------------------------------------------------------ // These are all linear to/from some other space (where 'linear' has been shortened out of the function name). // So 'ToGamma' is 'LinearToGamma', and 'FromGamma' is 'LinearFromGamma'. // These are branch free implementations. // The AToSrgbF1() function is useful for stores for compute shaders for GPUs without hardware linear->sRGB store conversion. //------------------------------------------------------------------------------------------------------------------------------ // TRANSFER FUNCTIONS // ================== // 709 ..... Rec709 used for some HDTVs // Gamma ... Typically 2.2 for some PC displays, or 2.4-2.5 for CRTs, or 2.2 FreeSync2 native // Pq ...... PQ native for HDR10 // Srgb .... The sRGB output, typical of PC displays, useful for 10-bit output, or storing to 8-bit UNORM without SRGB type // Two ..... Gamma 2.0, fastest conversion (useful for intermediate pass approximations) // Three ... Gamma 3.0, less fast, but good for HDR. //------------------------------------------------------------------------------------------------------------------------------ // KEEPING TO SPEC // =============== // Both Rec.709 and sRGB have a linear segment which as spec'ed would intersect the curved segment 2 times. // (a.) For 8-bit sRGB, steps {0 to 10.3} are in the linear region (4% of the encoding range). // (b.) For 8-bit 709, steps {0 to 20.7} are in the linear region (8% of the encoding range). // Also there is a slight step in the transition regions. // Precision of the coefficients in the spec being the likely cause. // Main usage case of the sRGB code is to do the linear->sRGB converstion in a compute shader before store. // This is to work around lack of hardware (typically only ROP does the conversion for free). // To "correct" the linear segment, would be to introduce error, because hardware decode of sRGB->linear is fixed (and free). // So this header keeps with the spec. // For linear->sRGB transforms, the linear segment in some respects reduces error, because rounding in that region is linear. // Rounding in the curved region in hardware (and fast software code) introduces error due to rounding in non-linear. //------------------------------------------------------------------------------------------------------------------------------ // FOR PQ // ====== // Both input and output is {0.0-1.0}, and where output 1.0 represents 10000.0 cd/m^2. // All constants are only specified to FP32 precision. // External PQ source reference, // - https://github.com/ampas/aces-dev/blob/master/transforms/ctl/utilities/ACESlib.Utilities_Color.a1.0.1.ctl //------------------------------------------------------------------------------------------------------------------------------ // PACKED VERSIONS // =============== // These are the A*H2() functions. // There is no PQ functions as FP16 seemed to not have enough precision for the conversion. // The remaining functions are "good enough" for 8-bit, and maybe 10-bit if not concerned about a few 1-bit errors. // Precision is lowest in the 709 conversion, higher in sRGB, higher still in Two and Gamma (when using 2.2 at least). //------------------------------------------------------------------------------------------------------------------------------ // NOTES // ===== // Could be faster for PQ conversions to be in ALU or a texture lookup depending on usage case. //============================================================================================================================== #if 1 AF1 ATo709F1(AF1 c){AF3 j=AF3(0.018*4.5,4.5,0.45);AF2 k=AF2(1.099,-0.099); return clamp(j.x ,c*j.y ,pow(c,j.z )*k.x +k.y );} AF2 ATo709F2(AF2 c){AF3 j=AF3(0.018*4.5,4.5,0.45);AF2 k=AF2(1.099,-0.099); return clamp(j.xx ,c*j.yy ,pow(c,j.zz )*k.xx +k.yy );} AF3 ATo709F3(AF3 c){AF3 j=AF3(0.018*4.5,4.5,0.45);AF2 k=AF2(1.099,-0.099); return clamp(j.xxx,c*j.yyy,pow(c,j.zzz)*k.xxx+k.yyy);} //------------------------------------------------------------------------------------------------------------------------------ // Note 'rcpX' is '1/x', where the 'x' is what would be used in AFromGamma(). AF1 AToGammaF1(AF1 c,AF1 rcpX){return pow(c,AF1_(rcpX));} AF2 AToGammaF2(AF2 c,AF1 rcpX){return pow(c,AF2_(rcpX));} AF3 AToGammaF3(AF3 c,AF1 rcpX){return pow(c,AF3_(rcpX));} //------------------------------------------------------------------------------------------------------------------------------ AF1 AToPqF1(AF1 x){AF1 p=pow(x,AF1_(0.159302)); return pow((AF1_(0.835938)+AF1_(18.8516)*p)/(AF1_(1.0)+AF1_(18.6875)*p),AF1_(78.8438));} AF2 AToPqF1(AF2 x){AF2 p=pow(x,AF2_(0.159302)); return pow((AF2_(0.835938)+AF2_(18.8516)*p)/(AF2_(1.0)+AF2_(18.6875)*p),AF2_(78.8438));} AF3 AToPqF1(AF3 x){AF3 p=pow(x,AF3_(0.159302)); return pow((AF3_(0.835938)+AF3_(18.8516)*p)/(AF3_(1.0)+AF3_(18.6875)*p),AF3_(78.8438));} //------------------------------------------------------------------------------------------------------------------------------ AF1 AToSrgbF1(AF1 c){AF3 j=AF3(0.0031308*12.92,12.92,1.0/2.4);AF2 k=AF2(1.055,-0.055); return clamp(j.x ,c*j.y ,pow(c,j.z )*k.x +k.y );} AF2 AToSrgbF2(AF2 c){AF3 j=AF3(0.0031308*12.92,12.92,1.0/2.4);AF2 k=AF2(1.055,-0.055); return clamp(j.xx ,c*j.yy ,pow(c,j.zz )*k.xx +k.yy );} AF3 AToSrgbF3(AF3 c){AF3 j=AF3(0.0031308*12.92,12.92,1.0/2.4);AF2 k=AF2(1.055,-0.055); return clamp(j.xxx,c*j.yyy,pow(c,j.zzz)*k.xxx+k.yyy);} //------------------------------------------------------------------------------------------------------------------------------ AF1 AToTwoF1(AF1 c){return sqrt(c);} AF2 AToTwoF2(AF2 c){return sqrt(c);} AF3 AToTwoF3(AF3 c){return sqrt(c);} //------------------------------------------------------------------------------------------------------------------------------ AF1 AToThreeF1(AF1 c){return pow(c,AF1_(1.0/3.0));} AF2 AToThreeF2(AF2 c){return pow(c,AF2_(1.0/3.0));} AF3 AToThreeF3(AF3 c){return pow(c,AF3_(1.0/3.0));} #endif //============================================================================================================================== #if 1 // Unfortunately median won't work here. AF1 AFrom709F1(AF1 c){AF3 j=AF3(0.081/4.5,1.0/4.5,1.0/0.45);AF2 k=AF2(1.0/1.099,0.099/1.099); return AZolSelF1(AZolSignedF1(c-j.x ),c*j.y ,pow(c*k.x +k.y ,j.z ));} AF2 AFrom709F2(AF2 c){AF3 j=AF3(0.081/4.5,1.0/4.5,1.0/0.45);AF2 k=AF2(1.0/1.099,0.099/1.099); return AZolSelF2(AZolSignedF2(c-j.xx ),c*j.yy ,pow(c*k.xx +k.yy ,j.zz ));} AF3 AFrom709F3(AF3 c){AF3 j=AF3(0.081/4.5,1.0/4.5,1.0/0.45);AF2 k=AF2(1.0/1.099,0.099/1.099); return AZolSelF3(AZolSignedF3(c-j.xxx),c*j.yyy,pow(c*k.xxx+k.yyy,j.zzz));} //------------------------------------------------------------------------------------------------------------------------------ AF1 AFromGammaF1(AF1 c,AF1 x){return pow(c,AF1_(x));} AF2 AFromGammaF2(AF2 c,AF1 x){return pow(c,AF2_(x));} AF3 AFromGammaF3(AF3 c,AF1 x){return pow(c,AF3_(x));} //------------------------------------------------------------------------------------------------------------------------------ AF1 AFromPqF1(AF1 x){AF1 p=pow(x,AF1_(0.0126833)); return pow(ASatF1(p-AF1_(0.835938))/(AF1_(18.8516)-AF1_(18.6875)*p),AF1_(6.27739));} AF2 AFromPqF1(AF2 x){AF2 p=pow(x,AF2_(0.0126833)); return pow(ASatF2(p-AF2_(0.835938))/(AF2_(18.8516)-AF2_(18.6875)*p),AF2_(6.27739));} AF3 AFromPqF1(AF3 x){AF3 p=pow(x,AF3_(0.0126833)); return pow(ASatF3(p-AF3_(0.835938))/(AF3_(18.8516)-AF3_(18.6875)*p),AF3_(6.27739));} //------------------------------------------------------------------------------------------------------------------------------ // Unfortunately median won't work here. AF1 AFromSrgbF1(AF1 c){AF3 j=AF3(0.04045/12.92,1.0/12.92,2.4);AF2 k=AF2(1.0/1.055,0.055/1.055); return AZolSelF1(AZolSignedF1(c-j.x ),c*j.y ,pow(c*k.x +k.y ,j.z ));} AF2 AFromSrgbF2(AF2 c){AF3 j=AF3(0.04045/12.92,1.0/12.92,2.4);AF2 k=AF2(1.0/1.055,0.055/1.055); return AZolSelF2(AZolSignedF2(c-j.xx ),c*j.yy ,pow(c*k.xx +k.yy ,j.zz ));} AF3 AFromSrgbF3(AF3 c){AF3 j=AF3(0.04045/12.92,1.0/12.92,2.4);AF2 k=AF2(1.0/1.055,0.055/1.055); return AZolSelF3(AZolSignedF3(c-j.xxx),c*j.yyy,pow(c*k.xxx+k.yyy,j.zzz));} //------------------------------------------------------------------------------------------------------------------------------ AF1 AFromTwoF1(AF1 c){return c*c;} AF2 AFromTwoF2(AF2 c){return c*c;} AF3 AFromTwoF3(AF3 c){return c*c;} //------------------------------------------------------------------------------------------------------------------------------ AF1 AFromThreeF1(AF1 c){return c*c*c;} AF2 AFromThreeF2(AF2 c){return c*c*c;} AF3 AFromThreeF3(AF3 c){return c*c*c;} #endif //============================================================================================================================== #ifdef A_HALF AH1 ATo709H1(AH1 c){AH3 j=AH3(0.018*4.5,4.5,0.45);AH2 k=AH2(1.099,-0.099); return clamp(j.x ,c*j.y ,pow(c,j.z )*k.x +k.y );} AH2 ATo709H2(AH2 c){AH3 j=AH3(0.018*4.5,4.5,0.45);AH2 k=AH2(1.099,-0.099); return clamp(j.xx ,c*j.yy ,pow(c,j.zz )*k.xx +k.yy );} AH3 ATo709H3(AH3 c){AH3 j=AH3(0.018*4.5,4.5,0.45);AH2 k=AH2(1.099,-0.099); return clamp(j.xxx,c*j.yyy,pow(c,j.zzz)*k.xxx+k.yyy);} //------------------------------------------------------------------------------------------------------------------------------ AH1 AToGammaH1(AH1 c,AH1 rcpX){return pow(c,AH1_(rcpX));} AH2 AToGammaH2(AH2 c,AH1 rcpX){return pow(c,AH2_(rcpX));} AH3 AToGammaH3(AH3 c,AH1 rcpX){return pow(c,AH3_(rcpX));} //------------------------------------------------------------------------------------------------------------------------------ AH1 AToSrgbH1(AH1 c){AH3 j=AH3(0.0031308*12.92,12.92,1.0/2.4);AH2 k=AH2(1.055,-0.055); return clamp(j.x ,c*j.y ,pow(c,j.z )*k.x +k.y );} AH2 AToSrgbH2(AH2 c){AH3 j=AH3(0.0031308*12.92,12.92,1.0/2.4);AH2 k=AH2(1.055,-0.055); return clamp(j.xx ,c*j.yy ,pow(c,j.zz )*k.xx +k.yy );} AH3 AToSrgbH3(AH3 c){AH3 j=AH3(0.0031308*12.92,12.92,1.0/2.4);AH2 k=AH2(1.055,-0.055); return clamp(j.xxx,c*j.yyy,pow(c,j.zzz)*k.xxx+k.yyy);} //------------------------------------------------------------------------------------------------------------------------------ AH1 AToTwoH1(AH1 c){return sqrt(c);} AH2 AToTwoH2(AH2 c){return sqrt(c);} AH3 AToTwoH3(AH3 c){return sqrt(c);} //------------------------------------------------------------------------------------------------------------------------------ AH1 AToThreeF1(AH1 c){return pow(c,AH1_(1.0/3.0));} AH2 AToThreeF2(AH2 c){return pow(c,AH2_(1.0/3.0));} AH3 AToThreeF3(AH3 c){return pow(c,AH3_(1.0/3.0));} #endif //============================================================================================================================== #ifdef A_HALF AH1 AFrom709H1(AH1 c){AH3 j=AH3(0.081/4.5,1.0/4.5,1.0/0.45);AH2 k=AH2(1.0/1.099,0.099/1.099); return AZolSelH1(AZolSignedH1(c-j.x ),c*j.y ,pow(c*k.x +k.y ,j.z ));} AH2 AFrom709H2(AH2 c){AH3 j=AH3(0.081/4.5,1.0/4.5,1.0/0.45);AH2 k=AH2(1.0/1.099,0.099/1.099); return AZolSelH2(AZolSignedH2(c-j.xx ),c*j.yy ,pow(c*k.xx +k.yy ,j.zz ));} AH3 AFrom709H3(AH3 c){AH3 j=AH3(0.081/4.5,1.0/4.5,1.0/0.45);AH2 k=AH2(1.0/1.099,0.099/1.099); return AZolSelH3(AZolSignedH3(c-j.xxx),c*j.yyy,pow(c*k.xxx+k.yyy,j.zzz));} //------------------------------------------------------------------------------------------------------------------------------ AH1 AFromGammaH1(AH1 c,AH1 x){return pow(c,AH1_(x));} AH2 AFromGammaH2(AH2 c,AH1 x){return pow(c,AH2_(x));} AH3 AFromGammaH3(AH3 c,AH1 x){return pow(c,AH3_(x));} //------------------------------------------------------------------------------------------------------------------------------ AH1 AHromSrgbF1(AH1 c){AH3 j=AH3(0.04045/12.92,1.0/12.92,2.4);AH2 k=AH2(1.0/1.055,0.055/1.055); return AZolSelH1(AZolSignedH1(c-j.x ),c*j.y ,pow(c*k.x +k.y ,j.z ));} AH2 AHromSrgbF2(AH2 c){AH3 j=AH3(0.04045/12.92,1.0/12.92,2.4);AH2 k=AH2(1.0/1.055,0.055/1.055); return AZolSelH2(AZolSignedH2(c-j.xx ),c*j.yy ,pow(c*k.xx +k.yy ,j.zz ));} AH3 AHromSrgbF3(AH3 c){AH3 j=AH3(0.04045/12.92,1.0/12.92,2.4);AH2 k=AH2(1.0/1.055,0.055/1.055); return AZolSelH3(AZolSignedH3(c-j.xxx),c*j.yyy,pow(c*k.xxx+k.yyy,j.zzz));} //------------------------------------------------------------------------------------------------------------------------------ AH1 AFromTwoH1(AH1 c){return c*c;} AH2 AFromTwoH2(AH2 c){return c*c;} AH3 AFromTwoH3(AH3 c){return c*c;} //------------------------------------------------------------------------------------------------------------------------------ AH1 AFromThreeH1(AH1 c){return c*c*c;} AH2 AFromThreeH2(AH2 c){return c*c*c;} AH3 AFromThreeH3(AH3 c){return c*c*c;} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // CS REMAP //============================================================================================================================== // Simple remap 64x1 to 8x8 with rotated 2x2 pixel quads in quad linear. // 543210 // ====== // ..xxx. // yy...y AU2 ARmp8x8(AU1 a){return AU2(ABfe(a,1u,3u),ABfiM(ABfe(a,3u,3u),a,1u));} //============================================================================================================================== // More complex remap 64x1 to 8x8 which is necessary for 2D wave reductions. // 543210 // ====== // .xx..x // y..yy. // Details, // LANE TO 8x8 MAPPING // =================== // 00 01 08 09 10 11 18 19 // 02 03 0a 0b 12 13 1a 1b // 04 05 0c 0d 14 15 1c 1d // 06 07 0e 0f 16 17 1e 1f // 20 21 28 29 30 31 38 39 // 22 23 2a 2b 32 33 3a 3b // 24 25 2c 2d 34 35 3c 3d // 26 27 2e 2f 36 37 3e 3f AU2 ARmpRed8x8(AU1 a){return AU2(ABfiM(ABfe(a,2u,3u),a,1u),ABfiM(ABfe(a,3u,3u),ABfe(a,1u,2u),2u));} //============================================================================================================================== #ifdef A_HALF AW2 ARmp8x8H(AU1 a){return AW2(ABfe(a,1u,3u),ABfiM(ABfe(a,3u,3u),a,1u));} AW2 ARmpRed8x8H(AU1 a){return AW2(ABfiM(ABfe(a,2u,3u),a,1u),ABfiM(ABfe(a,3u,3u),ABfe(a,1u,2u),2u));} #endif #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // // REFERENCE // //------------------------------------------------------------------------------------------------------------------------------ // IEEE FLOAT RULES // ================ // - saturate(NaN)=0, saturate(-INF)=0, saturate(+INF)=1 // - {+/-}0 * {+/-}INF = NaN // - -INF + (+INF) = NaN // - {+/-}0 / {+/-}0 = NaN // - {+/-}INF / {+/-}INF = NaN // - a<(-0) := sqrt(a) = NaN (a=-0.0 won't NaN) // - 0 == -0 // - 4/0 = +INF // - 4/-0 = -INF // - 4+INF = +INF // - 4-INF = -INF // - 4*(+INF) = +INF // - 4*(-INF) = -INF // - -4*(+INF) = -INF // - sqrt(+INF) = +INF //------------------------------------------------------------------------------------------------------------------------------ // FP16 ENCODING // ============= // fedcba9876543210 // ---------------- // ......mmmmmmmmmm 10-bit mantissa (encodes 11-bit 0.5 to 1.0 except for denormals) // .eeeee.......... 5-bit exponent // .00000.......... denormals // .00001.......... -14 exponent // .11110.......... 15 exponent // .111110000000000 infinity // .11111nnnnnnnnnn NaN with n!=0 // s............... sign //------------------------------------------------------------------------------------------------------------------------------ // FP16/INT16 ALIASING DENORMAL // ============================ // 11-bit unsigned integers alias with half float denormal/normal values, // 1 = 2^(-24) = 1/16777216 ....................... first denormal value // 2 = 2^(-23) // ... // 1023 = 2^(-14)*(1-2^(-10)) = 2^(-14)*(1-1/1024) ... last denormal value // 1024 = 2^(-14) = 1/16384 .......................... first normal value that still maps to integers // 2047 .............................................. last normal value that still maps to integers // Scaling limits, // 2^15 = 32768 ...................................... largest power of 2 scaling // Largest pow2 conversion mapping is at *32768, // 1 : 2^(-9) = 1/512 // 2 : 1/256 // 4 : 1/128 // 8 : 1/64 // 16 : 1/32 // 32 : 1/16 // 64 : 1/8 // 128 : 1/4 // 256 : 1/2 // 512 : 1 // 1024 : 2 // 2047 : a little less than 4 //============================================================================================================================== //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // // // GPU/CPU PORTABILITY // // //------------------------------------------------------------------------------------------------------------------------------ // This is the GPU implementation. // See the CPU implementation for docs. //============================================================================================================================== #ifdef A_GPU #define A_TRUE true #define A_FALSE false #define A_STATIC //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // VECTOR ARGUMENT/RETURN/INITIALIZATION PORTABILITY //============================================================================================================================== #define retAD2 AD2 #define retAD3 AD3 #define retAD4 AD4 #define retAF2 AF2 #define retAF3 AF3 #define retAF4 AF4 #define retAL2 AL2 #define retAL3 AL3 #define retAL4 AL4 #define retAU2 AU2 #define retAU3 AU3 #define retAU4 AU4 //------------------------------------------------------------------------------------------------------------------------------ #define inAD2 in AD2 #define inAD3 in AD3 #define inAD4 in AD4 #define inAF2 in AF2 #define inAF3 in AF3 #define inAF4 in AF4 #define inAL2 in AL2 #define inAL3 in AL3 #define inAL4 in AL4 #define inAU2 in AU2 #define inAU3 in AU3 #define inAU4 in AU4 //------------------------------------------------------------------------------------------------------------------------------ #define inoutAD2 inout AD2 #define inoutAD3 inout AD3 #define inoutAD4 inout AD4 #define inoutAF2 inout AF2 #define inoutAF3 inout AF3 #define inoutAF4 inout AF4 #define inoutAL2 inout AL2 #define inoutAL3 inout AL3 #define inoutAL4 inout AL4 #define inoutAU2 inout AU2 #define inoutAU3 inout AU3 #define inoutAU4 inout AU4 //------------------------------------------------------------------------------------------------------------------------------ #define outAD2 out AD2 #define outAD3 out AD3 #define outAD4 out AD4 #define outAF2 out AF2 #define outAF3 out AF3 #define outAF4 out AF4 #define outAL2 out AL2 #define outAL3 out AL3 #define outAL4 out AL4 #define outAU2 out AU2 #define outAU3 out AU3 #define outAU4 out AU4 //------------------------------------------------------------------------------------------------------------------------------ #define varAD2(x) AD2 x #define varAD3(x) AD3 x #define varAD4(x) AD4 x #define varAF2(x) AF2 x #define varAF3(x) AF3 x #define varAF4(x) AF4 x #define varAL2(x) AL2 x #define varAL3(x) AL3 x #define varAL4(x) AL4 x #define varAU2(x) AU2 x #define varAU3(x) AU3 x #define varAU4(x) AU4 x //------------------------------------------------------------------------------------------------------------------------------ #define initAD2(x,y) AD2(x,y) #define initAD3(x,y,z) AD3(x,y,z) #define initAD4(x,y,z,w) AD4(x,y,z,w) #define initAF2(x,y) AF2(x,y) #define initAF3(x,y,z) AF3(x,y,z) #define initAF4(x,y,z,w) AF4(x,y,z,w) #define initAL2(x,y) AL2(x,y) #define initAL3(x,y,z) AL3(x,y,z) #define initAL4(x,y,z,w) AL4(x,y,z,w) #define initAU2(x,y) AU2(x,y) #define initAU3(x,y,z) AU3(x,y,z) #define initAU4(x,y,z,w) AU4(x,y,z,w) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // SCALAR RETURN OPS //============================================================================================================================== #define AAbsD1(a) abs(AD1(a)) #define AAbsF1(a) abs(AF1(a)) //------------------------------------------------------------------------------------------------------------------------------ #define ACosD1(a) cos(AD1(a)) #define ACosF1(a) cos(AF1(a)) //------------------------------------------------------------------------------------------------------------------------------ #define ADotD2(a,b) dot(AD2(a),AD2(b)) #define ADotD3(a,b) dot(AD3(a),AD3(b)) #define ADotD4(a,b) dot(AD4(a),AD4(b)) #define ADotF2(a,b) dot(AF2(a),AF2(b)) #define ADotF3(a,b) dot(AF3(a),AF3(b)) #define ADotF4(a,b) dot(AF4(a),AF4(b)) //------------------------------------------------------------------------------------------------------------------------------ #define AExp2D1(a) exp2(AD1(a)) #define AExp2F1(a) exp2(AF1(a)) //------------------------------------------------------------------------------------------------------------------------------ #define AFloorD1(a) floor(AD1(a)) #define AFloorF1(a) floor(AF1(a)) //------------------------------------------------------------------------------------------------------------------------------ #define ALog2D1(a) log2(AD1(a)) #define ALog2F1(a) log2(AF1(a)) //------------------------------------------------------------------------------------------------------------------------------ #define AMaxD1(a,b) max(a,b) #define AMaxF1(a,b) max(a,b) #define AMaxL1(a,b) max(a,b) #define AMaxU1(a,b) max(a,b) //------------------------------------------------------------------------------------------------------------------------------ #define AMinD1(a,b) min(a,b) #define AMinF1(a,b) min(a,b) #define AMinL1(a,b) min(a,b) #define AMinU1(a,b) min(a,b) //------------------------------------------------------------------------------------------------------------------------------ #define ASinD1(a) sin(AD1(a)) #define ASinF1(a) sin(AF1(a)) //------------------------------------------------------------------------------------------------------------------------------ #define ASqrtD1(a) sqrt(AD1(a)) #define ASqrtF1(a) sqrt(AF1(a)) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // SCALAR RETURN OPS - DEPENDENT //============================================================================================================================== #define APowD1(a,b) pow(AD1(a),AF1(b)) #define APowF1(a,b) pow(AF1(a),AF1(b)) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // VECTOR OPS //------------------------------------------------------------------------------------------------------------------------------ // These are added as needed for production or prototyping, so not necessarily a complete set. // They follow a convention of taking in a destination and also returning the destination value to increase utility. //============================================================================================================================== #ifdef A_DUBL AD2 opAAbsD2(outAD2 d,inAD2 a){d=abs(a);return d;} AD3 opAAbsD3(outAD3 d,inAD3 a){d=abs(a);return d;} AD4 opAAbsD4(outAD4 d,inAD4 a){d=abs(a);return d;} //------------------------------------------------------------------------------------------------------------------------------ AD2 opAAddD2(outAD2 d,inAD2 a,inAD2 b){d=a+b;return d;} AD3 opAAddD3(outAD3 d,inAD3 a,inAD3 b){d=a+b;return d;} AD4 opAAddD4(outAD4 d,inAD4 a,inAD4 b){d=a+b;return d;} //------------------------------------------------------------------------------------------------------------------------------ AD2 opAAddOneD2(outAD2 d,inAD2 a,AD1 b){d=a+AD2_(b);return d;} AD3 opAAddOneD3(outAD3 d,inAD3 a,AD1 b){d=a+AD3_(b);return d;} AD4 opAAddOneD4(outAD4 d,inAD4 a,AD1 b){d=a+AD4_(b);return d;} //------------------------------------------------------------------------------------------------------------------------------ AD2 opACpyD2(outAD2 d,inAD2 a){d=a;return d;} AD3 opACpyD3(outAD3 d,inAD3 a){d=a;return d;} AD4 opACpyD4(outAD4 d,inAD4 a){d=a;return d;} //------------------------------------------------------------------------------------------------------------------------------ AD2 opALerpD2(outAD2 d,inAD2 a,inAD2 b,inAD2 c){d=ALerpD2(a,b,c);return d;} AD3 opALerpD3(outAD3 d,inAD3 a,inAD3 b,inAD3 c){d=ALerpD3(a,b,c);return d;} AD4 opALerpD4(outAD4 d,inAD4 a,inAD4 b,inAD4 c){d=ALerpD4(a,b,c);return d;} //------------------------------------------------------------------------------------------------------------------------------ AD2 opALerpOneD2(outAD2 d,inAD2 a,inAD2 b,AD1 c){d=ALerpD2(a,b,AD2_(c));return d;} AD3 opALerpOneD3(outAD3 d,inAD3 a,inAD3 b,AD1 c){d=ALerpD3(a,b,AD3_(c));return d;} AD4 opALerpOneD4(outAD4 d,inAD4 a,inAD4 b,AD1 c){d=ALerpD4(a,b,AD4_(c));return d;} //------------------------------------------------------------------------------------------------------------------------------ AD2 opAMaxD2(outAD2 d,inAD2 a,inAD2 b){d=max(a,b);return d;} AD3 opAMaxD3(outAD3 d,inAD3 a,inAD3 b){d=max(a,b);return d;} AD4 opAMaxD4(outAD4 d,inAD4 a,inAD4 b){d=max(a,b);return d;} //------------------------------------------------------------------------------------------------------------------------------ AD2 opAMinD2(outAD2 d,inAD2 a,inAD2 b){d=min(a,b);return d;} AD3 opAMinD3(outAD3 d,inAD3 a,inAD3 b){d=min(a,b);return d;} AD4 opAMinD4(outAD4 d,inAD4 a,inAD4 b){d=min(a,b);return d;} //------------------------------------------------------------------------------------------------------------------------------ AD2 opAMulD2(outAD2 d,inAD2 a,inAD2 b){d=a*b;return d;} AD3 opAMulD3(outAD3 d,inAD3 a,inAD3 b){d=a*b;return d;} AD4 opAMulD4(outAD4 d,inAD4 a,inAD4 b){d=a*b;return d;} //------------------------------------------------------------------------------------------------------------------------------ AD2 opAMulOneD2(outAD2 d,inAD2 a,AD1 b){d=a*AD2_(b);return d;} AD3 opAMulOneD3(outAD3 d,inAD3 a,AD1 b){d=a*AD3_(b);return d;} AD4 opAMulOneD4(outAD4 d,inAD4 a,AD1 b){d=a*AD4_(b);return d;} //------------------------------------------------------------------------------------------------------------------------------ AD2 opANegD2(outAD2 d,inAD2 a){d=-a;return d;} AD3 opANegD3(outAD3 d,inAD3 a){d=-a;return d;} AD4 opANegD4(outAD4 d,inAD4 a){d=-a;return d;} //------------------------------------------------------------------------------------------------------------------------------ AD2 opARcpD2(outAD2 d,inAD2 a){d=ARcpD2(a);return d;} AD3 opARcpD3(outAD3 d,inAD3 a){d=ARcpD3(a);return d;} AD4 opARcpD4(outAD4 d,inAD4 a){d=ARcpD4(a);return d;} #endif //============================================================================================================================== AF2 opAAbsF2(outAF2 d,inAF2 a){d=abs(a);return d;} AF3 opAAbsF3(outAF3 d,inAF3 a){d=abs(a);return d;} AF4 opAAbsF4(outAF4 d,inAF4 a){d=abs(a);return d;} //------------------------------------------------------------------------------------------------------------------------------ AF2 opAAddF2(outAF2 d,inAF2 a,inAF2 b){d=a+b;return d;} AF3 opAAddF3(outAF3 d,inAF3 a,inAF3 b){d=a+b;return d;} AF4 opAAddF4(outAF4 d,inAF4 a,inAF4 b){d=a+b;return d;} //------------------------------------------------------------------------------------------------------------------------------ AF2 opAAddOneF2(outAF2 d,inAF2 a,AF1 b){d=a+AF2_(b);return d;} AF3 opAAddOneF3(outAF3 d,inAF3 a,AF1 b){d=a+AF3_(b);return d;} AF4 opAAddOneF4(outAF4 d,inAF4 a,AF1 b){d=a+AF4_(b);return d;} //------------------------------------------------------------------------------------------------------------------------------ AF2 opACpyF2(outAF2 d,inAF2 a){d=a;return d;} AF3 opACpyF3(outAF3 d,inAF3 a){d=a;return d;} AF4 opACpyF4(outAF4 d,inAF4 a){d=a;return d;} //------------------------------------------------------------------------------------------------------------------------------ AF2 opALerpF2(outAF2 d,inAF2 a,inAF2 b,inAF2 c){d=ALerpF2(a,b,c);return d;} AF3 opALerpF3(outAF3 d,inAF3 a,inAF3 b,inAF3 c){d=ALerpF3(a,b,c);return d;} AF4 opALerpF4(outAF4 d,inAF4 a,inAF4 b,inAF4 c){d=ALerpF4(a,b,c);return d;} //------------------------------------------------------------------------------------------------------------------------------ AF2 opALerpOneF2(outAF2 d,inAF2 a,inAF2 b,AF1 c){d=ALerpF2(a,b,AF2_(c));return d;} AF3 opALerpOneF3(outAF3 d,inAF3 a,inAF3 b,AF1 c){d=ALerpF3(a,b,AF3_(c));return d;} AF4 opALerpOneF4(outAF4 d,inAF4 a,inAF4 b,AF1 c){d=ALerpF4(a,b,AF4_(c));return d;} //------------------------------------------------------------------------------------------------------------------------------ AF2 opAMaxF2(outAF2 d,inAF2 a,inAF2 b){d=max(a,b);return d;} AF3 opAMaxF3(outAF3 d,inAF3 a,inAF3 b){d=max(a,b);return d;} AF4 opAMaxF4(outAF4 d,inAF4 a,inAF4 b){d=max(a,b);return d;} //------------------------------------------------------------------------------------------------------------------------------ AF2 opAMinF2(outAF2 d,inAF2 a,inAF2 b){d=min(a,b);return d;} AF3 opAMinF3(outAF3 d,inAF3 a,inAF3 b){d=min(a,b);return d;} AF4 opAMinF4(outAF4 d,inAF4 a,inAF4 b){d=min(a,b);return d;} //------------------------------------------------------------------------------------------------------------------------------ AF2 opAMulF2(outAF2 d,inAF2 a,inAF2 b){d=a*b;return d;} AF3 opAMulF3(outAF3 d,inAF3 a,inAF3 b){d=a*b;return d;} AF4 opAMulF4(outAF4 d,inAF4 a,inAF4 b){d=a*b;return d;} //------------------------------------------------------------------------------------------------------------------------------ AF2 opAMulOneF2(outAF2 d,inAF2 a,AF1 b){d=a*AF2_(b);return d;} AF3 opAMulOneF3(outAF3 d,inAF3 a,AF1 b){d=a*AF3_(b);return d;} AF4 opAMulOneF4(outAF4 d,inAF4 a,AF1 b){d=a*AF4_(b);return d;} //------------------------------------------------------------------------------------------------------------------------------ AF2 opANegF2(outAF2 d,inAF2 a){d=-a;return d;} AF3 opANegF3(outAF3 d,inAF3 a){d=-a;return d;} AF4 opANegF4(outAF4 d,inAF4 a){d=-a;return d;} //------------------------------------------------------------------------------------------------------------------------------ AF2 opARcpF2(outAF2 d,inAF2 a){d=ARcpF2(a);return d;} AF3 opARcpF3(outAF3 d,inAF3 a){d=ARcpF3(a);return d;} AF4 opARcpF4(outAF4 d,inAF4 a){d=ARcpF4(a);return d;} #endif ValveSoftware-gamescope-eb620ab/src/shaders/ffx_fsr1.h000066400000000000000000001656261502457270500230640ustar00rootroot00000000000000//_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // // // AMD FidelityFX SUPER RESOLUTION [FSR 1] ::: SPATIAL SCALING & EXTRAS - v1.20210629 // // //------------------------------------------------------------------------------------------------------------------------------ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //------------------------------------------------------------------------------------------------------------------------------ // FidelityFX Super Resolution Sample // // Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files(the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions : // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. //------------------------------------------------------------------------------------------------------------------------------ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //------------------------------------------------------------------------------------------------------------------------------ // ABOUT // ===== // FSR is a collection of algorithms relating to generating a higher resolution image. // This specific header focuses on single-image non-temporal image scaling, and related tools. // // The core functions are EASU and RCAS: // [EASU] Edge Adaptive Spatial Upsampling ....... 1x to 4x area range spatial scaling, clamped adaptive elliptical filter. // [RCAS] Robust Contrast Adaptive Sharpening .... A non-scaling variation on CAS. // RCAS needs to be applied after EASU as a separate pass. // // Optional utility functions are: // [LFGA] Linear Film Grain Applicator ........... Tool to apply film grain after scaling. // [SRTM] Simple Reversible Tone-Mapper .......... Linear HDR {0 to FP16_MAX} to {0 to 1} and back. // [TEPD] Temporal Energy Preserving Dither ...... Temporally energy preserving dithered {0 to 1} linear to gamma 2.0 conversion. // See each individual sub-section for inline documentation. //------------------------------------------------------------------------------------------------------------------------------ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //------------------------------------------------------------------------------------------------------------------------------ // FUNCTION PERMUTATIONS // ===================== // *F() ..... Single item computation with 32-bit. // *H() ..... Single item computation with 16-bit, with packing (aka two 16-bit ops in parallel) when possible. // *Hx2() ... Processing two items in parallel with 16-bit, easier packing. // Not all interfaces in this file have a *Hx2() form. //============================================================================================================================== //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // // FSR - [EASU] EDGE ADAPTIVE SPATIAL UPSAMPLING // //------------------------------------------------------------------------------------------------------------------------------ // EASU provides a high quality spatial-only scaling at relatively low cost. // Meaning EASU is appropiate for laptops and other low-end GPUs. // Quality from 1x to 4x area scaling is good. //------------------------------------------------------------------------------------------------------------------------------ // The scalar uses a modified fast approximation to the standard lanczos(size=2) kernel. // EASU runs in a single pass, so it applies a directionally and anisotropically adaptive radial lanczos. // This is also kept as simple as possible to have minimum runtime. //------------------------------------------------------------------------------------------------------------------------------ // The lanzcos filter has negative lobes, so by itself it will introduce ringing. // To remove all ringing, the algorithm uses the nearest 2x2 input texels as a neighborhood, // and limits output to the minimum and maximum of that neighborhood. //------------------------------------------------------------------------------------------------------------------------------ // Input image requirements: // // Color needs to be encoded as 3 channel[red, green, blue](e.g.XYZ not supported) // Each channel needs to be in the range[0, 1] // Any color primaries are supported // Display / tonemapping curve needs to be as if presenting to sRGB display or similar(e.g.Gamma 2.0) // There should be no banding in the input // There should be no high amplitude noise in the input // There should be no noise in the input that is not at input pixel granularity // For performance purposes, use 32bpp formats //------------------------------------------------------------------------------------------------------------------------------ // Best to apply EASU at the end of the frame after tonemapping // but before film grain or composite of the UI. //------------------------------------------------------------------------------------------------------------------------------ // Example of including this header for D3D HLSL : // // #define A_GPU 1 // #define A_HLSL 1 // #define A_HALF 1 // #include "ffx_a.h" // #define FSR_EASU_H 1 // #define FSR_RCAS_H 1 // //declare input callbacks // #include "ffx_fsr1.h" // // Example of including this header for Vulkan GLSL : // // #define A_GPU 1 // #define A_GLSL 1 // #define A_HALF 1 // #include "ffx_a.h" // #define FSR_EASU_H 1 // #define FSR_RCAS_H 1 // //declare input callbacks // #include "ffx_fsr1.h" // // Example of including this header for Vulkan HLSL : // // #define A_GPU 1 // #define A_HLSL 1 // #define A_HLSL_6_2 1 // #define A_NO_16_BIT_CAST 1 // #define A_HALF 1 // #include "ffx_a.h" // #define FSR_EASU_H 1 // #define FSR_RCAS_H 1 // //declare input callbacks // #include "ffx_fsr1.h" // // Example of declaring the required input callbacks for GLSL : // The callbacks need to gather4 for each color channel using the specified texture coordinate 'p'. // EASU uses gather4 to reduce position computation logic and for free Arrays of Structures to Structures of Arrays conversion. // // AH4 FsrEasuRH(AF2 p){return AH4(textureGather(sampler2D(tex,sam),p,0));} // AH4 FsrEasuGH(AF2 p){return AH4(textureGather(sampler2D(tex,sam),p,1));} // AH4 FsrEasuBH(AF2 p){return AH4(textureGather(sampler2D(tex,sam),p,2));} // ... // The FsrEasuCon function needs to be called from the CPU or GPU to set up constants. // The difference in viewport and input image size is there to support Dynamic Resolution Scaling. // To use FsrEasuCon() on the CPU, define A_CPU before including ffx_a and ffx_fsr1. // Including a GPU example here, the 'con0' through 'con3' values would be stored out to a constant buffer. // AU4 con0,con1,con2,con3; // FsrEasuCon(con0,con1,con2,con3, // 1920.0,1080.0, // Viewport size (top left aligned) in the input image which is to be scaled. // 3840.0,2160.0, // The size of the input image. // 2560.0,1440.0); // The output resolution. //============================================================================================================================== //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // CONSTANT SETUP //============================================================================================================================== // Call to setup required constant values (works on CPU or GPU). A_STATIC void FsrEasuCon( outAU4 con0, outAU4 con1, outAU4 con2, outAU4 con3, // This the rendered image resolution being upscaled AF1 inputViewportInPixelsX, AF1 inputViewportInPixelsY, // This is the resolution of the resource containing the input image (useful for dynamic resolution) AF1 inputSizeInPixelsX, AF1 inputSizeInPixelsY, // This is the display resolution which the input image gets upscaled to AF1 outputSizeInPixelsX, AF1 outputSizeInPixelsY){ // Output integer position to a pixel position in viewport. con0[0]=AU1_AF1(inputViewportInPixelsX*ARcpF1(outputSizeInPixelsX)); con0[1]=AU1_AF1(inputViewportInPixelsY*ARcpF1(outputSizeInPixelsY)); con0[2]=AU1_AF1(AF1_(0.5)*inputViewportInPixelsX*ARcpF1(outputSizeInPixelsX)-AF1_(0.5)); con0[3]=AU1_AF1(AF1_(0.5)*inputViewportInPixelsY*ARcpF1(outputSizeInPixelsY)-AF1_(0.5)); // Viewport pixel position to normalized image space. // This is used to get upper-left of 'F' tap. con1[0]=AU1_AF1(ARcpF1(inputSizeInPixelsX)); con1[1]=AU1_AF1(ARcpF1(inputSizeInPixelsY)); // Centers of gather4, first offset from upper-left of 'F'. // +---+---+ // | | | // +--(0)--+ // | b | c | // +---F---+---+---+ // | e | f | g | h | // +--(1)--+--(2)--+ // | i | j | k | l | // +---+---+---+---+ // | n | o | // +--(3)--+ // | | | // +---+---+ con1[2]=AU1_AF1(AF1_( 1.0)*ARcpF1(inputSizeInPixelsX)); con1[3]=AU1_AF1(AF1_(-1.0)*ARcpF1(inputSizeInPixelsY)); // These are from (0) instead of 'F'. con2[0]=AU1_AF1(AF1_(-1.0)*ARcpF1(inputSizeInPixelsX)); con2[1]=AU1_AF1(AF1_( 2.0)*ARcpF1(inputSizeInPixelsY)); con2[2]=AU1_AF1(AF1_( 1.0)*ARcpF1(inputSizeInPixelsX)); con2[3]=AU1_AF1(AF1_( 2.0)*ARcpF1(inputSizeInPixelsY)); con3[0]=AU1_AF1(AF1_( 0.0)*ARcpF1(inputSizeInPixelsX)); con3[1]=AU1_AF1(AF1_( 4.0)*ARcpF1(inputSizeInPixelsY)); con3[2]=con3[3]=0;} //If the an offset into the input image resource A_STATIC void FsrEasuConOffset( outAU4 con0, outAU4 con1, outAU4 con2, outAU4 con3, // This the rendered image resolution being upscaled AF1 inputViewportInPixelsX, AF1 inputViewportInPixelsY, // This is the resolution of the resource containing the input image (useful for dynamic resolution) AF1 inputSizeInPixelsX, AF1 inputSizeInPixelsY, // This is the display resolution which the input image gets upscaled to AF1 outputSizeInPixelsX, AF1 outputSizeInPixelsY, // This is the input image offset into the resource containing it (useful for dynamic resolution) AF1 inputOffsetInPixelsX, AF1 inputOffsetInPixelsY) { FsrEasuCon(con0, con1, con2, con3, inputViewportInPixelsX, inputViewportInPixelsY, inputSizeInPixelsX, inputSizeInPixelsY, outputSizeInPixelsX, outputSizeInPixelsY); con0[2] = AU1_AF1(AF1_(0.5) * inputViewportInPixelsX * ARcpF1(outputSizeInPixelsX) - AF1_(0.5) + inputOffsetInPixelsX); con0[3] = AU1_AF1(AF1_(0.5) * inputViewportInPixelsY * ARcpF1(outputSizeInPixelsY) - AF1_(0.5) + inputOffsetInPixelsY); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // NON-PACKED 32-BIT VERSION //============================================================================================================================== #if defined(A_GPU)&&defined(FSR_EASU_F) // Input callback prototypes, need to be implemented by calling shader AF4 FsrEasuRF(AF2 p); AF4 FsrEasuGF(AF2 p); AF4 FsrEasuBF(AF2 p); //------------------------------------------------------------------------------------------------------------------------------ // Filtering for a given tap for the scalar. void FsrEasuTapF( inout AF3 aC, // Accumulated color, with negative lobe. inout AF1 aW, // Accumulated weight. AF2 off, // Pixel offset from resolve position to tap. AF2 dir, // Gradient direction. AF2 len, // Length. AF1 lob, // Negative lobe strength. AF1 clp, // Clipping point. AF3 c){ // Tap color. // Rotate offset by direction. AF2 v; v.x=(off.x*( dir.x))+(off.y*dir.y); v.y=(off.x*(-dir.y))+(off.y*dir.x); // Anisotropy. v*=len; // Compute distance^2. AF1 d2=v.x*v.x+v.y*v.y; // Limit to the window as at corner, 2 taps can easily be outside. d2=min(d2,clp); // Approximation of lancos2 without sin() or rcp(), or sqrt() to get x. // (25/16 * (2/5 * x^2 - 1)^2 - (25/16 - 1)) * (1/4 * x^2 - 1)^2 // |_______________________________________| |_______________| // base window // The general form of the 'base' is, // (a*(b*x^2-1)^2-(a-1)) // Where 'a=1/(2*b-b^2)' and 'b' moves around the negative lobe. AF1 wB=AF1_(2.0/5.0)*d2+AF1_(-1.0); AF1 wA=lob*d2+AF1_(-1.0); wB*=wB; wA*=wA; wB=AF1_(25.0/16.0)*wB+AF1_(-(25.0/16.0-1.0)); AF1 w=wB*wA; // Do weighted average. aC+=c*w;aW+=w;} //------------------------------------------------------------------------------------------------------------------------------ // Accumulate direction and length. void FsrEasuSetF( inout AF2 dir, inout AF1 len, AF2 pp, AP1 biS,AP1 biT,AP1 biU,AP1 biV, AF1 lA,AF1 lB,AF1 lC,AF1 lD,AF1 lE){ // Compute bilinear weight, branches factor out as predicates are compiler time immediates. // s t // u v AF1 w = AF1_(0.0); if(biS)w=(AF1_(1.0)-pp.x)*(AF1_(1.0)-pp.y); if(biT)w= pp.x *(AF1_(1.0)-pp.y); if(biU)w=(AF1_(1.0)-pp.x)* pp.y ; if(biV)w= pp.x * pp.y ; // Direction is the '+' diff. // a // b c d // e // Then takes magnitude from abs average of both sides of 'c'. // Length converts gradient reversal to 0, smoothly to non-reversal at 1, shaped, then adding horz and vert terms. AF1 dc=lD-lC; AF1 cb=lC-lB; AF1 lenX=max(abs(dc),abs(cb)); lenX=APrxLoRcpF1(lenX); AF1 dirX=lD-lB; dir.x+=dirX*w; lenX=ASatF1(abs(dirX)*lenX); lenX*=lenX; len+=lenX*w; // Repeat for the y axis. AF1 ec=lE-lC; AF1 ca=lC-lA; AF1 lenY=max(abs(ec),abs(ca)); lenY=APrxLoRcpF1(lenY); AF1 dirY=lE-lA; dir.y+=dirY*w; lenY=ASatF1(abs(dirY)*lenY); lenY*=lenY; len+=lenY*w;} //------------------------------------------------------------------------------------------------------------------------------ void FsrEasuF( out AF3 pix, AU2 ip, // Integer pixel position in output. AU4 con0, // Constants generated by FsrEasuCon(). AU4 con1, AU4 con2, AU4 con3){ //------------------------------------------------------------------------------------------------------------------------------ // Get position of 'f'. AF2 pp=AF2(ip)*AF2_AU2(con0.xy)+AF2_AU2(con0.zw); AF2 fp=floor(pp); pp-=fp; //------------------------------------------------------------------------------------------------------------------------------ // 12-tap kernel. // b c // e f g h // i j k l // n o // Gather 4 ordering. // a b // r g // For packed FP16, need either {rg} or {ab} so using the following setup for gather in all versions, // a b <- unused (z) // r g // a b a b // r g r g // a b // r g <- unused (z) // Allowing dead-code removal to remove the 'z's. AF2 p0=fp*AF2_AU2(con1.xy)+AF2_AU2(con1.zw); // These are from p0 to avoid pulling two constants on pre-Navi hardware. AF2 p1=p0+AF2_AU2(con2.xy); AF2 p2=p0+AF2_AU2(con2.zw); AF2 p3=p0+AF2_AU2(con3.xy); AF4 bczzR=FsrEasuRF(p0); AF4 bczzG=FsrEasuGF(p0); AF4 bczzB=FsrEasuBF(p0); AF4 ijfeR=FsrEasuRF(p1); AF4 ijfeG=FsrEasuGF(p1); AF4 ijfeB=FsrEasuBF(p1); AF4 klhgR=FsrEasuRF(p2); AF4 klhgG=FsrEasuGF(p2); AF4 klhgB=FsrEasuBF(p2); AF4 zzonR=FsrEasuRF(p3); AF4 zzonG=FsrEasuGF(p3); AF4 zzonB=FsrEasuBF(p3); //------------------------------------------------------------------------------------------------------------------------------ // Simplest multi-channel approximate luma possible (luma times 2, in 2 FMA/MAD). AF4 bczzL=bczzB*AF4_(0.5)+(bczzR*AF4_(0.5)+bczzG); AF4 ijfeL=ijfeB*AF4_(0.5)+(ijfeR*AF4_(0.5)+ijfeG); AF4 klhgL=klhgB*AF4_(0.5)+(klhgR*AF4_(0.5)+klhgG); AF4 zzonL=zzonB*AF4_(0.5)+(zzonR*AF4_(0.5)+zzonG); // Rename. AF1 bL=bczzL.x; AF1 cL=bczzL.y; AF1 iL=ijfeL.x; AF1 jL=ijfeL.y; AF1 fL=ijfeL.z; AF1 eL=ijfeL.w; AF1 kL=klhgL.x; AF1 lL=klhgL.y; AF1 hL=klhgL.z; AF1 gL=klhgL.w; AF1 oL=zzonL.z; AF1 nL=zzonL.w; // Accumulate for bilinear interpolation. AF2 dir=AF2_(0.0); AF1 len=AF1_(0.0); FsrEasuSetF(dir,len,pp,true, false,false,false,bL,eL,fL,gL,jL); FsrEasuSetF(dir,len,pp,false,true ,false,false,cL,fL,gL,hL,kL); FsrEasuSetF(dir,len,pp,false,false,true ,false,fL,iL,jL,kL,nL); FsrEasuSetF(dir,len,pp,false,false,false,true ,gL,jL,kL,lL,oL); //------------------------------------------------------------------------------------------------------------------------------ // Normalize with approximation, and cleanup close to zero. AF2 dir2=dir*dir; AF1 dirR=dir2.x+dir2.y; AP1 zro=dirR w = -m/(n+e+w+s) // 1 == (w*(n+e+w+s)+m)/(4*w+1) -> w = (1-m)/(n+e+w+s-4*1) // Then chooses the 'w' which results in no clipping, limits 'w', and multiplies by the 'sharp' amount. // This solution above has issues with MSAA input as the steps along the gradient cause edge detection issues. // So RCAS uses 4x the maximum and 4x the minimum (depending on equation)in place of the individual taps. // As well as switching from 'm' to either the minimum or maximum (depending on side), to help in energy conservation. // This stabilizes RCAS. // RCAS does a simple highpass which is normalized against the local contrast then shaped, // 0.25 // 0.25 -1 0.25 // 0.25 // This is used as a noise detection filter, to reduce the effect of RCAS on grain, and focus on real edges. // // GLSL example for the required callbacks : // // AH4 FsrRcasLoadH(ASW2 p){return AH4(imageLoad(imgSrc,ASU2(p)));} // void FsrRcasInputH(inout AH1 r,inout AH1 g,inout AH1 b) // { // //do any simple input color conversions here or leave empty if none needed // } // // FsrRcasCon need to be called from the CPU or GPU to set up constants. // Including a GPU example here, the 'con' value would be stored out to a constant buffer. // // AU4 con; // FsrRcasCon(con, // 0.0); // The scale is {0.0 := maximum sharpness, to N>0, where N is the number of stops (halving) of the reduction of sharpness}. // --------------- // RCAS sharpening supports a CAS-like pass-through alpha via, // #define FSR_RCAS_PASSTHROUGH_ALPHA 1 // RCAS also supports a define to enable a more expensive path to avoid some sharpening of noise. // Would suggest it is better to apply film grain after RCAS sharpening (and after scaling) instead of using this define, // #define FSR_RCAS_DENOISE 1 //============================================================================================================================== // This is set at the limit of providing unnatural results for sharpening. #define FSR_RCAS_LIMIT (0.25-(1.0/16.0)) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // CONSTANT SETUP //============================================================================================================================== // Call to setup required constant values (works on CPU or GPU). A_STATIC void FsrRcasCon( outAU4 con, // The scale is {0.0 := maximum, to N>0, where N is the number of stops (halving) of the reduction of sharpness}. AF1 sharpness){ // Transform from stops to linear value. sharpness=AExp2F1(-sharpness); varAF2(hSharp)=initAF2(sharpness,sharpness); con[0]=AU1_AF1(sharpness); con[1]=AU1_AH2_AF2(hSharp); con[2]=0; con[3]=0;} //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // NON-PACKED 32-BIT VERSION //============================================================================================================================== #if defined(A_GPU)&&defined(FSR_RCAS_F) // Input callback prototypes that need to be implemented by calling shader AF4 FsrRcasLoadF(ASU2 p); void FsrRcasInputF(inout AF1 r,inout AF1 g,inout AF1 b); //------------------------------------------------------------------------------------------------------------------------------ void FsrRcasF( out AF1 pixR, // Output values, non-vector so port between RcasFilter() and RcasFilterH() is easy. out AF1 pixG, out AF1 pixB, #ifdef FSR_RCAS_PASSTHROUGH_ALPHA out AF1 pixA, #endif AU2 ip, // Integer pixel position in output. AU4 con){ // Constant generated by RcasSetup(). // Algorithm uses minimal 3x3 pixel neighborhood. // b // d e f // h ASU2 sp=ASU2(ip); AF3 b=FsrRcasLoadF(sp+ASU2( 0,-1)).rgb; AF3 d=FsrRcasLoadF(sp+ASU2(-1, 0)).rgb; #ifdef FSR_RCAS_PASSTHROUGH_ALPHA AF4 ee=FsrRcasLoadF(sp); AF3 e=ee.rgb;pixA=ee.a; #else AF3 e=FsrRcasLoadF(sp).rgb; #endif AF3 f=FsrRcasLoadF(sp+ASU2( 1, 0)).rgb; AF3 h=FsrRcasLoadF(sp+ASU2( 0, 1)).rgb; // Rename (32-bit) or regroup (16-bit). AF1 bR=b.r; AF1 bG=b.g; AF1 bB=b.b; AF1 dR=d.r; AF1 dG=d.g; AF1 dB=d.b; AF1 eR=e.r; AF1 eG=e.g; AF1 eB=e.b; AF1 fR=f.r; AF1 fG=f.g; AF1 fB=f.b; AF1 hR=h.r; AF1 hG=h.g; AF1 hB=h.b; // Run optional input transform. FsrRcasInputF(bR,bG,bB); FsrRcasInputF(dR,dG,dB); FsrRcasInputF(eR,eG,eB); FsrRcasInputF(fR,fG,fB); FsrRcasInputF(hR,hG,hB); // Luma times 2. AF1 bL=bB*AF1_(0.5)+(bR*AF1_(0.5)+bG); AF1 dL=dB*AF1_(0.5)+(dR*AF1_(0.5)+dG); AF1 eL=eB*AF1_(0.5)+(eR*AF1_(0.5)+eG); AF1 fL=fB*AF1_(0.5)+(fR*AF1_(0.5)+fG); AF1 hL=hB*AF1_(0.5)+(hR*AF1_(0.5)+hG); // Noise detection. AF1 nz=AF1_(0.25)*bL+AF1_(0.25)*dL+AF1_(0.25)*fL+AF1_(0.25)*hL-eL; nz=ASatF1(abs(nz)*APrxMedRcpF1(AMax3F1(AMax3F1(bL,dL,eL),fL,hL)-AMin3F1(AMin3F1(bL,dL,eL),fL,hL))); nz=AF1_(-0.5)*nz+AF1_(1.0); // Min and max of ring. AF1 mn4R=min(AMin3F1(bR,dR,fR),hR); AF1 mn4G=min(AMin3F1(bG,dG,fG),hG); AF1 mn4B=min(AMin3F1(bB,dB,fB),hB); AF1 mx4R=max(AMax3F1(bR,dR,fR),hR); AF1 mx4G=max(AMax3F1(bG,dG,fG),hG); AF1 mx4B=max(AMax3F1(bB,dB,fB),hB); // Immediate constants for peak range. AF2 peakC=AF2(1.0,-1.0*4.0); // Limiters, these need to be high precision RCPs. AF1 hitMinR=min(mn4R,eR)*ARcpF1(AF1_(4.0)*mx4R); AF1 hitMinG=min(mn4G,eG)*ARcpF1(AF1_(4.0)*mx4G); AF1 hitMinB=min(mn4B,eB)*ARcpF1(AF1_(4.0)*mx4B); AF1 hitMaxR=(peakC.x-max(mx4R,eR))*ARcpF1(AF1_(4.0)*mn4R+peakC.y); AF1 hitMaxG=(peakC.x-max(mx4G,eG))*ARcpF1(AF1_(4.0)*mn4G+peakC.y); AF1 hitMaxB=(peakC.x-max(mx4B,eB))*ARcpF1(AF1_(4.0)*mn4B+peakC.y); AF1 lobeR=max(-hitMinR,hitMaxR); AF1 lobeG=max(-hitMinG,hitMaxG); AF1 lobeB=max(-hitMinB,hitMaxB); AF1 lobe=max(AF1_(-FSR_RCAS_LIMIT),min(AMax3F1(lobeR,lobeG,lobeB),AF1_(0.0)))*AF1_AU1(con.x); // Apply noise removal. #ifdef FSR_RCAS_DENOISE lobe*=nz; #endif // Resolve, which needs the medium precision rcp approximation to avoid visible tonality changes. AF1 rcpL=APrxMedRcpF1(AF1_(4.0)*lobe+AF1_(1.0)); pixR=(lobe*bR+lobe*dR+lobe*hR+lobe*fR+eR)*rcpL; pixG=(lobe*bG+lobe*dG+lobe*hG+lobe*fG+eG)*rcpL; pixB=(lobe*bB+lobe*dB+lobe*hB+lobe*fB+eB)*rcpL; return;} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // NON-PACKED 16-BIT VERSION //============================================================================================================================== #if defined(A_GPU)&&defined(A_HALF)&&defined(FSR_RCAS_H) // Input callback prototypes that need to be implemented by calling shader AH4 FsrRcasLoadH(ASW2 p); void FsrRcasInputH(inout AH1 r,inout AH1 g,inout AH1 b); //------------------------------------------------------------------------------------------------------------------------------ void FsrRcasH( out AH1 pixR, // Output values, non-vector so port between RcasFilter() and RcasFilterH() is easy. out AH1 pixG, out AH1 pixB, #ifdef FSR_RCAS_PASSTHROUGH_ALPHA out AH1 pixA, #endif AU2 ip, // Integer pixel position in output. AU4 con){ // Constant generated by RcasSetup(). // Sharpening algorithm uses minimal 3x3 pixel neighborhood. // b // d e f // h ASW2 sp=ASW2(ip); AH3 b=FsrRcasLoadH(sp+ASW2( 0,-1)).rgb; AH3 d=FsrRcasLoadH(sp+ASW2(-1, 0)).rgb; #ifdef FSR_RCAS_PASSTHROUGH_ALPHA AH4 ee=FsrRcasLoadH(sp); AH3 e=ee.rgb;pixA=ee.a; #else AH3 e=FsrRcasLoadH(sp).rgb; #endif AH3 f=FsrRcasLoadH(sp+ASW2( 1, 0)).rgb; AH3 h=FsrRcasLoadH(sp+ASW2( 0, 1)).rgb; // Rename (32-bit) or regroup (16-bit). AH1 bR=b.r; AH1 bG=b.g; AH1 bB=b.b; AH1 dR=d.r; AH1 dG=d.g; AH1 dB=d.b; AH1 eR=e.r; AH1 eG=e.g; AH1 eB=e.b; AH1 fR=f.r; AH1 fG=f.g; AH1 fB=f.b; AH1 hR=h.r; AH1 hG=h.g; AH1 hB=h.b; // Run optional input transform. FsrRcasInputH(bR,bG,bB); FsrRcasInputH(dR,dG,dB); FsrRcasInputH(eR,eG,eB); FsrRcasInputH(fR,fG,fB); FsrRcasInputH(hR,hG,hB); // Luma times 2. AH1 bL=bB*AH1_(0.5)+(bR*AH1_(0.5)+bG); AH1 dL=dB*AH1_(0.5)+(dR*AH1_(0.5)+dG); AH1 eL=eB*AH1_(0.5)+(eR*AH1_(0.5)+eG); AH1 fL=fB*AH1_(0.5)+(fR*AH1_(0.5)+fG); AH1 hL=hB*AH1_(0.5)+(hR*AH1_(0.5)+hG); // Noise detection. AH1 nz=AH1_(0.25)*bL+AH1_(0.25)*dL+AH1_(0.25)*fL+AH1_(0.25)*hL-eL; nz=ASatH1(abs(nz)*APrxMedRcpH1(AMax3H1(AMax3H1(bL,dL,eL),fL,hL)-AMin3H1(AMin3H1(bL,dL,eL),fL,hL))); nz=AH1_(-0.5)*nz+AH1_(1.0); // Min and max of ring. AH1 mn4R=min(AMin3H1(bR,dR,fR),hR); AH1 mn4G=min(AMin3H1(bG,dG,fG),hG); AH1 mn4B=min(AMin3H1(bB,dB,fB),hB); AH1 mx4R=max(AMax3H1(bR,dR,fR),hR); AH1 mx4G=max(AMax3H1(bG,dG,fG),hG); AH1 mx4B=max(AMax3H1(bB,dB,fB),hB); // Immediate constants for peak range. AH2 peakC=AH2(1.0,-1.0*4.0); // Limiters, these need to be high precision RCPs. AH1 hitMinR=min(mn4R,eR)*ARcpH1(AH1_(4.0)*mx4R); AH1 hitMinG=min(mn4G,eG)*ARcpH1(AH1_(4.0)*mx4G); AH1 hitMinB=min(mn4B,eB)*ARcpH1(AH1_(4.0)*mx4B); AH1 hitMaxR=(peakC.x-max(mx4R,eR))*ARcpH1(AH1_(4.0)*mn4R+peakC.y); AH1 hitMaxG=(peakC.x-max(mx4G,eG))*ARcpH1(AH1_(4.0)*mn4G+peakC.y); AH1 hitMaxB=(peakC.x-max(mx4B,eB))*ARcpH1(AH1_(4.0)*mn4B+peakC.y); AH1 lobeR=max(-hitMinR,hitMaxR); AH1 lobeG=max(-hitMinG,hitMaxG); AH1 lobeB=max(-hitMinB,hitMaxB); AH1 lobe=max(AH1_(-FSR_RCAS_LIMIT),min(AMax3H1(lobeR,lobeG,lobeB),AH1_(0.0)))*AH2_AU1(con.y).x; // Apply noise removal. #ifdef FSR_RCAS_DENOISE lobe*=nz; #endif // Resolve, which needs the medium precision rcp approximation to avoid visible tonality changes. AH1 rcpL=APrxMedRcpH1(AH1_(4.0)*lobe+AH1_(1.0)); pixR=(lobe*bR+lobe*dR+lobe*hR+lobe*fR+eR)*rcpL; pixG=(lobe*bG+lobe*dG+lobe*hG+lobe*fG+eG)*rcpL; pixB=(lobe*bB+lobe*dB+lobe*hB+lobe*fB+eB)*rcpL;} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // PACKED 16-BIT VERSION //============================================================================================================================== #if defined(A_GPU)&&defined(A_HALF)&&defined(FSR_RCAS_HX2) // Input callback prototypes that need to be implemented by the calling shader AH4 FsrRcasLoadHx2(ASW2 p); void FsrRcasInputHx2(inout AH2 r,inout AH2 g,inout AH2 b); //------------------------------------------------------------------------------------------------------------------------------ // Can be used to convert from packed Structures of Arrays to Arrays of Structures for store. void FsrRcasDepackHx2(out AH4 pix0,out AH4 pix1,AH2 pixR,AH2 pixG,AH2 pixB){ #ifdef A_HLSL // Invoke a slower path for DX only, since it won't allow uninitialized values. pix0.a=pix1.a=0.0; #endif pix0.rgb=AH3(pixR.x,pixG.x,pixB.x); pix1.rgb=AH3(pixR.y,pixG.y,pixB.y);} //------------------------------------------------------------------------------------------------------------------------------ void FsrRcasHx2( // Output values are for 2 8x8 tiles in a 16x8 region. // pix.x = left 8x8 tile // pix.y = right 8x8 tile // This enables later processing to easily be packed as well. out AH2 pixR, out AH2 pixG, out AH2 pixB, #ifdef FSR_RCAS_PASSTHROUGH_ALPHA out AH2 pixA, #endif AU2 ip, // Integer pixel position in output. AU4 con){ // Constant generated by RcasSetup(). // No scaling algorithm uses minimal 3x3 pixel neighborhood. ASW2 sp0=ASW2(ip); AH3 b0=FsrRcasLoadHx2(sp0+ASW2( 0,-1)).rgb; AH3 d0=FsrRcasLoadHx2(sp0+ASW2(-1, 0)).rgb; #ifdef FSR_RCAS_PASSTHROUGH_ALPHA AH4 ee0=FsrRcasLoadHx2(sp0); AH3 e0=ee0.rgb;pixA.r=ee0.a; #else AH3 e0=FsrRcasLoadHx2(sp0).rgb; #endif AH3 f0=FsrRcasLoadHx2(sp0+ASW2( 1, 0)).rgb; AH3 h0=FsrRcasLoadHx2(sp0+ASW2( 0, 1)).rgb; ASW2 sp1=sp0+ASW2(8,0); AH3 b1=FsrRcasLoadHx2(sp1+ASW2( 0,-1)).rgb; AH3 d1=FsrRcasLoadHx2(sp1+ASW2(-1, 0)).rgb; #ifdef FSR_RCAS_PASSTHROUGH_ALPHA AH4 ee1=FsrRcasLoadHx2(sp1); AH3 e1=ee1.rgb;pixA.g=ee1.a; #else AH3 e1=FsrRcasLoadHx2(sp1).rgb; #endif AH3 f1=FsrRcasLoadHx2(sp1+ASW2( 1, 0)).rgb; AH3 h1=FsrRcasLoadHx2(sp1+ASW2( 0, 1)).rgb; // Arrays of Structures to Structures of Arrays conversion. AH2 bR=AH2(b0.r,b1.r); AH2 bG=AH2(b0.g,b1.g); AH2 bB=AH2(b0.b,b1.b); AH2 dR=AH2(d0.r,d1.r); AH2 dG=AH2(d0.g,d1.g); AH2 dB=AH2(d0.b,d1.b); AH2 eR=AH2(e0.r,e1.r); AH2 eG=AH2(e0.g,e1.g); AH2 eB=AH2(e0.b,e1.b); AH2 fR=AH2(f0.r,f1.r); AH2 fG=AH2(f0.g,f1.g); AH2 fB=AH2(f0.b,f1.b); AH2 hR=AH2(h0.r,h1.r); AH2 hG=AH2(h0.g,h1.g); AH2 hB=AH2(h0.b,h1.b); // Run optional input transform. FsrRcasInputHx2(bR,bG,bB); FsrRcasInputHx2(dR,dG,dB); FsrRcasInputHx2(eR,eG,eB); FsrRcasInputHx2(fR,fG,fB); FsrRcasInputHx2(hR,hG,hB); // Luma times 2. AH2 bL=bB*AH2_(0.5)+(bR*AH2_(0.5)+bG); AH2 dL=dB*AH2_(0.5)+(dR*AH2_(0.5)+dG); AH2 eL=eB*AH2_(0.5)+(eR*AH2_(0.5)+eG); AH2 fL=fB*AH2_(0.5)+(fR*AH2_(0.5)+fG); AH2 hL=hB*AH2_(0.5)+(hR*AH2_(0.5)+hG); // Noise detection. AH2 nz=AH2_(0.25)*bL+AH2_(0.25)*dL+AH2_(0.25)*fL+AH2_(0.25)*hL-eL; nz=ASatH2(abs(nz)*APrxMedRcpH2(AMax3H2(AMax3H2(bL,dL,eL),fL,hL)-AMin3H2(AMin3H2(bL,dL,eL),fL,hL))); nz=AH2_(-0.5)*nz+AH2_(1.0); // Min and max of ring. AH2 mn4R=min(AMin3H2(bR,dR,fR),hR); AH2 mn4G=min(AMin3H2(bG,dG,fG),hG); AH2 mn4B=min(AMin3H2(bB,dB,fB),hB); AH2 mx4R=max(AMax3H2(bR,dR,fR),hR); AH2 mx4G=max(AMax3H2(bG,dG,fG),hG); AH2 mx4B=max(AMax3H2(bB,dB,fB),hB); // Immediate constants for peak range. AH2 peakC=AH2(1.0,-1.0*4.0); // Limiters, these need to be high precision RCPs. AH2 hitMinR=min(mn4R,eR)*ARcpH2(AH2_(4.0)*mx4R); AH2 hitMinG=min(mn4G,eG)*ARcpH2(AH2_(4.0)*mx4G); AH2 hitMinB=min(mn4B,eB)*ARcpH2(AH2_(4.0)*mx4B); AH2 hitMaxR=(peakC.x-max(mx4R,eR))*ARcpH2(AH2_(4.0)*mn4R+peakC.y); AH2 hitMaxG=(peakC.x-max(mx4G,eG))*ARcpH2(AH2_(4.0)*mn4G+peakC.y); AH2 hitMaxB=(peakC.x-max(mx4B,eB))*ARcpH2(AH2_(4.0)*mn4B+peakC.y); AH2 lobeR=max(-hitMinR,hitMaxR); AH2 lobeG=max(-hitMinG,hitMaxG); AH2 lobeB=max(-hitMinB,hitMaxB); AH2 lobe=max(AH2_(-FSR_RCAS_LIMIT),min(AMax3H2(lobeR,lobeG,lobeB),AH2_(0.0)))*AH2_(AH2_AU1(con.y).x); // Apply noise removal. #ifdef FSR_RCAS_DENOISE lobe*=nz; #endif // Resolve, which needs the medium precision rcp approximation to avoid visible tonality changes. AH2 rcpL=APrxMedRcpH2(AH2_(4.0)*lobe+AH2_(1.0)); pixR=(lobe*bR+lobe*dR+lobe*hR+lobe*fR+eR)*rcpL; pixG=(lobe*bG+lobe*dG+lobe*hG+lobe*fG+eG)*rcpL; pixB=(lobe*bB+lobe*dB+lobe*hB+lobe*fB+eB)*rcpL;} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // // FSR - [LFGA] LINEAR FILM GRAIN APPLICATOR // //------------------------------------------------------------------------------------------------------------------------------ // Adding output-resolution film grain after scaling is a good way to mask both rendering and scaling artifacts. // Suggest using tiled blue noise as film grain input, with peak noise frequency set for a specific look and feel. // The 'Lfga*()' functions provide a convenient way to introduce grain. // These functions limit grain based on distance to signal limits. // This is done so that the grain is temporally energy preserving, and thus won't modify image tonality. // Grain application should be done in a linear colorspace. // The grain should be temporally changing, but have a temporal sum per pixel that adds to zero (non-biased). //------------------------------------------------------------------------------------------------------------------------------ // Usage, // FsrLfga*( // color, // In/out linear colorspace color {0 to 1} ranged. // grain, // Per pixel grain texture value {-0.5 to 0.5} ranged, input is 3-channel to support colored grain. // amount); // Amount of grain (0 to 1} ranged. //------------------------------------------------------------------------------------------------------------------------------ // Example if grain texture is monochrome: 'FsrLfgaF(color,AF3_(grain),amount)' //============================================================================================================================== #if defined(A_GPU) // Maximum grain is the minimum distance to the signal limit. void FsrLfgaF(inout AF3 c,AF3 t,AF1 a){c+=(t*AF3_(a))*min(AF3_(1.0)-c,c);} #endif //============================================================================================================================== #if defined(A_GPU)&&defined(A_HALF) // Half precision version (slower). void FsrLfgaH(inout AH3 c,AH3 t,AH1 a){c+=(t*AH3_(a))*min(AH3_(1.0)-c,c);} //------------------------------------------------------------------------------------------------------------------------------ // Packed half precision version (faster). void FsrLfgaHx2(inout AH2 cR,inout AH2 cG,inout AH2 cB,AH2 tR,AH2 tG,AH2 tB,AH1 a){ cR+=(tR*AH2_(a))*min(AH2_(1.0)-cR,cR);cG+=(tG*AH2_(a))*min(AH2_(1.0)-cG,cG);cB+=(tB*AH2_(a))*min(AH2_(1.0)-cB,cB);} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // // FSR - [SRTM] SIMPLE REVERSIBLE TONE-MAPPER // //------------------------------------------------------------------------------------------------------------------------------ // This provides a way to take linear HDR color {0 to FP16_MAX} and convert it into a temporary {0 to 1} ranged post-tonemapped linear. // The tonemapper preserves RGB ratio, which helps maintain HDR color bleed during filtering. //------------------------------------------------------------------------------------------------------------------------------ // Reversible tonemapper usage, // FsrSrtm*(color); // {0 to FP16_MAX} converted to {0 to 1}. // FsrSrtmInv*(color); // {0 to 1} converted into {0 to 32768, output peak safe for FP16}. //============================================================================================================================== #if defined(A_GPU) void FsrSrtmF(inout AF3 c){c*=AF3_(ARcpF1(AMax3F1(c.r,c.g,c.b)+AF1_(1.0)));} // The extra max solves the c=1.0 case (which is a /0). void FsrSrtmInvF(inout AF3 c){c*=AF3_(ARcpF1(max(AF1_(1.0/32768.0),AF1_(1.0)-AMax3F1(c.r,c.g,c.b))));} #endif //============================================================================================================================== #if defined(A_GPU)&&defined(A_HALF) void FsrSrtmH(inout AH3 c){c*=AH3_(ARcpH1(AMax3H1(c.r,c.g,c.b)+AH1_(1.0)));} void FsrSrtmInvH(inout AH3 c){c*=AH3_(ARcpH1(max(AH1_(1.0/32768.0),AH1_(1.0)-AMax3H1(c.r,c.g,c.b))));} //------------------------------------------------------------------------------------------------------------------------------ void FsrSrtmHx2(inout AH2 cR,inout AH2 cG,inout AH2 cB){ AH2 rcp=ARcpH2(AMax3H2(cR,cG,cB)+AH2_(1.0));cR*=rcp;cG*=rcp;cB*=rcp;} void FsrSrtmInvHx2(inout AH2 cR,inout AH2 cG,inout AH2 cB){ AH2 rcp=ARcpH2(max(AH2_(1.0/32768.0),AH2_(1.0)-AMax3H2(cR,cG,cB)));cR*=rcp;cG*=rcp;cB*=rcp;} #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //_____________________________________________________________/\_______________________________________________________________ //============================================================================================================================== // // FSR - [TEPD] TEMPORAL ENERGY PRESERVING DITHER // //------------------------------------------------------------------------------------------------------------------------------ // Temporally energy preserving dithered {0 to 1} linear to gamma 2.0 conversion. // Gamma 2.0 is used so that the conversion back to linear is just to square the color. // The conversion comes in 8-bit and 10-bit modes, designed for output to 8-bit UNORM or 10:10:10:2 respectively. // Given good non-biased temporal blue noise as dither input, // the output dither will temporally conserve energy. // This is done by choosing the linear nearest step point instead of perceptual nearest. // See code below for details. //------------------------------------------------------------------------------------------------------------------------------ // DX SPEC RULES FOR FLOAT->UNORM 8-BIT CONVERSION // =============================================== // - Output is 'uint(floor(saturate(n)*255.0+0.5))'. // - Thus rounding is to nearest. // - NaN gets converted to zero. // - INF is clamped to {0.0 to 1.0}. //============================================================================================================================== #if defined(A_GPU) // Hand tuned integer position to dither value, with more values than simple checkerboard. // Only 32-bit has enough precision for this compddation. // Output is {0 to <1}. AF1 FsrTepdDitF(AU2 p,AU1 f){ AF1 x=AF1_(p.x+f); AF1 y=AF1_(p.y); // The 1.61803 golden ratio. AF1 a=AF1_((1.0+sqrt(5.0))/2.0); // Number designed to provide a good visual pattern. AF1 b=AF1_(1.0/3.69); x=x*a+(y*b); return AFractF1(x);} //------------------------------------------------------------------------------------------------------------------------------ // This version is 8-bit gamma 2.0. // The 'c' input is {0 to 1}. // Output is {0 to 1} ready for image store. void FsrTepdC8F(inout AF3 c,AF1 dit){ AF3 n=sqrt(c); n=floor(n*AF3_(255.0))*AF3_(1.0/255.0); AF3 a=n*n; AF3 b=n+AF3_(1.0/255.0);b=b*b; // Ratio of 'a' to 'b' required to produce 'c'. // APrxLoRcpF1() won't work here (at least for very high dynamic ranges). // APrxMedRcpF1() is an IADD,FMA,MUL. AF3 r=(c-b)*APrxMedRcpF3(a-b); // Use the ratio as a cutoff to choose 'a' or 'b'. // AGtZeroF1() is a MUL. c=ASatF3(n+AGtZeroF3(AF3_(dit)-r)*AF3_(1.0/255.0));} //------------------------------------------------------------------------------------------------------------------------------ // This version is 10-bit gamma 2.0. // The 'c' input is {0 to 1}. // Output is {0 to 1} ready for image store. void FsrTepdC10F(inout AF3 c,AF1 dit){ AF3 n=sqrt(c); n=floor(n*AF3_(1023.0))*AF3_(1.0/1023.0); AF3 a=n*n; AF3 b=n+AF3_(1.0/1023.0);b=b*b; AF3 r=(c-b)*APrxMedRcpF3(a-b); c=ASatF3(n+AGtZeroF3(AF3_(dit)-r)*AF3_(1.0/1023.0));} #endif //============================================================================================================================== #if defined(A_GPU)&&defined(A_HALF) AH1 FsrTepdDitH(AU2 p,AU1 f){ AF1 x=AF1_(p.x+f); AF1 y=AF1_(p.y); AF1 a=AF1_((1.0+sqrt(5.0))/2.0); AF1 b=AF1_(1.0/3.69); x=x*a+(y*b); return AH1(AFractF1(x));} //------------------------------------------------------------------------------------------------------------------------------ void FsrTepdC8H(inout AH3 c,AH1 dit){ AH3 n=sqrt(c); n=floor(n*AH3_(255.0))*AH3_(1.0/255.0); AH3 a=n*n; AH3 b=n+AH3_(1.0/255.0);b=b*b; AH3 r=(c-b)*APrxMedRcpH3(a-b); c=ASatH3(n+AGtZeroH3(AH3_(dit)-r)*AH3_(1.0/255.0));} //------------------------------------------------------------------------------------------------------------------------------ void FsrTepdC10H(inout AH3 c,AH1 dit){ AH3 n=sqrt(c); n=floor(n*AH3_(1023.0))*AH3_(1.0/1023.0); AH3 a=n*n; AH3 b=n+AH3_(1.0/1023.0);b=b*b; AH3 r=(c-b)*APrxMedRcpH3(a-b); c=ASatH3(n+AGtZeroH3(AH3_(dit)-r)*AH3_(1.0/1023.0));} //============================================================================================================================== // This computes dither for positions 'p' and 'p+{8,0}'. AH2 FsrTepdDitHx2(AU2 p,AU1 f){ AF2 x; x.x=AF1_(p.x+f); x.y=x.x+AF1_(8.0); AF1 y=AF1_(p.y); AF1 a=AF1_((1.0+sqrt(5.0))/2.0); AF1 b=AF1_(1.0/3.69); x=x*AF2_(a)+AF2_(y*b); return AH2(AFractF2(x));} //------------------------------------------------------------------------------------------------------------------------------ void FsrTepdC8Hx2(inout AH2 cR,inout AH2 cG,inout AH2 cB,AH2 dit){ AH2 nR=sqrt(cR); AH2 nG=sqrt(cG); AH2 nB=sqrt(cB); nR=floor(nR*AH2_(255.0))*AH2_(1.0/255.0); nG=floor(nG*AH2_(255.0))*AH2_(1.0/255.0); nB=floor(nB*AH2_(255.0))*AH2_(1.0/255.0); AH2 aR=nR*nR; AH2 aG=nG*nG; AH2 aB=nB*nB; AH2 bR=nR+AH2_(1.0/255.0);bR=bR*bR; AH2 bG=nG+AH2_(1.0/255.0);bG=bG*bG; AH2 bB=nB+AH2_(1.0/255.0);bB=bB*bB; AH2 rR=(cR-bR)*APrxMedRcpH2(aR-bR); AH2 rG=(cG-bG)*APrxMedRcpH2(aG-bG); AH2 rB=(cB-bB)*APrxMedRcpH2(aB-bB); cR=ASatH2(nR+AGtZeroH2(dit-rR)*AH2_(1.0/255.0)); cG=ASatH2(nG+AGtZeroH2(dit-rG)*AH2_(1.0/255.0)); cB=ASatH2(nB+AGtZeroH2(dit-rB)*AH2_(1.0/255.0));} //------------------------------------------------------------------------------------------------------------------------------ void FsrTepdC10Hx2(inout AH2 cR,inout AH2 cG,inout AH2 cB,AH2 dit){ AH2 nR=sqrt(cR); AH2 nG=sqrt(cG); AH2 nB=sqrt(cB); nR=floor(nR*AH2_(1023.0))*AH2_(1.0/1023.0); nG=floor(nG*AH2_(1023.0))*AH2_(1.0/1023.0); nB=floor(nB*AH2_(1023.0))*AH2_(1.0/1023.0); AH2 aR=nR*nR; AH2 aG=nG*nG; AH2 aB=nB*nB; AH2 bR=nR+AH2_(1.0/1023.0);bR=bR*bR; AH2 bG=nG+AH2_(1.0/1023.0);bG=bG*bG; AH2 bB=nB+AH2_(1.0/1023.0);bB=bB*bB; AH2 rR=(cR-bR)*APrxMedRcpH2(aR-bR); AH2 rG=(cG-bG)*APrxMedRcpH2(aG-bG); AH2 rB=(cB-bB)*APrxMedRcpH2(aB-bB); cR=ASatH2(nR+AGtZeroH2(dit-rR)*AH2_(1.0/1023.0)); cG=ASatH2(nG+AGtZeroH2(dit-rG)*AH2_(1.0/1023.0)); cB=ASatH2(nB+AGtZeroH2(dit-rB)*AH2_(1.0/1023.0));} #endif ValveSoftware-gamescope-eb620ab/src/shaders/heatmap.h000066400000000000000000000202651502457270500227520ustar00rootroot00000000000000// Adapter for Gamescope from // https://github.com/microsoft/Windows-universal-samples/blob/main/Samples/D2DAdvancedColorImages/cpp/D2DAdvancedColorImages/LuminanceHeatmapEffect.hlsl // Licensed under MIT. ////////////////////////////////////// // MS WCG Image Viewer Luminance Map ////////////////////////////////////// // Nits to color mappings: // 0.00 Black // 3.16 Blue // 10.0 Cyan // 31.6 Green // 100.0 Yellow // 316.0 Orange // 1000.0 Red // 3160.0 Magenta // 10000.0 White // This approximates a logarithmic plot where two colors represent one order of magnitude in nits. // Define constants based on above behavior: 9 "stops" for a piecewise linear gradient in scRGB space. #define STOP0_NITS 0.00f #define STOP1_NITS 3.16f #define STOP2_NITS 10.0f #define STOP3_NITS 31.6f #define STOP4_NITS 100.f #define STOP5_NITS 316.f #define STOP6_NITS 1000.f #define STOP7_NITS 3160.f #define STOP8_NITS 10000.f #define STOP0_COLOR vec3(0.0f, 0.0f, 0.0f) // Black #define STOP1_COLOR vec3(0.0f, 0.0f, 1.0f) // Blue #define STOP2_COLOR vec3(0.0f, 1.0f, 1.0f) // Cyan #define STOP3_COLOR vec3(0.0f, 1.0f, 0.0f) // Green #define STOP4_COLOR vec3(1.0f, 1.0f, 0.0f) // Yellow #define STOP5_COLOR vec3(1.0f, 0.2f, 0.0f) // Orange // Orange isn't a simple combination of primary colors but allows us to have 8 gradient segments, // which gives us cleaner definitions for the nits --> color mappings. #define STOP6_COLOR vec3(1.0f, 0.0f, 0.0f) // Red #define STOP7_COLOR vec3(1.0f, 0.0f, 1.0f) // Magenta #define STOP8_COLOR vec3(1.0f, 1.0f, 1.0f) // White vec3 hdr_heatmap_impl_ms_wcg(float nits) { // 2: Determine which gradient segment will be used. // Only one of useSegmentN will be 1 (true) for a given nits value. float useSegment0 = sign(nits - STOP0_NITS) - sign(nits - STOP1_NITS); float useSegment1 = sign(nits - STOP1_NITS) - sign(nits - STOP2_NITS); float useSegment2 = sign(nits - STOP2_NITS) - sign(nits - STOP3_NITS); float useSegment3 = sign(nits - STOP3_NITS) - sign(nits - STOP4_NITS); float useSegment4 = sign(nits - STOP4_NITS) - sign(nits - STOP5_NITS); float useSegment5 = sign(nits - STOP5_NITS) - sign(nits - STOP6_NITS); float useSegment6 = sign(nits - STOP6_NITS) - sign(nits - STOP7_NITS); float useSegment7 = sign(nits - STOP7_NITS) - sign(nits - STOP8_NITS); // 3: Calculate the interpolated color. float lerpSegment0 = (nits - STOP0_NITS) / (STOP1_NITS - STOP0_NITS); float lerpSegment1 = (nits - STOP1_NITS) / (STOP2_NITS - STOP1_NITS); float lerpSegment2 = (nits - STOP2_NITS) / (STOP3_NITS - STOP2_NITS); float lerpSegment3 = (nits - STOP3_NITS) / (STOP4_NITS - STOP3_NITS); float lerpSegment4 = (nits - STOP4_NITS) / (STOP5_NITS - STOP4_NITS); float lerpSegment5 = (nits - STOP5_NITS) / (STOP6_NITS - STOP5_NITS); float lerpSegment6 = (nits - STOP6_NITS) / (STOP7_NITS - STOP6_NITS); float lerpSegment7 = (nits - STOP7_NITS) / (STOP8_NITS - STOP7_NITS); // Only the "active" gradient segment contributes to the output color. return mix(STOP0_COLOR, STOP1_COLOR, lerpSegment0) * useSegment0 + mix(STOP1_COLOR, STOP2_COLOR, lerpSegment1) * useSegment1 + mix(STOP2_COLOR, STOP3_COLOR, lerpSegment2) * useSegment2 + mix(STOP3_COLOR, STOP4_COLOR, lerpSegment3) * useSegment3 + mix(STOP4_COLOR, STOP5_COLOR, lerpSegment4) * useSegment4 + mix(STOP5_COLOR, STOP6_COLOR, lerpSegment5) * useSegment5 + mix(STOP6_COLOR, STOP7_COLOR, lerpSegment6) * useSegment6 + mix(STOP7_COLOR, STOP8_COLOR, lerpSegment7) * useSegment7; } ////////////////////////////////////// // Nicer looking HDR heatmap by Lilium // Has some cool greyscale stuff 8) ////////////////////////////////////// // STOP0 being pure black and not needed #define LILIUM_STOP1_NITS 100.f #define LILIUM_STOP2_NITS 203.f #define LILIUM_STOP3_NITS 400.f #define LILIUM_STOP4_NITS 1000.f #define LILIUM_STOP5_NITS 4000.f #define LILIUM_STOP6_NITS 10000.f #define LILIUM_SCALE_GREYSCALE 0.25f ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // colours fade linearly into each other above 100 nits as the nits increase // 0- 100 nits is kept in greyscale and scaled down by 25% so it doesn't overshadow the other parts of the heatmap // 100- 203 nits is cyan into green // 203- 400 nits is green into yellow // 400- 1000 nits is yellow into red // 1000- 4000 nits is red into pink // 4000-10000 nits is pink into blue ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// float hdr_heatmap_lilium_fade_in(float Y, float currentStop, float normaliseTo) { return (Y - currentStop) / (normaliseTo - currentStop); } float hdr_heatmap_lilium_fade_out(float Y, float currentStop, float normaliseTo) { return 1.f - hdr_heatmap_lilium_fade_in(Y, currentStop, normaliseTo); } vec3 hdr_heatmap_lilium_impl(float Y) { vec3 outColor; if (Y <= LILIUM_STOP1_NITS) // <= 100 nits { //shades of grey const float currentGreyscale = Y > 0.f ? Y / LILIUM_STOP1_NITS * LILIUM_SCALE_GREYSCALE : 0.f; outColor.x = currentGreyscale; outColor.y = currentGreyscale; outColor.z = currentGreyscale; } else if (Y <= LILIUM_STOP2_NITS) // <= 203 nits { //cyan to green outColor.x = 0.f; outColor.y = 1.f; outColor.z = hdr_heatmap_lilium_fade_out(Y, LILIUM_STOP1_NITS, LILIUM_STOP2_NITS); } else if (Y <= LILIUM_STOP3_NITS) // <= 400 nits { //green to yellow outColor.x = hdr_heatmap_lilium_fade_in(Y, LILIUM_STOP2_NITS, LILIUM_STOP3_NITS); outColor.y = 1.f; outColor.z = 0.f; } else if (Y <= LILIUM_STOP4_NITS) // <= 1000 nits { //yellow to red outColor.x = 1.f; outColor.y = hdr_heatmap_lilium_fade_out(Y, LILIUM_STOP3_NITS, LILIUM_STOP4_NITS); outColor.z = 0.f; } else if (Y <= LILIUM_STOP5_NITS) // <= 4000 nits { //red to pink outColor.x = 1.f; outColor.y = 0.f; outColor.z = hdr_heatmap_lilium_fade_in(Y, LILIUM_STOP4_NITS, LILIUM_STOP5_NITS); } else // > 4000 nits { //pink to blue outColor.x = Y <= 10000.f ? hdr_heatmap_lilium_fade_out(Y, LILIUM_STOP5_NITS, LILIUM_STOP6_NITS) : 0.f; // protect against values above 10000 nits outColor.y = 0.f; outColor.z = 1.f; } return outColor; } float fit(float x, float inmin, float inmax, float outmin, float outmax ) { return (x - inmin) / (inmax - inmin) * ( outmax-outmin ) + outmin; } vec3 hdr_heatmap_hard(float Y) { const float HEATMAP_SCALE_LEVEL0 = 203.f; const float HEATMAP_SCALE_LEVEL1 = 500.f; const float HEATMAP_SCALE_LEVEL2 = 1000.f; const float HEATMAP_SCALE_LEVEL3 = 5000.f; const float low = 0.15f; const float hi = 1.f; const float currentGreyscale = Y > 0.f ? Y / HEATMAP_SCALE_LEVEL0 * 0.25f : 0.f; // Color discontinuities are purposeful to make boundaries obvious vec3 outColor; if (Y <= HEATMAP_SCALE_LEVEL0) { outColor = vec3(currentGreyscale); // grey } else if (Y <= HEATMAP_SCALE_LEVEL1) // 200-500 grey to green { outColor = mix( vec3(currentGreyscale), vec3(low,hi,low), vec3( fit(Y, HEATMAP_SCALE_LEVEL0, HEATMAP_SCALE_LEVEL1, 0.0, 0.25) ) ); } else if (Y <= HEATMAP_SCALE_LEVEL2) // 500-1000 green to yellow { outColor = mix( vec3(currentGreyscale), vec3(hi,hi,low), vec3( fit(Y, HEATMAP_SCALE_LEVEL1, HEATMAP_SCALE_LEVEL2, 0.25, 1.0) ) ); } else // 1000 - 10000 yellow to red { outColor = mix( vec3(hi,hi,low), vec3(hi,low,low), vec3( fit(Y, HEATMAP_SCALE_LEVEL2, HEATMAP_SCALE_LEVEL3, 0.25, 1.0) ) ); } return outColor; } bool colorspace_is_2020(uint colorspace) { return colorspace == colorspace_pq; } vec3 hdr_heatmap(vec3 inputColor, uint colorspace) { vec3 xyz; // scRGB encoding (linear / 80.0f) -> nits; inputColor *= 80.0f; if (colorspace_is_2020(colorspace)) xyz = inputColor * rec2020_to_xyz; else xyz = inputColor * rec709_to_xyz; vec3 outputColor; if ( checkDebugFlag(compositedebug_Heatmap_Hard)) outputColor = hdr_heatmap_hard( max(max(inputColor.x, inputColor.y), inputColor.z) ); else if (checkDebugFlag(compositedebug_Heatmap_MSWCG)) outputColor = hdr_heatmap_impl_ms_wcg(xyz.y); // MS WCG viewer heatmap else outputColor = hdr_heatmap_lilium_impl(xyz.y); // Lilium Heatmap return outputColor; } ValveSoftware-gamescope-eb620ab/src/shaders/shaderfilter.h000066400000000000000000000003701502457270500240020ustar00rootroot00000000000000#ifndef SHADERFILTER_H #define SHADERFILTER_H const int shader_filter_max_bits = 4; uint get_layer_shaderfilter(uint layerIdx) { return bitfieldExtract(u_shaderFilter, int(layerIdx) * shader_filter_max_bits, shader_filter_max_bits); } #endifValveSoftware-gamescope-eb620ab/src/steamcompmgr.cpp000066400000000000000000010137031502457270500227330ustar00rootroot00000000000000/* * Based on xcompmgr by Keith Packard et al. * http://cgit.freedesktop.org/xorg/app/xcompmgr/ * Original xcompmgr legal notices follow: * * Copyright © 2003 Keith Packard * * Permission to use, copy, modify, distribute, and sell this software and its * documentation for any purpose is hereby granted without fee, provided that * the above copyright notice appear in all copies and that both that * copyright notice and this permission notice appear in supporting * documentation, and that the name of Keith Packard not be used in * advertising or publicity pertaining to distribution of the software without * specific, written prior permission. Keith Packard makes no * representations about the suitability of this software for any purpose. It * is provided "as is" without express or implied warranty. * * KEITH PACKARD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO * EVENT SHALL KEITH PACKARD BE LIABLE FOR ANY SPECIAL, INDIRECT OR * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, * DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */ /* Modified by Matthew Hawn. I don't know what to say here so follow what it * says above. Not that I can really do anything about it */ #include "gamescope_shared.h" #include "xwayland_ctx.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(__linux__) #include #elif defined(__DragonFly__) || defined(__FreeBSD__) #include #endif #include #include #include #include #include #include #include #include #include #include "waitable.h" #include "main.hpp" #include "wlserver.hpp" #include "rendervulkan.hpp" #include "steamcompmgr.hpp" #include "vblankmanager.hpp" #include "log.hpp" #include "Utils/Defer.h" #include "win32_styles.h" #include "edid.h" #include "hdmi.h" #include "convar.h" #include "refresh_rate.h" #include "commit.h" #include "reshade_effect_manager.hpp" #include "BufferMemo.h" #include "Utils/Process.h" #include "Utils/Algorithm.h" #include "wlr_begin.hpp" #include "wlr/types/wlr_pointer_constraints_v1.h" #include "wlr_end.hpp" #if HAVE_AVIF #include "avif/avif.h" #endif static const int g_nBaseCursorScale = 36; #if HAVE_PIPEWIRE #include "pipewire.hpp" #endif #define STB_IMAGE_IMPLEMENTATION #define STB_IMAGE_WRITE_IMPLEMENTATION #include #include #include #define GPUVIS_TRACE_IMPLEMENTATION #include "gpuvis_trace_utils.h" #if HAVE_LIBSYSTEMD #include #endif LogScope xwm_log("xwm"); LogScope g_WaitableLog("waitable"); gamescope::ConVar cv_overlay_unmultiplied_alpha{ "overlay_unmultiplied_alpha", false }; bool g_bWasPartialComposite = false; bool ShouldDrawCursor(); /// // Color Mgmt // gamescope_color_mgmt_tracker_t g_ColorMgmt{}; static gamescope_color_mgmt_luts g_ColorMgmtLutsOverride[ EOTF_Count ]; std::atomic> g_ColorMgmtLooks[EOTF_Count]; gamescope_color_mgmt_luts g_ColorMgmtLuts[ EOTF_Count ]; gamescope_color_mgmt_luts g_ScreenshotColorMgmtLuts[ EOTF_Count ]; gamescope_color_mgmt_luts g_ScreenshotColorMgmtLutsHDR[ EOTF_Count ]; static lut1d_t g_tmpLut1d; static lut3d_t g_tmpLut3d; extern int g_nDynamicRefreshHz; bool g_bForceHDRSupportDebug = false; extern float g_flInternalDisplayBrightnessNits; extern float g_flHDRItmSdrNits; extern float g_flHDRItmTargetNits; uint64_t g_lastWinSeq = 0; static std::shared_ptr s_scRGB709To2020Matrix; std::string clipboard; std::string primarySelection; std::string g_reshade_effect{}; extern ReshadeEffectPipeline *g_pLastReshadeEffect; uint32_t g_reshade_technique_idx = 0; bool g_bSteamIsActiveWindow = false; bool g_bForceInternal = false; namespace gamescope { extern std::shared_ptr GetX11HostCursor(); } #if HAVE_LIBSYSTEMD static sd_bus *g_dbus; static std::unordered_map g_vramCapacities; static const char *unit_from_pid(pid_t pid) { if (!pid) return NULL; sd_bus_message *reply = NULL; const char *path = NULL; if (sd_bus_call_method(g_dbus, "org.freedesktop.systemd1", "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager", "GetUnitByPID", NULL, &reply, "u", pid) < 0) { xwm_log.warnf("D-Bus call to get unit corresponding to pid %u failed!\n", pid); goto fail; } if (sd_bus_message_read(reply, "o", &path) < 0) xwm_log.warnf("Failed to extract unit from D-Bus reply for PID %u!\n", pid); path = strdup(path); fail: sd_bus_message_unref(reply); return path; } static int set_memory_low(const char *unit_path, bool focused) { sd_bus_message *message; sd_bus_message *reply; #define CHECK_MESSAGE(expr) \ ret = expr; \ if (ret < 0) { \ fprintf(stderr, #expr "failed with ret %d\n", ret); \ goto fail_message; \ } int ret = sd_bus_message_new_method_call(g_dbus, &message, "org.freedesktop.systemd1", unit_path, "org.freedesktop.systemd1.Unit", "SetProperties"); if (ret < 0) return ret; CHECK_MESSAGE(sd_bus_message_append(message, "b", false)); CHECK_MESSAGE(sd_bus_message_open_container(message, SD_BUS_TYPE_ARRAY, "(sv)")); { CHECK_MESSAGE(sd_bus_message_open_container(message, SD_BUS_TYPE_STRUCT, "sv")); { CHECK_MESSAGE(sd_bus_message_append(message, "s", "DevMemoryLow")); CHECK_MESSAGE(sd_bus_message_open_container(message, SD_BUS_TYPE_VARIANT, "a(st)")); { CHECK_MESSAGE(sd_bus_message_open_container(message, SD_BUS_TYPE_ARRAY, "(st)")); for (auto &cap: g_vramCapacities) { CHECK_MESSAGE( sd_bus_message_append(message, "(st)", cap.first.c_str(), focused ? cap.second : 0u)); } CHECK_MESSAGE(sd_bus_message_close_container(message)); } CHECK_MESSAGE(sd_bus_message_close_container(message)); } CHECK_MESSAGE(sd_bus_message_close_container(message)); } CHECK_MESSAGE(sd_bus_message_close_container(message)); CHECK_MESSAGE(sd_bus_call(g_dbus, message, UINT64_MAX, NULL, &reply)); sd_bus_message_unref(reply); fail_message: sd_bus_message_unref(message); return ret; } #endif static std::vector< steamcompmgr_win_t* > GetGlobalPossibleFocusWindows(); static bool pick_primary_focus_and_override( focus_t *out, Window focusControlWindow, const std::vector& vecPossibleFocusWindows, bool globalFocus, const std::vector& ctxFocusControlAppIDs, uint64_t ulVirtualFocusKey = 0, gamescope::VirtualConnectorStrategy eStrategy = gamescope::VirtualConnectorStrategies::PerWindow); bool env_to_bool(const char *env) { if (!env || !*env) return false; return !!atoi(env); } uint64_t timespec_to_nanos(struct timespec& spec) { return spec.tv_sec * 1'000'000'000ul + spec.tv_nsec; } timespec nanos_to_timespec( uint64_t ulNanos ) { timespec ts = { .tv_sec = time_t( ulNanos / 1'000'000'000ul ), .tv_nsec = long( ulNanos % 1'000'000'000ul ), }; return ts; } static void update_runtime_info(); gamescope::ConVar cv_adaptive_sync( "adaptive_sync", false, "Whether or not adaptive sync is enabled if available." ); gamescope::ConVar cv_adaptive_sync_ignore_overlay( "adaptive_sync_ignore_overlay", false, "Whether or not to ignore overlay planes for pushing commits with adaptive sync." ); gamescope::ConVar cv_adaptive_sync_overlay_cycles( "adaptive_sync_overlay_cycles", 1, "Number of vblank cycles to ignore overlay repaints before forcing a commit with adaptive sync." ); gamescope::ConVar cv_upscale_preemptive( "upscale_preemptive", true, "Allow pre-emptive upscaling" ); gamescope::ConVar cv_upscale_preemptive_debug_force_sync( "upscale_preemptive_debug_force_sync", false, "Force synchronize pre-emptive upscaling" ); uint64_t g_SteamCompMgrLimitedAppRefreshCycle = 16'666'666; uint64_t g_SteamCompMgrAppRefreshCycle = 16'666'666; static const gamescope_color_mgmt_t k_ScreenshotColorMgmt = { .enabled = true, .displayColorimetry = displaycolorimetry_709, .displayEOTF = EOTF_Gamma22, .outputEncodingColorimetry = displaycolorimetry_709, .outputEncodingEOTF = EOTF_Gamma22, }; static const gamescope_color_mgmt_t k_ScreenshotColorMgmtHDR = { .enabled = true, .displayColorimetry = displaycolorimetry_2020, .displayEOTF = EOTF_PQ, .outputEncodingColorimetry = displaycolorimetry_2020, .outputEncodingEOTF = EOTF_PQ, }; //#define COLOR_MGMT_MICROBENCH // sudo cpupower frequency-set --governor performance static void create_color_mgmt_luts(const gamescope_color_mgmt_t& newColorMgmt, gamescope_color_mgmt_luts outColorMgmtLuts[ EOTF_Count ]) { const displaycolorimetry_t& displayColorimetry = newColorMgmt.displayColorimetry; const displaycolorimetry_t& outputEncodingColorimetry = newColorMgmt.outputEncodingColorimetry; for ( uint32_t nInputEOTF = 0; nInputEOTF < EOTF_Count; nInputEOTF++ ) { if (!outColorMgmtLuts[nInputEOTF].vk_lut1d) outColorMgmtLuts[nInputEOTF].vk_lut1d = vulkan_create_1d_lut(s_nLutSize1d); if (!outColorMgmtLuts[nInputEOTF].vk_lut3d) outColorMgmtLuts[nInputEOTF].vk_lut3d = vulkan_create_3d_lut(s_nLutEdgeSize3d, s_nLutEdgeSize3d, s_nLutEdgeSize3d); if ( g_ColorMgmtLutsOverride[nInputEOTF].HasLuts() ) { memcpy(g_ColorMgmtLuts[nInputEOTF].lut1d, g_ColorMgmtLutsOverride[nInputEOTF].lut1d, sizeof(g_ColorMgmtLutsOverride[nInputEOTF].lut1d)); memcpy(g_ColorMgmtLuts[nInputEOTF].lut3d, g_ColorMgmtLutsOverride[nInputEOTF].lut3d, sizeof(g_ColorMgmtLutsOverride[nInputEOTF].lut3d)); } else { displaycolorimetry_t inputColorimetry{}; colormapping_t colorMapping{}; tonemapping_t tonemapping{}; tonemapping.bUseShaper = true; EOTF inputEOTF = static_cast( nInputEOTF ); float flGain = 1.f; std::shared_ptr pSharedLook = g_ColorMgmtLooks[ nInputEOTF ]; lut3d_t * pLook = pSharedLook && pSharedLook->lutEdgeSize > 0 ? pSharedLook.get() : nullptr; if ( inputEOTF == EOTF_Gamma22 ) { flGain = newColorMgmt.flSDRInputGain; if ( newColorMgmt.outputEncodingEOTF == EOTF_Gamma22 ) { // G22 -> G22. Does not matter what the g22 mult is tonemapping.g22_luminance = 1.f; // xwm_log.infof("G22 -> G22"); } else if ( newColorMgmt.outputEncodingEOTF == EOTF_PQ ) { // G22 -> PQ. SDR content going on an HDR output tonemapping.g22_luminance = newColorMgmt.flSDROnHDRBrightness; // xwm_log.infof("G22 -> PQ"); } // The final display colorimetry is used to build the output mapping, as we want a gamut-aware handling // for sdrGamutWideness indepdendent of the output encoding (for SDR data), and when mapping SDR -> PQ output // we only want to utilize a portion of the gamut the actual display can reproduce buildSDRColorimetry( &inputColorimetry, &colorMapping, newColorMgmt.sdrGamutWideness, displayColorimetry ); } else if ( inputEOTF == EOTF_PQ ) { flGain = newColorMgmt.flHDRInputGain; if ( newColorMgmt.outputEncodingEOTF == EOTF_Gamma22 ) { // PQ -> G22 Leverage the display's native brightness tonemapping.g22_luminance = newColorMgmt.flInternalDisplayBrightness; // Determine the tonemapping parameters // Use the external atoms if provided tonemap_info_t source = newColorMgmt.hdrTonemapSourceMetadata; tonemap_info_t dest = newColorMgmt.hdrTonemapDisplayMetadata; // Otherwise, rely on the Vulkan source info and the EDID // TODO: If source is invalid, use the provided metadata. // TODO: If hdrTonemapDisplayMetadata is invalid, use the one provided by the display // Adjust the source brightness range by the requested HDR input gain dest.flBlackPointNits /= flGain; dest.flWhitePointNits /= flGain; if ( source.BIsValid() && dest.BIsValid() ) { tonemapping.eOperator = newColorMgmt.hdrTonemapOperator; tonemapping.eetf2390.init( source, newColorMgmt.hdrTonemapDisplayMetadata ); } else { tonemapping.eOperator = ETonemapOperator_None; } /* xwm_log.infof("PQ -> 2.2 - g22_luminance %f gain %f", tonemapping.g22_luminance, flGain ); xwm_log.infof("source %f %f", source.flBlackPointNits, source.flWhitePointNits ); xwm_log.infof("dest %f %f", dest.flBlackPointNits, dest.flWhitePointNits ); xwm_log.infof("operator %d", (int) tonemapping.eOperator );*/ } else if ( newColorMgmt.outputEncodingEOTF == EOTF_PQ ) { // PQ -> PQ passthrough (though this does apply gain) // TODO: should we manipulate the output static metadata to reflect the gain factor? tonemapping.g22_luminance = 1.f; // xwm_log.infof("PQ -> PQ"); } buildPQColorimetry( &inputColorimetry, &colorMapping, displayColorimetry ); } calcColorTransform( &g_tmpLut1d, s_nLutSize1d, &g_tmpLut3d, inputColorimetry, inputEOTF, outputEncodingColorimetry, newColorMgmt.outputEncodingEOTF, newColorMgmt.outputVirtualWhite, newColorMgmt.chromaticAdaptationMode, colorMapping, newColorMgmt.nightmode, tonemapping, pLook, flGain ); // Create quantized output luts for ( size_t i=0, end = g_tmpLut1d.dataR.size(); i cv_tearing_enabled{ "tearing_enabled", false, "Whether or not tearing is enabled." }; int g_nSteamMaxHeight = 0; bool g_bVRRCapable_CachedValue = false; bool g_bVRRInUse_CachedValue = false; bool g_bSupportsHDR_CachedValue = false; bool g_bForceHDR10OutputDebug = false; gamescope::ConVar cv_hdr_enabled{ "hdr_enabled", false, "Whether or not HDR is enabled if it is available." }; bool g_bHDRItmEnable = false; int g_nCurrentRefreshRate_CachedValue = 0; static void update_color_mgmt() { if ( !GetBackend()->GetCurrentConnector() ) return; GetBackend()->GetCurrentConnector()->GetNativeColorimetry( g_bOutputHDREnabled, &g_ColorMgmt.pending.displayColorimetry, &g_ColorMgmt.pending.displayEOTF, &g_ColorMgmt.pending.outputEncodingColorimetry, &g_ColorMgmt.pending.outputEncodingEOTF ); g_ColorMgmt.pending.flInternalDisplayBrightness = GetBackend()->GetCurrentConnector()->GetHDRInfo().uMaxContentLightLevel; #ifdef COLOR_MGMT_MICROBENCH struct timespec t0, t1; #else // check if any part of our color mgmt stack is dirty if ( g_ColorMgmt.pending == g_ColorMgmt.current && g_ColorMgmt.serial != 0 ) return; #endif #ifdef COLOR_MGMT_MICROBENCH clock_gettime(CLOCK_MONOTONIC_RAW, &t0); #endif if (g_ColorMgmt.pending.enabled) { create_color_mgmt_luts(g_ColorMgmt.pending, g_ColorMgmtLuts); } else { for ( uint32_t i = 0; i < EOTF_Count; i++ ) g_ColorMgmtLuts[i].reset(); } #ifdef COLOR_MGMT_MICROBENCH clock_gettime(CLOCK_MONOTONIC_RAW, &t1); #endif #ifdef COLOR_MGMT_MICROBENCH double delta = (timespec_to_nanos(t1) - timespec_to_nanos(t0)) / 1000000.0; static uint32_t iter = 0; static const uint32_t iter_count = 120; static double accum = 0; accum += delta; if (iter++ == iter_count) { printf("update_color_mgmt: %.3fms\n", accum / iter_count); iter = 0; accum = 0; } #endif static uint32_t s_NextColorMgmtSerial = 0; g_ColorMgmt.serial = ++s_NextColorMgmtSerial; g_ColorMgmt.current = g_ColorMgmt.pending; } static void update_screenshot_color_mgmt() { create_color_mgmt_luts(k_ScreenshotColorMgmt, g_ScreenshotColorMgmtLuts); create_color_mgmt_luts(k_ScreenshotColorMgmtHDR, g_ScreenshotColorMgmtLutsHDR); } bool set_color_sdr_gamut_wideness( float flVal ) { if ( g_ColorMgmt.pending.sdrGamutWideness == flVal ) return false; g_ColorMgmt.pending.sdrGamutWideness = flVal; return g_ColorMgmt.pending.enabled; } bool set_internal_display_brightness( float flVal ) { if ( flVal < 1.f ) { flVal = 500.f; } if ( g_ColorMgmt.pending.flInternalDisplayBrightness == flVal ) return false; g_ColorMgmt.pending.flInternalDisplayBrightness = flVal; g_flInternalDisplayBrightnessNits = flVal; return g_ColorMgmt.pending.enabled; } bool set_sdr_on_hdr_brightness( float flVal ) { if ( flVal < 1.f ) { flVal = 203.f; } if ( g_ColorMgmt.pending.flSDROnHDRBrightness == flVal ) return false; g_ColorMgmt.pending.flSDROnHDRBrightness = flVal; return g_ColorMgmt.pending.enabled; } bool set_hdr_input_gain( float flVal ) { if ( flVal < 0.f ) { flVal = 1.f; } if ( g_ColorMgmt.pending.flHDRInputGain == flVal ) return false; g_ColorMgmt.pending.flHDRInputGain = flVal; return g_ColorMgmt.pending.enabled; } bool set_sdr_input_gain( float flVal ) { if ( flVal < 0.f ) { flVal = 1.f; } if ( g_ColorMgmt.pending.flSDRInputGain == flVal ) return false; g_ColorMgmt.pending.flSDRInputGain = flVal; return g_ColorMgmt.pending.enabled; } bool set_color_nightmode( const nightmode_t &nightmode ) { if ( g_ColorMgmt.pending.nightmode == nightmode ) return false; g_ColorMgmt.pending.nightmode = nightmode; return g_ColorMgmt.pending.enabled; } bool set_color_mgmt_enabled( bool bEnabled ) { if ( g_ColorMgmt.pending.enabled == bEnabled ) return false; g_ColorMgmt.pending.enabled = bEnabled; return true; } static gamescope::OwningRc s_MuraCorrectionImage[gamescope::GAMESCOPE_SCREEN_TYPE_COUNT]; static std::shared_ptr s_MuraCTMBlob[gamescope::GAMESCOPE_SCREEN_TYPE_COUNT]; static float g_flMuraScale = 1.0f; static bool g_bMuraCompensationDisabled = false; bool is_mura_correction_enabled() { if ( !GetBackend()->GetCurrentConnector() ) return false; return s_MuraCorrectionImage[GetBackend()->GetCurrentConnector()->GetScreenType()] != nullptr && !g_bMuraCompensationDisabled; } void update_mura_ctm() { s_MuraCTMBlob[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL] = nullptr; if (s_MuraCorrectionImage[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL] == nullptr) return; static constexpr float kMuraMapScale = 0.0625f; static constexpr float kMuraOffset = -127.0f / 255.0f; // Mura's influence scales non-linearly with brightness, so we have an additional scale // on top of the scale factor for the underlying mura map. const float flScale = g_flMuraScale * kMuraMapScale; glm::mat3x4 mura_scale_offset = glm::mat3x4 { flScale, 0, 0, kMuraOffset * flScale, 0, flScale, 0, kMuraOffset * flScale, 0, 0, 0, 0, // No mura comp for blue channel. }; s_MuraCTMBlob[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL] = GetBackend()->CreateBackendBlob( mura_scale_offset ); } bool g_bMuraDebugFullColor = false; bool set_mura_overlay( const char *path ) { xwm_log.infof("[josh mura correction] Setting mura correction image to: %s", path); s_MuraCorrectionImage[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL] = nullptr; update_mura_ctm(); std::string red_path = std::string(path) + "_red.png"; std::string green_path = std::string(path) + "_green.png"; int red_w, red_h, red_comp; unsigned char *red_data = stbi_load(red_path.c_str(), &red_w, &red_h, &red_comp, 1); int green_w, green_h, green_comp; unsigned char *green_data = stbi_load(green_path.c_str(), &green_w, &green_h, &green_comp, 1); if (!red_data || !green_data || red_w != green_w || red_h != green_h || red_comp != green_comp || red_comp != 1 || green_comp != 1) { xwm_log.infof("[josh mura correction] Couldn't load mura correction image, disabling mura correction."); return true; } int w = red_w; int h = red_h; unsigned char *data = (unsigned char*)malloc(red_w * red_h * 4); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { data[(y * w * 4) + (x * 4) + 0] = g_bMuraDebugFullColor ? 255 : red_data[y * w + x]; data[(y * w * 4) + (x * 4) + 1] = g_bMuraDebugFullColor ? 255 : green_data[y * w + x]; data[(y * w * 4) + (x * 4) + 2] = 127; // offset of 0. data[(y * w * 4) + (x * 4) + 3] = 0; // Make alpha = 0 so we act as addtive. } } free(red_data); free(green_data); CVulkanTexture::createFlags texCreateFlags; texCreateFlags.bFlippable = true; texCreateFlags.bSampled = true; s_MuraCorrectionImage[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL] = vulkan_create_texture_from_bits(w, h, w, h, DRM_FORMAT_ABGR8888, texCreateFlags, (void*)data); free(data); xwm_log.infof("[josh mura correction] Loaded new mura correction image!"); update_mura_ctm(); return true; } bool set_mura_scale(float new_scale) { bool diff = g_flMuraScale != new_scale; g_flMuraScale = new_scale; update_mura_ctm(); return diff; } bool set_color_3dlut_override(const char *path) { int nLutIndex = EOTF_Gamma22; g_ColorMgmt.pending.externalDirtyCtr++; g_ColorMgmtLutsOverride[nLutIndex].bHasLut3D = false; FILE *f = fopen(path, "rb"); if (!f) { return true; } defer( fclose(f) ); fseek(f, 0, SEEK_END); long fsize = ftell(f); fseek(f, 0, SEEK_SET); size_t elems = fsize / sizeof(uint16_t); if (elems == 0) { return true; } fread(g_ColorMgmtLutsOverride[nLutIndex].lut3d, elems, sizeof(uint16_t), f); g_ColorMgmtLutsOverride[nLutIndex].bHasLut3D = true; return true; } bool set_color_shaperlut_override(const char *path) { int nLutIndex = EOTF_Gamma22; g_ColorMgmt.pending.externalDirtyCtr++; g_ColorMgmtLutsOverride[nLutIndex].bHasLut1D = false; FILE *f = fopen(path, "rb"); if (!f) { return true; } defer( fclose(f) ); fseek(f, 0, SEEK_END); long fsize = ftell(f); fseek(f, 0, SEEK_SET); size_t elems = fsize / sizeof(uint16_t); if (elems == 0) { return true; } fread(g_ColorMgmtLutsOverride[nLutIndex].lut1d, elems, sizeof(uint16_t), f); g_ColorMgmtLutsOverride[nLutIndex].bHasLut1D = true; return true; } bool set_color_look_pq(const char *path) { bool bRaisesBlackLevelFloor = false; g_ColorMgmtLooks[EOTF_PQ] = LoadCubeLut( path, bRaisesBlackLevelFloor ); cv_overlay_unmultiplied_alpha = bRaisesBlackLevelFloor; g_ColorMgmt.pending.externalDirtyCtr++; return true; } bool set_color_look_g22(const char *path) { bool bRaisesBlackLevelFloor = false; g_ColorMgmtLooks[EOTF_Gamma22] = LoadCubeLut( path, bRaisesBlackLevelFloor ); cv_overlay_unmultiplied_alpha = bRaisesBlackLevelFloor; g_ColorMgmt.pending.externalDirtyCtr++; return true; } bool g_bColorSliderInUse = false; template< typename T > constexpr const T& clamp( const T& x, const T& min, const T& max ) { return x < min ? min : max < x ? max : x; } extern bool g_bForceRelativeMouse; CommitDoneList_t g_steamcompmgr_xdg_done_commits; struct ignore { struct ignore *next; unsigned long sequence; }; gamescope::CAsyncWaiter> g_ImageWaiter{ "gamescope_img" }; gamescope::CWaiter g_SteamCompMgrWaiter; Window x11_win(steamcompmgr_win_t *w) { if (w == nullptr) return None; if (w->type != steamcompmgr_win_type_t::XWAYLAND) return None; return w->xwayland().id; } static uint64_t s_ulFocusSerial = 0ul; void MakeFocusDirty() { s_ulFocusSerial++; } static inline uint64_t GetFocusSerial() { return s_ulFocusSerial; } bool focus_t::IsDirty() { return ulCurrentFocusSerial != GetFocusSerial(); } struct global_focus_t : public focus_t { steamcompmgr_win_t *keyboardFocusWindow; steamcompmgr_win_t *fadeWindow; MouseCursor *cursor; gamescope::VirtualConnectorKey_t ulVirtualFocusKey = 0; std::shared_ptr pVirtualConnector; gamescope::INestedHints *GetNestedHints() { gamescope::IBackendConnector *pConnector = this->pVirtualConnector.get(); if ( !pConnector ) pConnector = GetBackend()->GetCurrentConnector(); if ( pConnector ) { return pConnector->GetNestedHints(); } return nullptr; } }; std::unordered_map g_VirtualConnectorFocuses; global_focus_t *GetCurrentFocus() { if ( GetBackend()->GetCurrentConnector() ) { uint64_t ulKey = GetBackend()->GetCurrentConnector()->GetVirtualConnectorKey(); auto iter = g_VirtualConnectorFocuses.find( ulKey ); if ( iter != g_VirtualConnectorFocuses.end() ) return &iter->second; } if ( g_VirtualConnectorFocuses.size() > 0 ) return &g_VirtualConnectorFocuses.begin()->second; return nullptr; } uint32_t currentOutputWidth, currentOutputHeight; bool currentHDROutput = false; bool currentHDRForce = false; std::vector< uint32_t > vecFocuscontrolAppIDs; bool gameFocused; unsigned int gamesRunningCount; float overscanScaleRatio = 1.0; float zoomScaleRatio = 1.0; float globalScaleRatio = 1.0f; float focusedWindowScaleX = 1.0f; float focusedWindowScaleY = 1.0f; float focusedWindowOffsetX = 0.0f; float focusedWindowOffsetY = 0.0f; uint32_t inputCounter; uint32_t lastPublishedInputCounter; std::atomic hasRepaint = false; bool hasRepaintNonBasePlane = false; static gamescope::ConCommand cc_debug_force_repaint( "debug_force_repaint", "Force a repaint", []( std::span args ) { hasRepaint = true; }); unsigned long damageSequence = 0; uint64_t cursorHideTime = 10'000ul * 1'000'000ul; bool gotXError = false; unsigned int fadeOutStartTime = 0; unsigned int g_FadeOutDuration = 0; extern float g_flMaxWindowScale; bool synchronize; std::mutex g_SteamCompMgrXWaylandServerMutex; gamescope::VBlankTime g_SteamCompMgrVBlankTime = {}; uint64_t g_uCurrentBasePlaneCommitID = 0; bool g_bCurrentBasePlaneIsFifo = false; static int g_nSteamCompMgrTargetFPS = 0; static uint64_t g_uDynamicRefreshEqualityTime = 0; static int g_nDynamicRefreshRate[gamescope::GAMESCOPE_SCREEN_TYPE_COUNT] = { 0, 0 }; // Delay to stop modes flickering back and forth. static const uint64_t g_uDynamicRefreshDelay = 600'000'000; // 600ms static int g_nCombinedAppRefreshCycleOverride[gamescope::GAMESCOPE_SCREEN_TYPE_COUNT] = { 0, 0 }; bool g_nCombinedAppRefreshCycleChangeRefresh[gamescope::GAMESCOPE_SCREEN_TYPE_COUNT] = { true, true }; bool g_nCombinedAppRefreshCycleChangeFPS[gamescope::GAMESCOPE_SCREEN_TYPE_COUNT] = { true, true }; static void _update_app_target_refresh_cycle() { if ( !GetBackend()->GetCurrentConnector() ) return; gamescope::GamescopeScreenType type = GetBackend()->GetCurrentConnector()->GetScreenType(); int target_fps = g_nCombinedAppRefreshCycleOverride[type]; g_nDynamicRefreshRate[ type ] = 0; g_nSteamCompMgrTargetFPS = 0; if ( !target_fps ) { return; } if ( g_nCombinedAppRefreshCycleChangeFPS[ type ] ) { g_nSteamCompMgrTargetFPS = target_fps; } if ( g_nCombinedAppRefreshCycleChangeRefresh[ type ] ) { auto rates = GetBackend()->GetCurrentConnector()->GetValidDynamicRefreshRates(); // Find highest mode to do refresh doubling with. for ( auto rate = rates.rbegin(); rate != rates.rend(); rate++ ) { if (*rate % target_fps == 0) { g_nDynamicRefreshRate[ type ] = *rate; return; } } } } static void update_app_target_refresh_cycle() { int nPrevFPSLimit = g_nSteamCompMgrTargetFPS; _update_app_target_refresh_cycle(); if ( !!g_nSteamCompMgrTargetFPS != !!nPrevFPSLimit ) update_runtime_info(); } void steamcompmgr_set_app_refresh_cycle_override( gamescope::GamescopeScreenType type, int override_fps, bool change_refresh, bool change_fps_cap ) { g_nCombinedAppRefreshCycleOverride[ type ] = override_fps; g_nCombinedAppRefreshCycleChangeRefresh[ type ] = change_refresh; g_nCombinedAppRefreshCycleChangeFPS[ type ] = change_fps_cap; update_app_target_refresh_cycle(); } gamescope::ConCommand cc_debug_set_fps_limit( "debug_set_fps_limit", "Set refresh cycle (debug)", [](std::span svArgs) { if ( svArgs.size() < 2 ) return; // TODO: Expose all facets as args. std::optional onFps = gamescope::Parse( svArgs[1] ); if ( !onFps ) { console_log.errorf( "Failed to parse FPS." ); return; } int32_t nFps = *onFps; steamcompmgr_set_app_refresh_cycle_override( GetBackend()->GetScreenType(), nFps, true, true ); }); static int g_nRuntimeInfoFd = -1; bool g_bFSRActive = false; BlurMode g_BlurMode = BLUR_MODE_OFF; BlurMode g_BlurModeOld = BLUR_MODE_OFF; unsigned int g_BlurFadeDuration = 0; int g_BlurRadius = 5; unsigned int g_BlurFadeStartTime = 0; pid_t focusWindow_pid; std::shared_ptr focusWindow_engine = nullptr; focus_t g_steamcompmgr_xdg_focus; std::vector> g_steamcompmgr_xdg_wins; static bool window_is_steam( steamcompmgr_win_t *w ) { return w && ( w->isSteamLegacyBigPicture || w->appID == 769 ); } bool g_bChangeDynamicRefreshBasedOnGameOpenRatherThanActive = false; bool steamcompmgr_window_should_limit_fps( steamcompmgr_win_t *w ) { return w && !window_is_steam( w ) && !w->isOverlay && !w->isExternalOverlay; } static bool steamcompmgr_user_has_any_game_open() { gamescope_xwayland_server_t *server = NULL; for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++) { if (!server->ctx) continue; if (steamcompmgr_window_should_limit_fps( server->ctx->focus.focusWindow )) return true; } return false; } bool steamcompmgr_window_should_refresh_switch( steamcompmgr_win_t *w ) { if ( GetBackend()->GetCurrentConnector() && GetBackend()->GetCurrentConnector()->IsVRRActive() ) return false; if ( g_bChangeDynamicRefreshBasedOnGameOpenRatherThanActive ) return steamcompmgr_user_has_any_game_open(); return steamcompmgr_window_should_limit_fps( w ); } enum HeldCommitTypes_t { HELD_COMMIT_BASE, HELD_COMMIT_FADE, HELD_COMMIT_COUNT, }; std::array, HELD_COMMIT_COUNT> g_HeldCommits; bool g_bPendingFade = false; /* opacity property name; sometime soon I'll write up an EWMH spec for it */ #define OPACITY_PROP "_NET_WM_WINDOW_OPACITY" #define GAME_PROP "STEAM_GAME" #define STEAM_PROP "STEAM_BIGPICTURE" #define OVERLAY_PROP "STEAM_OVERLAY" #define EXTERNAL_OVERLAY_PROP "GAMESCOPE_EXTERNAL_OVERLAY" #define GAMES_RUNNING_PROP "STEAM_GAMES_RUNNING" #define SCREEN_SCALE_PROP "STEAM_SCREEN_SCALE" #define SCREEN_MAGNIFICATION_PROP "STEAM_SCREEN_MAGNIFICATION" #define TRANSLUCENT 0x00000000 #define OPAQUE 0xffffffff #define ICCCM_WITHDRAWN_STATE 0 #define ICCCM_NORMAL_STATE 1 #define ICCCM_ICONIC_STATE 3 #define NET_WM_STATE_REMOVE 0 #define NET_WM_STATE_ADD 1 #define NET_WM_STATE_TOGGLE 2 #define SYSTEM_TRAY_REQUEST_DOCK 0 #define SYSTEM_TRAY_BEGIN_MESSAGE 1 #define SYSTEM_TRAY_CANCEL_MESSAGE 2 #define FRAME_RATE_SAMPLING_PERIOD 160 unsigned int frameCounter; unsigned int lastSampledFrameTime; float currentFrameRate; static bool debugFocus = false; static bool drawDebugInfo = false; static bool debugEvents = false; extern bool steamMode; gamescope::ConVar cv_composite_force{ "composite_force", false, "Force composition always, never use scanout" }; static bool useXRes = true; namespace gamescope { CScreenshotManager &CScreenshotManager::Get() { static CScreenshotManager s_Instance; return s_Instance; } static ConCommand cc_screenshot( "screenshot", "Take a screenshot to a given path.", []( std::span args ) { std::string_view szPath = "/tmp/gamescope.png"; if ( args.size() > 1 ) szPath = args[1]; gamescope_control_screenshot_type eScreenshotType = GAMESCOPE_CONTROL_SCREENSHOT_TYPE_BASE_PLANE_ONLY; if ( args.size() > 2 ) { std::optional oType = Parse( args[2] ); if ( oType ) eScreenshotType = static_cast( *oType ); } gamescope::CScreenshotManager::Get().TakeScreenshot( gamescope::GamescopeScreenshotInfo { .szScreenshotPath = std::string( szPath ), .eScreenshotType = eScreenshotType, .uScreenshotFlags = 0, .bX11PropertyRequested = false, } ); }); } static std::atomic g_bForceRepaint{false}; extern int g_nCursorScaleHeight; // poor man's semaphore class sem { public: void wait( void ) { std::unique_lock lock(mtx); while(count == 0){ cv.wait(lock); } count--; } void signal( void ) { std::unique_lock lock(mtx); count++; cv.notify_one(); } private: std::mutex mtx; std::condition_variable cv; int count = 0; }; sem statsThreadSem; std::mutex statsEventQueueLock; std::vector< std::string > statsEventQueue; std::string statsThreadPath; int statsPipeFD = -1; bool statsThreadRun; void statsThreadMain( void ) { pthread_setname_np( pthread_self(), "gamescope-stats" ); signal(SIGPIPE, SIG_IGN); while ( statsPipeFD == -1 ) { statsPipeFD = open( statsThreadPath.c_str(), O_WRONLY | O_CLOEXEC ); if ( statsPipeFD == -1 ) { sleep( 10 ); } } wait: statsThreadSem.wait(); if ( statsThreadRun == false ) { return; } std::string event; retry: { std::unique_lock< std::mutex > lock( statsEventQueueLock ); if( statsEventQueue.empty() ) { goto wait; } event = statsEventQueue[ 0 ]; statsEventQueue.erase( statsEventQueue.begin() ); } dprintf( statsPipeFD, "%s", event.c_str() ); goto retry; } static inline void stats_printf( const char* format, ...) { static char buffer[256]; static std::string eventstr; va_list args; va_start(args, format); vsprintf(buffer,format, args); va_end(args); eventstr = buffer; { { std::unique_lock< std::mutex > lock( statsEventQueueLock ); if( statsEventQueue.size() > 50 ) { // overflow, drop event return; } statsEventQueue.push_back( eventstr ); statsThreadSem.signal(); } } } uint64_t get_time_in_nanos() { timespec ts; // Kernel reports page flips with CLOCK_MONOTONIC. clock_gettime(CLOCK_MONOTONIC, &ts); return timespec_to_nanos(ts); } void sleep_for_nanos(uint64_t nanos) { timespec ts = nanos_to_timespec( nanos ); nanosleep(&ts, nullptr); } void sleep_until_nanos(uint64_t nanos) { uint64_t now = get_time_in_nanos(); if (now >= nanos) return; sleep_for_nanos(nanos - now); } unsigned int get_time_in_milliseconds(void) { return (unsigned int)(get_time_in_nanos() / 1'000'000ul); } bool xwayland_ctx_t::HasQueuedEvents() { // If mode is QueuedAlready, XEventsQueued() returns the number of // events already in the event queue (and never performs a system call). return XEventsQueued( dpy, QueuedAlready ) != 0; } static steamcompmgr_win_t * find_win(xwayland_ctx_t *ctx, Window id, bool find_children = true) { steamcompmgr_win_t *w; if (id == None) { return NULL; } for (w = ctx->list; w; w = w->xwayland().next) { if (w->xwayland().id == id) { return w; } } if ( !find_children ) return nullptr; // Didn't find, must be a children somewhere; try again with parent. Window root = None; Window parent = None; Window *children = NULL; unsigned int childrenCount; XQueryTree(ctx->dpy, id, &root, &parent, &children, &childrenCount); if (children) XFree(children); if (root == parent || parent == None) { return NULL; } return find_win(ctx, parent); } static steamcompmgr_win_t * find_win( xwayland_ctx_t *ctx, struct wlr_surface *surf ) { steamcompmgr_win_t *w = nullptr; for (w = ctx->list; w; w = w->xwayland().next) { if ( w->xwayland().surface.main_surface == surf ) return w; if ( w->xwayland().surface.override_surface == surf ) return w; } return nullptr; } static gamescope::CBufferMemoizer s_BufferMemos; // This really needs cleanup, this function is so silly... static gamescope::Rc import_commit ( steamcompmgr_win_t *w, struct wlr_surface *surf, struct wlr_buffer *buf, bool async, std::shared_ptr swapchain_feedback, std::vector presentation_feedbacks, std::optional present_id, uint64_t desired_present_time, bool fifo ) { gamescope::Rc commit = new commit_t; commit->win_seq = w->seq; commit->surf = surf; commit->buf = buf; commit->async = async; commit->fifo = fifo; commit->is_steam = window_is_steam( w ); commit->presentation_feedbacks = std::move(presentation_feedbacks); if (swapchain_feedback) commit->feedback = *swapchain_feedback; commit->present_id = present_id; commit->desired_present_time = desired_present_time; if ( gamescope::OwningRc pTexture = s_BufferMemos.LookupVulkanTexture( buf ) ) { // Going from OwningRc -> Rc now. commit->vulkanTex = pTexture; return commit; } struct wlr_dmabuf_attributes dmabuf = {0}; gamescope::OwningRc pBackendFb; if ( wlr_buffer_get_dmabuf( buf, &dmabuf ) ) { pBackendFb = GetBackend()->ImportDmabufToBackend( buf, &dmabuf ); } gamescope::OwningRc pOwnedTexture = vulkan_create_texture_from_wlr_buffer( buf, std::move( pBackendFb ) ); commit->vulkanTex = pOwnedTexture; s_BufferMemos.MemoizeBuffer( buf, std::move( pOwnedTexture ) ); return commit; } static int32_t window_last_done_commit_index( steamcompmgr_win_t *w ) { int32_t lastCommit = -1; for ( uint32_t i = 0; i < w->commit_queue.size(); i++ ) { if ( w->commit_queue[ i ]->done ) { lastCommit = i; } } return lastCommit; } static bool window_has_commits( steamcompmgr_win_t *w ) { return window_last_done_commit_index( w ) != -1; } static void get_window_last_done_commit( steamcompmgr_win_t *w, gamescope::Rc &commit ) { int32_t lastCommit = window_last_done_commit_index( w ); if ( lastCommit == -1 ) { return; } if ( commit != w->commit_queue[ lastCommit ] ) commit = w->commit_queue[ lastCommit ]; } static commit_t* get_window_last_done_commit_peek( steamcompmgr_win_t *w ) { int32_t lastCommit = window_last_done_commit_index( w ); if ( lastCommit == -1 ) { return nullptr; } return w->commit_queue[ lastCommit ].get(); } static int64_t window_last_done_commit_id( steamcompmgr_win_t *w ) { if ( !w ) return 0; commit_t *pCommit = get_window_last_done_commit_peek( w ); if ( !pCommit ) return 0; return pCommit->commitID; } // For Steam, etc. static bool window_wants_no_focus_when_mouse_hidden( steamcompmgr_win_t *w ) { return window_is_steam( w ); } static bool window_is_fullscreen( steamcompmgr_win_t *w ) { return w && ( window_is_steam( w ) || w->isFullscreen ); } void calc_scale_factor_scaler(float &out_scale_x, float &out_scale_y, float sourceWidth, float sourceHeight) { float XOutputRatio = currentOutputWidth / (float)g_nNestedWidth; float YOutputRatio = currentOutputHeight / (float)g_nNestedHeight; float outputScaleRatio = std::min(XOutputRatio, YOutputRatio); float XRatio = (float)g_nNestedWidth / sourceWidth; float YRatio = (float)g_nNestedHeight / sourceHeight; if (g_upscaleScaler == GamescopeUpscaleScaler::STRETCH) { out_scale_x = XRatio * XOutputRatio; out_scale_y = YRatio * YOutputRatio; return; } if (g_upscaleScaler != GamescopeUpscaleScaler::FILL) { out_scale_x = std::min(XRatio, YRatio); out_scale_y = std::min(XRatio, YRatio); } else { out_scale_x = std::max(XRatio, YRatio); out_scale_y = std::max(XRatio, YRatio); } if (g_upscaleScaler == GamescopeUpscaleScaler::AUTO) { out_scale_x = std::min(g_flMaxWindowScale, out_scale_x); out_scale_y = std::min(g_flMaxWindowScale, out_scale_y); } out_scale_x *= outputScaleRatio; out_scale_y *= outputScaleRatio; if (g_upscaleScaler == GamescopeUpscaleScaler::INTEGER) { if (out_scale_x > 1.0f) { // x == y here always. out_scale_x = out_scale_y = floor(out_scale_x); } } } void calc_scale_factor(float &out_scale_x, float &out_scale_y, float sourceWidth, float sourceHeight) { calc_scale_factor_scaler(out_scale_x, out_scale_y, sourceWidth, sourceHeight); out_scale_x *= globalScaleRatio; out_scale_y *= globalScaleRatio; } /** * Constructor for a cursor. It is hidden in the beginning (normally until moved by user). */ MouseCursor::MouseCursor(xwayland_ctx_t *ctx) : m_texture(nullptr) , m_dirty(true) , m_imageEmpty(false) , m_ctx(ctx) { updateCursorFeedback( true ); } void MouseCursor::UpdatePosition() { wlserver_lock(); struct wlr_pointer_constraint_v1 *pConstraint = wlserver.GetCursorConstraint(); if ( pConstraint && pConstraint->current.cursor_hint.enabled ) { m_x = pConstraint->current.cursor_hint.x; m_y = pConstraint->current.cursor_hint.y; m_bConstrained = true; } else { m_x = wlserver.mouse_surface_cursorx; m_y = wlserver.mouse_surface_cursory; m_bConstrained = false; } wlserver_unlock(); } void MouseCursor::checkSuspension() { getTexture(); if ( ShouldDrawCursor() ) { const bool suspended = int64_t( get_time_in_nanos() ) - int64_t( wlserver.ulLastMovedCursorTime ) > int64_t( cursorHideTime ); if (!wlserver.bCursorHidden && suspended) { wlserver.bCursorHidden = true; steamcompmgr_win_t *window = m_ctx->focus.inputFocusWindow; // Rearm warp count if (window) { // Move the cursor to the bottom right corner, just off screen if we can // if the window (ie. Steam) doesn't want hover/focus events. if ( window_wants_no_focus_when_mouse_hidden(window) ) { wlserver_lock(); wlserver_fake_mouse_pos( window->GetGeometry().nWidth - 1, window->GetGeometry().nHeight - 1 ); wlserver_mousehide(); wlserver_unlock(); } } // We're hiding the cursor, force redraw if we were showing it if (window && !m_imageEmpty ) { hasRepaintNonBasePlane = true; nudge_steamcompmgr(); } } } else { wlserver.bCursorHidden = false; } wlserver.bCursorHasImage = !m_imageEmpty; updateCursorFeedback(); } void MouseCursor::setDirty() { // We can't prove it's empty until checking again m_imageEmpty = false; m_dirty = true; } bool MouseCursor::setCursorImage(char *data, int w, int h, int hx, int hy) { XRenderPictFormat *pictformat; Picture picture; XImage* ximage; Pixmap pixmap; Cursor cursor; GC gc; if (!(ximage = XCreateImage( m_ctx->dpy, DefaultVisual(m_ctx->dpy, DefaultScreen(m_ctx->dpy)), 32, ZPixmap, 0, data, w, h, 32, 0))) { xwm_log.errorf("Failed to make ximage for cursor"); goto error_image; } if (!(pixmap = XCreatePixmap(m_ctx->dpy, DefaultRootWindow(m_ctx->dpy), w, h, 32))) { xwm_log.errorf("Failed to make pixmap for cursor"); goto error_pixmap; } if (!(gc = XCreateGC(m_ctx->dpy, pixmap, 0, NULL))) { xwm_log.errorf("Failed to make gc for cursor"); goto error_gc; } XPutImage(m_ctx->dpy, pixmap, gc, ximage, 0, 0, 0, 0, w, h); if (!(pictformat = XRenderFindStandardFormat(m_ctx->dpy, PictStandardARGB32))) { xwm_log.errorf("Failed to create pictformat for cursor"); goto error_pictformat; } if (!(picture = XRenderCreatePicture(m_ctx->dpy, pixmap, pictformat, 0, NULL))) { xwm_log.errorf("Failed to create picture for cursor"); goto error_picture; } if (!(cursor = XRenderCreateCursor(m_ctx->dpy, picture, hx, hy))) { xwm_log.errorf("Failed to create cursor"); goto error_cursor; } XDefineCursor(m_ctx->dpy, DefaultRootWindow(m_ctx->dpy), cursor); XFlush(m_ctx->dpy); setDirty(); return true; error_cursor: XRenderFreePicture(m_ctx->dpy, picture); error_picture: error_pictformat: XFreeGC(m_ctx->dpy, gc); error_gc: XFreePixmap(m_ctx->dpy, pixmap); error_pixmap: // XDestroyImage frees the data. XDestroyImage(ximage); error_image: return false; } bool MouseCursor::setCursorImageByName(const char *name) { int index = XmuCursorNameToIndex(name); if (index < 0) return false; Cursor cursor = XcursorShapeLoadCursor( m_ctx->dpy, index ); XDefineCursor(m_ctx->dpy, DefaultRootWindow(m_ctx->dpy), cursor); XFlush(m_ctx->dpy); setDirty(); return true; } int MouseCursor::x() const { return m_x; } int MouseCursor::y() const { return m_y; } bool MouseCursor::getTexture() { uint64_t ulConnectorId = 0; if ( GetBackend()->GetCurrentConnector() ) ulConnectorId = GetBackend()->GetCurrentConnector()->GetConnectorID(); if ( ulConnectorId != m_ulLastConnectorId ) { m_ulLastConnectorId = ulConnectorId; m_dirty = true; } if (!m_dirty) { return !m_imageEmpty; } auto *image = XFixesGetCursorImage(m_ctx->dpy); if (!image) { return false; } m_hotspotX = image->xhot; m_hotspotY = image->yhot; int nDesiredWidth = image->width; int nDesiredHeight = image->height; if ( g_nCursorScaleHeight > 0 ) { GetDesiredSize( nDesiredWidth, nDesiredHeight ); } uint32_t surfaceWidth; uint32_t surfaceHeight; glm::uvec2 surfaceSize = GetBackend()->CursorSurfaceSize( glm::uvec2{ (uint32_t)nDesiredWidth, (uint32_t)nDesiredHeight } ); surfaceWidth = surfaceSize.x; surfaceHeight = surfaceSize.y; m_texture = nullptr; // Assume the cursor is fully translucent unless proven otherwise. bool bNoCursor = true; std::vector cursorBuffer; int nContentWidth = image->width; int nContentHeight = image->height; if (image->width && image->height) { if ( nDesiredWidth < image->width || nDesiredHeight < image->height ) { std::vector pixels(image->width * image->height); for (int i = 0; i < image->height; i++) { for (int j = 0; j < image->width; j++) { pixels[i * image->width + j] = image->pixels[i * image->width + j]; } } std::vector resizeBuffer( nDesiredWidth * nDesiredHeight ); stbir_resize_uint8_srgb( (unsigned char *)pixels.data(), image->width, image->height, 0, (unsigned char *)resizeBuffer.data(), nDesiredWidth, nDesiredHeight, 0, 4, 3, STBIR_FLAG_ALPHA_PREMULTIPLIED ); cursorBuffer = std::vector(surfaceWidth * surfaceHeight); for (int i = 0; i < nDesiredHeight; i++) { for (int j = 0; j < nDesiredWidth; j++) { cursorBuffer[i * surfaceWidth + j] = resizeBuffer[i * nDesiredWidth + j]; } } m_hotspotX = ( m_hotspotX * nDesiredWidth ) / image->width; m_hotspotY = ( m_hotspotY * nDesiredHeight ) / image->height; nContentWidth = nDesiredWidth; nContentHeight = nDesiredHeight; } else { cursorBuffer = std::vector(surfaceWidth * surfaceHeight); for (int i = 0; i < image->height; i++) { for (int j = 0; j < image->width; j++) { cursorBuffer[i * surfaceWidth + j] = image->pixels[i * image->width + j]; } } } } for (uint32_t i = 0; i < surfaceHeight; i++) { for (uint32_t j = 0; j < surfaceWidth; j++) { if ( cursorBuffer[i * surfaceWidth + j] & 0xff000000 ) { bNoCursor = false; break; } } } if (bNoCursor) cursorBuffer.clear(); m_imageEmpty = bNoCursor; m_dirty = false; updateCursorFeedback(); if (m_imageEmpty) { if ( GetBackend()->GetCurrentConnector() && GetBackend()->GetCurrentConnector()->GetNestedHints() ) GetBackend()->GetCurrentConnector()->GetNestedHints()->SetCursorImage( nullptr ); return false; } CVulkanTexture::createFlags texCreateFlags; texCreateFlags.bFlippable = true; if ( GetBackend()->SupportsPlaneHardwareCursor() ) { texCreateFlags.bLinear = true; // cursor buffer needs to be linear // TODO: choose format & modifiers from cursor plane } m_texture = vulkan_create_texture_from_bits(surfaceWidth, surfaceHeight, nContentWidth, nContentHeight, DRM_FORMAT_ARGB8888, texCreateFlags, cursorBuffer.data()); if ( GetBackend()->GetCurrentConnector() && GetBackend()->GetCurrentConnector()->GetNestedHints() ) { auto info = std::make_shared( gamescope::INestedHints::CursorInfo { .pPixels = std::move( cursorBuffer ), .uWidth = (uint32_t) nDesiredWidth, .uHeight = (uint32_t) nDesiredHeight, .uXHotspot = image->xhot, .uYHotspot = image->yhot, }); GetBackend()->GetCurrentConnector()->GetNestedHints()->SetCursorImage( std::move( info ) ); } assert(m_texture); XFree(image); return true; } void MouseCursor::GetDesiredSize( int& nWidth, int &nHeight ) { int nSize = g_nBaseCursorScale; if ( g_nCursorScaleHeight > 0 ) { nSize = nSize * floor(g_nOutputHeight / (float)g_nCursorScaleHeight); nSize = std::clamp( nSize, g_nBaseCursorScale, 256 ); } nSize = std::min( nSize, glm::compMin( GetBackend()->CursorSurfaceSize( glm::uvec2{ (uint32_t)nSize, (uint32_t)nSize } ) ) ); nWidth = nSize; nHeight = nSize; } void MouseCursor::paint(steamcompmgr_win_t *window, steamcompmgr_win_t *fit, struct FrameInfo_t *frameInfo) { if ( m_imageEmpty || wlserver.bCursorHidden ) return; int winX = x(); int winY = y(); // Also need new texture if (!getTexture()) { return; } int32_t sourceWidth = window->GetGeometry().nWidth; int32_t sourceHeight = window->GetGeometry().nHeight; if ( fit ) { // If we have an override window, try to fit it in as long as it won't make our scale go below 1.0. sourceWidth = std::max( sourceWidth, clamp( fit->GetGeometry().nX + fit->GetGeometry().nWidth, 0, currentOutputWidth ) ); sourceHeight = std::max( sourceHeight, clamp( fit->GetGeometry().nY + fit->GetGeometry().nHeight, 0, currentOutputHeight ) ); } float cursor_scale = 1.0f; if ( g_nCursorScaleHeight > 0 ) { int nDesiredWidth, nDesiredHeight; GetDesiredSize( nDesiredWidth, nDesiredHeight ); cursor_scale = nDesiredHeight / (float)m_texture->contentHeight(); } cursor_scale = std::max(cursor_scale, 1.0f); float scaledX, scaledY; float currentScaleRatio_x = 1.0; float currentScaleRatio_y = 1.0; int cursorOffsetX, cursorOffsetY; calc_scale_factor(currentScaleRatio_x, currentScaleRatio_y, sourceWidth, sourceHeight); cursorOffsetX = (currentOutputWidth - sourceWidth * currentScaleRatio_x) / 2.0f; cursorOffsetY = (currentOutputHeight - sourceHeight * currentScaleRatio_y) / 2.0f; // Actual point on scaled screen where the cursor hotspot should be scaledX = (winX - window->GetGeometry().nX) * currentScaleRatio_x + cursorOffsetX; scaledY = (winY - window->GetGeometry().nY) * currentScaleRatio_y + cursorOffsetY; if ( zoomScaleRatio != 1.0 ) { scaledX += ((sourceWidth / 2) - winX) * currentScaleRatio_x; scaledY += ((sourceHeight / 2) - winY) * currentScaleRatio_y; } // Apply the cursor offset inside the texture using the display scale scaledX = scaledX - (m_hotspotX * cursor_scale); scaledY = scaledY - (m_hotspotY * cursor_scale); int curLayer = frameInfo->layerCount++; FrameInfo_t::Layer_t *layer = &frameInfo->layers[ curLayer ]; layer->opacity = 1.0; layer->scale.x = 1.0f / cursor_scale; layer->scale.y = 1.0f / cursor_scale; layer->offset.x = -scaledX; layer->offset.y = -scaledY; layer->zpos = g_zposCursor; // cursor, on top of both bottom layers layer->applyColorMgmt = false; layer->tex = m_texture; layer->filter = cursor_scale != 1.0f ? GamescopeUpscaleFilter::LINEAR : GamescopeUpscaleFilter::NEAREST; layer->blackBorder = false; layer->ctm = nullptr; layer->hdr_metadata_blob = nullptr; layer->colorspace = GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB; layer->eAlphaBlendingMode = cv_overlay_unmultiplied_alpha ? ALPHA_BLENDING_MODE_COVERAGE : ALPHA_BLENDING_MODE_PREMULTIPLIED; } void MouseCursor::updateCursorFeedback( bool bForce ) { // Can't resolve this until cursor is un-dirtied. if ( m_dirty && !bForce ) return; bool bVisible = !isHidden(); if ( m_bCursorVisibleFeedback == bVisible && !bForce ) return; uint32_t value = bVisible ? 1 : 0; XChangeProperty(m_ctx->dpy, m_ctx->root, m_ctx->atoms.gamescopeCursorVisibleFeedback, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&value, 1 ); m_bCursorVisibleFeedback = bVisible; m_needs_server_flush = true; } struct BaseLayerInfo_t { float scale[2]; float offset[2]; float opacity; GamescopeUpscaleFilter filter; AlphaBlendingMode_t eAlphaBlendingMode = ALPHA_BLENDING_MODE_PREMULTIPLIED; }; std::array< BaseLayerInfo_t, HELD_COMMIT_COUNT > g_CachedPlanes = {}; static void paint_cached_base_layer(const gamescope::Rc& commit, const BaseLayerInfo_t& base, struct FrameInfo_t *frameInfo, float flOpacityScale, bool bOverrideOpacity ) { int curLayer = frameInfo->layerCount++; FrameInfo_t::Layer_t *layer = &frameInfo->layers[ curLayer ]; layer->scale.x = base.scale[0]; layer->scale.y = base.scale[1]; layer->offset.x = base.offset[0]; layer->offset.y = base.offset[1]; layer->opacity = bOverrideOpacity ? flOpacityScale : base.opacity * flOpacityScale; layer->colorspace = commit->colorspace(); layer->hdr_metadata_blob = nullptr; if (commit->feedback) { layer->hdr_metadata_blob = commit->feedback->hdr_metadata_blob; } layer->ctm = nullptr; if (layer->colorspace == GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB) layer->ctm = s_scRGB709To2020Matrix; layer->tex = commit->vulkanTex; layer->filter = base.filter; layer->eAlphaBlendingMode = base.eAlphaBlendingMode; layer->blackBorder = true; } namespace PaintWindowFlag { static const uint32_t BasePlane = 1u << 0; static const uint32_t FadeTarget = 1u << 1; static const uint32_t NotificationMode = 1u << 2; static const uint32_t DrawBorders = 1u << 3; static const uint32_t NoScale = 1u << 4; static const uint32_t NoFilter = 1u << 5; static const uint32_t CoverageMode = 1u << 6; } using PaintWindowFlags = uint32_t; wlserver_vk_swapchain_feedback* steamcompmgr_get_base_layer_swapchain_feedback() { if ( g_HeldCommits[ HELD_COMMIT_BASE ] == nullptr ) return nullptr; if ( !g_HeldCommits[ HELD_COMMIT_BASE ]->feedback ) return nullptr; return &(*g_HeldCommits[ HELD_COMMIT_BASE ]->feedback); } gamescope::ConVar cv_paint_debug_pause_base_plane( "paint_debug_pause_base_plane", false, "Pause updates to the base plane." ); static FrameInfo_t::Layer_t * paint_window_commit( const gamescope::Rc &lastCommit, steamcompmgr_win_t *w, steamcompmgr_win_t *scaleW, struct FrameInfo_t *frameInfo, MouseCursor *cursor, PaintWindowFlags flags = 0, float flOpacityScale = 1.0f, steamcompmgr_win_t *fit = nullptr ) { int32_t sourceWidth, sourceHeight; int32_t baseWidth, baseHeight; int drawXOffset = 0, drawYOffset = 0; float currentScaleRatio_x = 1.0; float currentScaleRatio_y = 1.0; float baseScaleRatio_x = 1.0; float baseScaleRatio_y = 1.0; // Exit out if we have no window or // no commit. // // We may have no commit if we're an overlay, // in which case, we don't want to add it, // or in the case of the base plane, this is our // first ever frame so we have no cached base layer // to hold on to, so we should not add a layer in that // instance either. if (!w || lastCommit == nullptr) return nullptr; // Base plane will stay as tex=0 if we don't have contents yet, which will // make us fall back to compositing and use the Vulkan null texture int curLayer = frameInfo->layerCount++; FrameInfo_t::Layer_t *layer = &frameInfo->layers[ curLayer ]; layer->filter = ( flags & PaintWindowFlag::NoFilter ) ? GamescopeUpscaleFilter::LINEAR : g_upscaleFilter; layer->tex = lastCommit->GetTexture( layer->filter, g_upscaleScaler, layer->colorspace ); if ( flags & PaintWindowFlag::NoScale ) { sourceWidth = currentOutputWidth; sourceHeight = currentOutputHeight; } else { // If w == scaleW, then scale the window by the committed buffer size // instead of the window size. // // Some games like Halo Infinite still make swapchains that are eg. // 3840x2160 on a 720p window if you do borderless fullscreen. // // Typically XWayland would do a blit here to avoid that, but when we // are using the bypass layer, we don't get that, so we need to handle // this case explicitly. if (w == scaleW) { sourceWidth = layer->tex->width(); sourceHeight = layer->tex->height(); baseWidth = lastCommit->vulkanTex->width(); baseHeight = lastCommit->vulkanTex->height(); } else { sourceWidth = scaleW->GetGeometry().nWidth; sourceHeight = scaleW->GetGeometry().nHeight; baseWidth = sourceWidth; baseHeight = sourceHeight; } if ( fit ) { // If we have an override window, try to fit it in as long as it won't make our scale go below 1.0. sourceWidth = std::max( sourceWidth, clamp( fit->GetGeometry().nX + fit->GetGeometry().nWidth, 0, currentOutputWidth ) ); sourceHeight = std::max( sourceHeight, clamp( fit->GetGeometry().nY + fit->GetGeometry().nHeight, 0, currentOutputHeight ) ); baseWidth = std::max( baseWidth, clamp( fit->GetGeometry().nX + fit->GetGeometry().nWidth, 0, currentOutputWidth ) ); baseHeight = std::max( baseHeight, clamp( fit->GetGeometry().nY + fit->GetGeometry().nHeight, 0, currentOutputHeight ) ); } } bool offset = ( ( w->GetGeometry().nX || w->GetGeometry().nY ) && w != scaleW ); if (sourceWidth != (int32_t)currentOutputWidth || sourceHeight != (int32_t)currentOutputHeight || offset || globalScaleRatio != 1.0f) { calc_scale_factor(currentScaleRatio_x, currentScaleRatio_y, sourceWidth, sourceHeight); drawXOffset = ((int)currentOutputWidth - (int)sourceWidth * currentScaleRatio_x) / 2.0f; drawYOffset = ((int)currentOutputHeight - (int)sourceHeight * currentScaleRatio_y) / 2.0f; if ( w != scaleW ) { drawXOffset += w->GetGeometry().nX * currentScaleRatio_x; drawYOffset += w->GetGeometry().nY * currentScaleRatio_y; } calc_scale_factor(baseScaleRatio_x, baseScaleRatio_y, baseWidth, baseHeight); if ( zoomScaleRatio != 1.0 ) { drawXOffset += (((int)baseWidth / 2) - (cursor ? cursor->x() : 0)) * baseScaleRatio_x; drawYOffset += (((int)baseHeight / 2) - (cursor ? cursor->y() : 0)) * baseScaleRatio_y; } } layer->opacity = ( (w->isOverlay || w->isExternalOverlay) ? w->opacity / (float)OPAQUE : 1.0f ) * flOpacityScale; layer->scale.x = 1.0 / currentScaleRatio_x; layer->scale.y = 1.0 / currentScaleRatio_y; if ( w != scaleW ) { layer->offset.x = -drawXOffset; layer->offset.y = -drawYOffset; } else { layer->offset.x = -drawXOffset; layer->offset.y = -drawYOffset; } layer->blackBorder = flags & PaintWindowFlag::DrawBorders; layer->applyColorMgmt = g_ColorMgmt.pending.enabled; layer->zpos = g_zposBase; if ( w != scaleW ) { layer->zpos = g_zposOverride; } if ( w->isOverlay || w->isSteamStreamingClient ) { layer->zpos = g_zposOverlay; } if ( w->isExternalOverlay ) { layer->zpos = g_zposExternalOverlay; } layer->hdr_metadata_blob = nullptr; if (lastCommit->feedback) { layer->hdr_metadata_blob = lastCommit->feedback->hdr_metadata_blob; } layer->ctm = nullptr; if (layer->colorspace == GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB) layer->ctm = s_scRGB709To2020Matrix; if (layer->filter == GamescopeUpscaleFilter::PIXEL) { // Don't bother doing more expensive filtering if we are sharp + integer. if (float_is_integer(currentScaleRatio_x) && float_is_integer(currentScaleRatio_y)) layer->filter = GamescopeUpscaleFilter::NEAREST; } layer->eAlphaBlendingMode = ( flags & PaintWindowFlag::CoverageMode ) ? ALPHA_BLENDING_MODE_COVERAGE : ALPHA_BLENDING_MODE_PREMULTIPLIED; return layer; } static void paint_window(steamcompmgr_win_t *w, steamcompmgr_win_t *scaleW, struct FrameInfo_t *frameInfo, MouseCursor *cursor, PaintWindowFlags flags = 0, float flOpacityScale = 1.0f, steamcompmgr_win_t *fit = nullptr ) { gamescope::Rc lastCommit; if ( w ) get_window_last_done_commit( w, lastCommit ); if ( flags & PaintWindowFlag::BasePlane ) { if ( lastCommit == nullptr || cv_paint_debug_pause_base_plane ) { // If we're the base plane and have no valid contents // pick up that buffer we've been holding onto if we have one. if ( g_HeldCommits[ HELD_COMMIT_BASE ] != nullptr ) { paint_cached_base_layer( g_HeldCommits[ HELD_COMMIT_BASE ], g_CachedPlanes[ HELD_COMMIT_BASE ], frameInfo, flOpacityScale, true ); return; } } else { if ( g_bPendingFade ) { fadeOutStartTime = get_time_in_milliseconds(); g_bPendingFade = false; } } } FrameInfo_t::Layer_t *layer = paint_window_commit( lastCommit, w, scaleW, frameInfo, cursor, flags, flOpacityScale, fit ); if ( layer && ( flags & PaintWindowFlag::BasePlane ) ) { BaseLayerInfo_t basePlane = {}; basePlane.scale[0] = layer->scale.x; basePlane.scale[1] = layer->scale.y; basePlane.offset[0] = layer->offset.x; basePlane.offset[1] = layer->offset.y; basePlane.opacity = layer->opacity; basePlane.filter = layer->filter; basePlane.eAlphaBlendingMode = layer->eAlphaBlendingMode; g_CachedPlanes[ HELD_COMMIT_BASE ] = basePlane; if ( !(flags & PaintWindowFlag::FadeTarget) ) g_CachedPlanes[ HELD_COMMIT_FADE ] = basePlane; g_uCurrentBasePlaneCommitID = lastCommit->commitID; g_bCurrentBasePlaneIsFifo = lastCommit->IsPerfOverlayFIFO(); } } bool g_bFirstFrame = true; static bool is_fading_out() { return fadeOutStartTime || g_bPendingFade; } static void update_touch_scaling( const struct FrameInfo_t *frameInfo ) { if ( !frameInfo->layerCount ) return; focusedWindowScaleX = frameInfo->layers[ frameInfo->layerCount - 1 ].scale.x; focusedWindowScaleY = frameInfo->layers[ frameInfo->layerCount - 1 ].scale.y; focusedWindowOffsetX = frameInfo->layers[ frameInfo->layerCount - 1 ].offset.x; focusedWindowOffsetY = frameInfo->layers[ frameInfo->layerCount - 1 ].offset.y; } #if HAVE_PIPEWIRE static void paint_pipewire() { static struct pipewire_buffer *s_pPipewireBuffer = nullptr; // If the stream stopped/changed, and the underlying pw_buffer was thus // destroyed, then destroy this buffer and grab a new one. if ( s_pPipewireBuffer && s_pPipewireBuffer->IsStale() ) { pipewire_destroy_buffer( s_pPipewireBuffer ); s_pPipewireBuffer = nullptr; } // Queue up a buffer with some metadata. if ( !s_pPipewireBuffer ) s_pPipewireBuffer = dequeue_pipewire_buffer(); if ( !s_pPipewireBuffer || !s_pPipewireBuffer->texture ) return; struct FrameInfo_t frameInfo = {}; frameInfo.applyOutputColorMgmt = true; frameInfo.outputEncodingEOTF = EOTF_Gamma22; frameInfo.allowVRR = false; frameInfo.bFadingOut = false; // Apply screenshot-style color management. for ( uint32_t nInputEOTF = 0; nInputEOTF < EOTF_Count; nInputEOTF++ ) { frameInfo.lut3D[nInputEOTF] = g_ScreenshotColorMgmtLuts[nInputEOTF].vk_lut3d; frameInfo.shaperLut[nInputEOTF] = g_ScreenshotColorMgmtLuts[nInputEOTF].vk_lut1d; } const uint64_t ulFocusAppId = s_pPipewireBuffer->gamescope_info.focus_appid; focus_t *pFocus = nullptr; if ( ulFocusAppId ) { static uint64_t s_ulLastFocusAppId = 0; bool bAppIdChange = ulFocusAppId != s_ulLastFocusAppId; if ( bAppIdChange ) { xwm_log.infof( "Exposing appid %lu (%u 32-bit) focus-wise on pipewire stream.", ulFocusAppId, uint32_t( ulFocusAppId ) ); s_ulLastFocusAppId = ulFocusAppId; } static focus_t s_PipewireFocus{}; if ( s_PipewireFocus.IsDirty() || bAppIdChange ) { std::vector vecPossibleFocusWindows = GetGlobalPossibleFocusWindows(); std::vector vecAppIds{ uint32_t( ulFocusAppId ) }; pick_primary_focus_and_override( &s_PipewireFocus, None, vecPossibleFocusWindows, false, vecAppIds ); } pFocus = &s_PipewireFocus; } else { pFocus = GetCurrentFocus(); } if ( !pFocus->focusWindow ) return; const bool bAppIdMatches = !ulFocusAppId || pFocus->focusWindow->appID == ulFocusAppId; if ( !bAppIdMatches ) return; // If the commits are the same as they were last time, don't repaint and don't push a new buffer on the stream. static uint64_t s_ulLastFocusCommitId = 0; static uint64_t s_ulLastOverrideCommitId = 0; uint64_t ulFocusCommitId = window_last_done_commit_id( pFocus->focusWindow ); uint64_t ulOverrideCommitId = window_last_done_commit_id( pFocus->overrideWindow ); if ( ulFocusCommitId == s_ulLastFocusCommitId && ulOverrideCommitId == s_ulLastOverrideCommitId ) return; s_ulLastFocusCommitId = ulFocusCommitId; s_ulLastOverrideCommitId = ulOverrideCommitId; uint32_t uWidth = s_pPipewireBuffer->texture->width(); uint32_t uHeight = s_pPipewireBuffer->texture->height(); const uint32_t uCompositeDebugBackup = g_uCompositeDebug; const uint32_t uBackupWidth = currentOutputWidth; const uint32_t uBackupHeight = currentOutputHeight; g_uCompositeDebug = 0; currentOutputWidth = uWidth; currentOutputHeight = uHeight; // Paint the windows we have onto the Pipewire stream. paint_window( pFocus->focusWindow, pFocus->focusWindow, &frameInfo, nullptr, 0, 1.0f, pFocus->overrideWindow ); if ( pFocus->overrideWindow && !pFocus->focusWindow->isSteamStreamingClient ) paint_window( pFocus->overrideWindow, pFocus->focusWindow, &frameInfo, nullptr, PaintWindowFlag::NoFilter, 1.0f, pFocus->overrideWindow ); gamescope::Rc pRGBTexture = s_pPipewireBuffer->texture->isYcbcr() ? vulkan_acquire_screenshot_texture( uWidth, uHeight, false, DRM_FORMAT_XRGB2101010 ) : gamescope::Rc{ s_pPipewireBuffer->texture }; gamescope::Rc pYUVTexture = s_pPipewireBuffer->texture->isYcbcr() ? s_pPipewireBuffer->texture : nullptr; std::optional oPipewireSequence = vulkan_screenshot( &frameInfo, pRGBTexture, pYUVTexture ); // If we ever want the fat compositing path, use this. //std::optional oPipewireSequence = vulkan_composite( &frameInfo, s_pPipewireBuffer->texture, false, pRGBTexture, false ); g_uCompositeDebug = uCompositeDebugBackup; currentOutputWidth = uBackupWidth; currentOutputHeight = uBackupHeight; if ( oPipewireSequence ) { vulkan_wait( *oPipewireSequence, true ); push_pipewire_buffer( s_pPipewireBuffer ); s_pPipewireBuffer = nullptr; } } #endif gamescope::ConVar cv_cursor_composite{ "cursor_composite", 1, "0 = Never composite a cursor. 1 = Composite cursor when not nested. 2 = Always composite a cursor manually" }; bool ShouldDrawCursor() { if ( cv_cursor_composite == 2 ) return true; if ( cv_cursor_composite == 0 ) return false; if ( g_bForceRelativeMouse ) return true; global_focus_t *pFocus = GetCurrentFocus(); if ( !pFocus ) return false; return !pFocus->GetNestedHints(); } gamescope::ConVar cv_paint_primary_plane{ "paint_primary_plane", true }; gamescope::ConVar cv_paint_override_redirect_plane{ "paint_override_redirect_plane", true }; gamescope::ConVar cv_paint_steam_overlay_plane{ "paint_steam_overlay_plane", true }; gamescope::ConVar cv_paint_external_overlay_plane{ "paint_external_overlay_plane", true }; gamescope::ConVar cv_paint_cursor_plane{ "paint_cursor_plane", true }; gamescope::ConVar cv_paint_mura_plane{ "paint_mura_plane", true }; static void paint_all( global_focus_t *pFocus, bool async ) { if ( !pFocus ) return; gamescope::IBackendConnector *pConnector = pFocus->pVirtualConnector.get(); if ( !pConnector ) pConnector = GetBackend()->GetCurrentConnector(); gamescope_xwayland_server_t *root_server = wlserver_get_xwayland_server(0); xwayland_ctx_t *root_ctx = root_server->ctx.get(); static long long int paintID = 0; update_color_mgmt(); paintID++; gpuvis_trace_begin_ctx_printf( paintID, "paint_all" ); steamcompmgr_win_t *w; steamcompmgr_win_t *overlay; steamcompmgr_win_t *externalOverlay; steamcompmgr_win_t *notification; steamcompmgr_win_t *override; steamcompmgr_win_t *input; unsigned int currentTime = get_time_in_milliseconds(); bool fadingOut = ( currentTime - fadeOutStartTime < g_FadeOutDuration || g_bPendingFade ) && g_HeldCommits[HELD_COMMIT_FADE] != nullptr; w = pFocus->focusWindow; overlay = pFocus->overlayWindow; externalOverlay = pFocus->externalOverlayWindow; notification = pFocus->notificationWindow; override = pFocus->overrideWindow; input = pFocus->inputFocusWindow; if (++frameCounter == 300) { currentFrameRate = 300 * 1000.0f / (currentTime - lastSampledFrameTime); lastSampledFrameTime = currentTime; frameCounter = 0; stats_printf( "fps=%f\n", currentFrameRate ); if ( window_is_steam( w ) ) { stats_printf( "focus=steam\n" ); } else { stats_printf( "focus=%i\n", w ? w->appID : 0 ); } } struct FrameInfo_t frameInfo = {}; frameInfo.applyOutputColorMgmt = g_ColorMgmt.pending.enabled; frameInfo.outputEncodingEOTF = g_ColorMgmt.pending.outputEncodingEOTF; frameInfo.allowVRR = cv_adaptive_sync; frameInfo.bFadingOut = fadingOut; // If the window we'd paint as the base layer is the streaming client, // find the video underlay and put it up first in the scenegraph if ( cv_paint_primary_plane ) { if ( w ) { if ( w->isSteamStreamingClient == true ) { steamcompmgr_win_t *videow = NULL; bool bHasVideoUnderlay = false; gamescope_xwayland_server_t *server = NULL; for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++) { for ( videow = server->ctx->list; videow; videow = videow->xwayland().next ) { if ( videow->isSteamStreamingClientVideo == true ) { // TODO: also check matching AppID so we can have several pairs paint_window(videow, videow, &frameInfo, pFocus->cursor, PaintWindowFlag::BasePlane | PaintWindowFlag::DrawBorders); bHasVideoUnderlay = true; break; } } } int nOldLayerCount = frameInfo.layerCount; uint32_t flags = 0; if ( !bHasVideoUnderlay ) flags |= PaintWindowFlag::BasePlane; paint_window(w, w, &frameInfo, pFocus->cursor, flags); if ( pFocus == GetCurrentFocus() ) update_touch_scaling( &frameInfo ); // paint UI unless it's fully hidden, which it communicates to us through opacity=0 // we paint it to extract scaling coefficients above, then remove the layer if one was added if ( w->opacity == TRANSLUCENT && bHasVideoUnderlay && nOldLayerCount < frameInfo.layerCount ) frameInfo.layerCount--; } else { if ( fadingOut ) { float opacityScale = g_bPendingFade ? 0.0f : ((currentTime - fadeOutStartTime) / (float)g_FadeOutDuration); paint_cached_base_layer(g_HeldCommits[HELD_COMMIT_FADE], g_CachedPlanes[HELD_COMMIT_FADE], &frameInfo, 1.0f - opacityScale, false); paint_window(w, w, &frameInfo, pFocus->cursor, PaintWindowFlag::BasePlane | PaintWindowFlag::FadeTarget | PaintWindowFlag::DrawBorders, opacityScale, override); } else { { if ( g_HeldCommits[HELD_COMMIT_FADE] != nullptr ) { g_HeldCommits[HELD_COMMIT_FADE] = nullptr; g_bPendingFade = false; fadeOutStartTime = 0; pFocus->fadeWindow = None; } } // Just draw focused window as normal, be it Steam or the game paint_window(w, w, &frameInfo, pFocus->cursor, PaintWindowFlag::BasePlane | PaintWindowFlag::DrawBorders, 1.0f, override); bool needsScaling = frameInfo.layers[0].scale.x < 0.999f && frameInfo.layers[0].scale.y < 0.999f; frameInfo.useFSRLayer0 = g_upscaleFilter == GamescopeUpscaleFilter::FSR && needsScaling; frameInfo.useNISLayer0 = g_upscaleFilter == GamescopeUpscaleFilter::NIS && needsScaling; } if ( pFocus == GetCurrentFocus() ) update_touch_scaling( &frameInfo ); } } else { if ( g_HeldCommits[HELD_COMMIT_BASE] != nullptr ) { float opacityScale = 1.0f; if ( fadingOut ) { opacityScale = g_bPendingFade ? 0.0f : ((currentTime - fadeOutStartTime) / (float)g_FadeOutDuration); } paint_cached_base_layer( g_HeldCommits[HELD_COMMIT_BASE], g_CachedPlanes[HELD_COMMIT_BASE], &frameInfo, opacityScale, true ); } } } // TODO: We want to paint this at the same scale as the normal window and probably // with an offset. // Josh: No override if we're streaming video // as we will have too many layers. Better to be safe than sorry. if ( override && w && !w->isSteamStreamingClient && cv_paint_override_redirect_plane ) { paint_window(override, w, &frameInfo, pFocus->cursor, PaintWindowFlag::NoFilter, 1.0f, override); // Don't update touch scaling for frameInfo. We don't ever make it our // wlserver_mousefocus window. //update_touch_scaling( &frameInfo ); } // If we have any layers that aren't a cursor or overlay, then we have valid contents for presentation. const bool bValidContents = frameInfo.layerCount > 0; if (externalOverlay && cv_paint_external_overlay_plane ) { if (externalOverlay->opacity) { paint_window(externalOverlay, externalOverlay, &frameInfo, pFocus->cursor, PaintWindowFlag::NoScale | PaintWindowFlag::NoFilter | ( cv_overlay_unmultiplied_alpha ? PaintWindowFlag::CoverageMode : 0 ) ); if ( externalOverlay == pFocus->inputFocusWindow && pFocus == GetCurrentFocus() ) update_touch_scaling( &frameInfo ); } } if ( cv_paint_steam_overlay_plane ) { if (overlay && overlay->opacity ) { paint_window(overlay, overlay, &frameInfo, pFocus->cursor, PaintWindowFlag::DrawBorders | PaintWindowFlag::NoFilter | ( cv_overlay_unmultiplied_alpha ? PaintWindowFlag::CoverageMode : 0 ) ); if ( overlay == pFocus->inputFocusWindow && pFocus == GetCurrentFocus() ) update_touch_scaling( &frameInfo ); } else if ( !GetBackend()->UsesVulkanSwapchain() && GetBackend()->IsSessionBased() ) { auto tex = vulkan_get_hacky_blank_texture(); if ( tex != nullptr ) { // HACK! HACK HACK HACK // To avoid stutter when toggling the overlay on int curLayer = frameInfo.layerCount++; FrameInfo_t::Layer_t *layer = &frameInfo.layers[ curLayer ]; layer->scale.x = g_nOutputWidth == tex->width() ? 1.0f : tex->width() / (float)g_nOutputWidth; layer->scale.y = g_nOutputHeight == tex->height() ? 1.0f : tex->height() / (float)g_nOutputHeight; layer->offset.x = 0.0f; layer->offset.y = 0.0f; layer->opacity = 1.0f; // BLAH layer->zpos = g_zposOverlay; layer->applyColorMgmt = g_ColorMgmt.pending.enabled; layer->eAlphaBlendingMode = ALPHA_BLENDING_MODE_COVERAGE; // misyl: Always coverage and not premult in case of a Look applied. layer->colorspace = GAMESCOPE_APP_TEXTURE_COLORSPACE_LINEAR; layer->hdr_metadata_blob = nullptr; layer->ctm = nullptr; layer->tex = tex; layer->filter = GamescopeUpscaleFilter::NEAREST; layer->blackBorder = true; } } } if (notification) { if (notification->opacity) { paint_window(notification, notification, &frameInfo, pFocus->cursor, PaintWindowFlag::NotificationMode | PaintWindowFlag::NoFilter); } } if (input) { // Make sure to un-dirty the texture before we do any painting logic. // We determine whether we are grabbed etc this way. pFocus->cursor->undirty(); } // Draw cursor if we need to if (input && ShouldDrawCursor() && cv_paint_cursor_plane) { pFocus->cursor->paint( input, w == input ? override : nullptr, &frameInfo); } if ( !bValidContents || GetBackend()->IsPaused() ) { return; } unsigned int blurFadeTime = get_time_in_milliseconds() - g_BlurFadeStartTime; bool blurFading = blurFadeTime < g_BlurFadeDuration; BlurMode currentBlurMode = blurFading ? std::max(g_BlurMode, g_BlurModeOld) : g_BlurMode; if (currentBlurMode && !(frameInfo.layerCount <= 1 && currentBlurMode == BLUR_MODE_COND)) { frameInfo.blurLayer0 = currentBlurMode; frameInfo.blurRadius = g_BlurRadius; if (blurFading) { float ratio = blurFadeTime / (float) g_BlurFadeDuration; bool fadingIn = g_BlurMode > g_BlurModeOld; if (!fadingIn) ratio = 1.0 - ratio; frameInfo.blurRadius = ratio * g_BlurRadius; } frameInfo.useFSRLayer0 = false; frameInfo.useNISLayer0 = false; } g_bFSRActive = frameInfo.useFSRLayer0; g_bFirstFrame = false; update_app_target_refresh_cycle(); const bool bSupportsDynamicRefresh = pConnector && !pConnector->GetValidDynamicRefreshRates().empty(); if ( bSupportsDynamicRefresh ) { auto rates = pConnector->GetValidDynamicRefreshRates(); int nDynamicRefreshHz = g_nDynamicRefreshRate[GetBackend()->GetScreenType()]; int nTargetRefreshHz = nDynamicRefreshHz && steamcompmgr_window_should_refresh_switch( pFocus->focusWindow ) ? nDynamicRefreshHz : int( rates[ rates.size() - 1 ] ); uint64_t now = get_time_in_nanos(); // The actual output hz generated by the mode, could be off by one either side, due to rounding. // // Check what we cached for the current dynamic refresh Hz we put in -- otherwise, convert // g_nOutputRefresh -> Hz rounded. int32_t nCurrentDynamicOutputHz = g_nDynamicRefreshHz ? g_nDynamicRefreshHz : gamescope::ConvertmHzToHz( g_nOutputRefresh ); if ( nCurrentDynamicOutputHz == nTargetRefreshHz ) { g_uDynamicRefreshEqualityTime = now; } else if ( g_uDynamicRefreshEqualityTime + g_uDynamicRefreshDelay < now ) { GetBackend()->HackTemporarySetDynamicRefresh( nTargetRefreshHz ); } } bool bDoMuraCompensation = is_mura_correction_enabled() && frameInfo.layerCount && cv_paint_mura_plane; if ( bDoMuraCompensation ) { auto& MuraCorrectionImage = s_MuraCorrectionImage[GetBackend()->GetScreenType()]; int curLayer = frameInfo.layerCount++; FrameInfo_t::Layer_t *layer = &frameInfo.layers[ curLayer ]; layer->applyColorMgmt = false; layer->scale = vec2_t{ 1.0f, 1.0f }; layer->blackBorder = true; layer->colorspace = GAMESCOPE_APP_TEXTURE_COLORSPACE_PASSTHRU; layer->hdr_metadata_blob = nullptr; layer->opacity = 1.0f; layer->zpos = g_zposMuraCorrection; layer->filter = GamescopeUpscaleFilter::NEAREST; layer->tex = MuraCorrectionImage; layer->ctm = s_MuraCTMBlob[GetBackend()->GetScreenType()]; // Blending needs to be done in Gamma 2.2 space for mura correction to work. frameInfo.applyOutputColorMgmt = false; } for (uint32_t i = 0; i < EOTF_Count; i++) { if ( g_ColorMgmtLuts[i].HasLuts() ) { frameInfo.shaperLut[i] = g_ColorMgmtLuts[i].vk_lut1d; frameInfo.lut3D[i] = g_ColorMgmtLuts[i].vk_lut3d; } } if ( pConnector && pConnector->Present( &frameInfo, async ) != 0 ) { return; } std::optional oScreenshotInfo = gamescope::CScreenshotManager::Get().ProcessPendingScreenshot(); if ( oScreenshotInfo ) { std::filesystem::path path = std::filesystem::path{ oScreenshotInfo->szScreenshotPath }; uint32_t drmCaptureFormat = DRM_FORMAT_INVALID; if ( path.extension() == ".avif" ) drmCaptureFormat = DRM_FORMAT_XRGB2101010; else if ( path.extension() == ".png" ) drmCaptureFormat = DRM_FORMAT_XRGB8888; else if ( path.extension() == ".nv12.bin" ) drmCaptureFormat = DRM_FORMAT_NV12; gamescope::Rc pScreenshotTexture; if ( drmCaptureFormat != DRM_FORMAT_INVALID ) pScreenshotTexture = vulkan_acquire_screenshot_texture( g_nOutputWidth, g_nOutputHeight, false, drmCaptureFormat ); if ( pScreenshotTexture ) { bool bHDRScreenshot = path.extension() == ".avif" && frameInfo.layerCount > 0 && ColorspaceIsHDR( frameInfo.layers[0].colorspace ) && oScreenshotInfo->eScreenshotType != GAMESCOPE_CONTROL_SCREENSHOT_TYPE_SCREEN_BUFFER; if ( drmCaptureFormat == DRM_FORMAT_NV12 || oScreenshotInfo->eScreenshotType != GAMESCOPE_CONTROL_SCREENSHOT_TYPE_SCREEN_BUFFER ) { // Basically no color mgmt applied for screenshots. (aside from being able to handle HDR content with LUTs) for ( uint32_t nInputEOTF = 0; nInputEOTF < EOTF_Count; nInputEOTF++ ) { auto& luts = bHDRScreenshot ? g_ScreenshotColorMgmtLutsHDR : g_ScreenshotColorMgmtLuts; frameInfo.lut3D[nInputEOTF] = luts[nInputEOTF].vk_lut3d; frameInfo.shaperLut[nInputEOTF] = luts[nInputEOTF].vk_lut1d; } if ( oScreenshotInfo->eScreenshotType == GAMESCOPE_CONTROL_SCREENSHOT_TYPE_BASE_PLANE_ONLY ) { // Remove everything but base planes from the screenshot. for (int i = 0; i < frameInfo.layerCount; i++) { if (frameInfo.layers[i].zpos >= (int)g_zposExternalOverlay) { frameInfo.layerCount = i; break; } } } else { if ( is_mura_correction_enabled() ) { // Remove the last layer which is for mura... for (int i = 0; i < frameInfo.layerCount; i++) { if (frameInfo.layers[i].zpos >= (int)g_zposMuraCorrection) { frameInfo.layerCount = i; break; } } } } // Re-enable output color management (blending) if it was disabled by mura. frameInfo.applyOutputColorMgmt = true; } frameInfo.outputEncodingEOTF = bHDRScreenshot ? EOTF_PQ : EOTF_Gamma22; uint32_t uCompositeDebugBackup = g_uCompositeDebug; if ( oScreenshotInfo->eScreenshotType != GAMESCOPE_CONTROL_SCREENSHOT_TYPE_SCREEN_BUFFER ) { g_uCompositeDebug = 0; } std::optional oScreenshotSeq; if ( drmCaptureFormat == DRM_FORMAT_NV12 ) oScreenshotSeq = vulkan_composite( &frameInfo, pScreenshotTexture, false, nullptr ); else if ( oScreenshotInfo->eScreenshotType == GAMESCOPE_CONTROL_SCREENSHOT_TYPE_FULL_COMPOSITION || oScreenshotInfo->eScreenshotType == GAMESCOPE_CONTROL_SCREENSHOT_TYPE_SCREEN_BUFFER ) oScreenshotSeq = vulkan_composite( &frameInfo, nullptr, false, pScreenshotTexture ); else oScreenshotSeq = vulkan_screenshot( &frameInfo, pScreenshotTexture, nullptr ); if ( oScreenshotInfo->eScreenshotType != GAMESCOPE_CONTROL_SCREENSHOT_TYPE_SCREEN_BUFFER ) { g_uCompositeDebug = uCompositeDebugBackup; } if ( !oScreenshotSeq ) { xwm_log.errorf("vulkan_screenshot failed"); return; } vulkan_wait( *oScreenshotSeq, false ); uint16_t maxCLLNits = 0; uint16_t maxFALLNits = 0; if ( bHDRScreenshot ) { // Unfortunately games give us very bogus values here. // Thus we don't really use them. // Instead rely on the display it was initially tonemapped for. //if ( g_ColorMgmt.current.appHDRMetadata ) //{ // maxCLLNits = g_ColorMgmt.current.appHDRMetadata->metadata.hdmi_metadata_type1.max_cll; // maxFALLNits = g_ColorMgmt.current.appHDRMetadata->metadata.hdmi_metadata_type1.max_fall; //} if ( !maxCLLNits && !maxFALLNits ) { if ( pConnector ) { maxCLLNits = pConnector->GetHDRInfo().uMaxContentLightLevel; maxFALLNits = pConnector->GetHDRInfo().uMaxFrameAverageLuminance; } } if ( !maxCLLNits && !maxFALLNits ) { maxCLLNits = g_ColorMgmt.pending.flInternalDisplayBrightness; maxFALLNits = g_ColorMgmt.pending.flInternalDisplayBrightness * 0.8f; } } std::thread screenshotThread = std::thread([=] { pthread_setname_np( pthread_self(), "gamescope-scrsh" ); const uint8_t *mappedData = pScreenshotTexture->mappedData(); bool bScreenshotSuccess = false; if ( pScreenshotTexture->format() == VK_FORMAT_A2R10G10B10_UNORM_PACK32 ) { // Make our own copy of the image to remove the alpha channel. constexpr uint32_t kCompCnt = 3; auto imageData = std::vector( g_nOutputWidth * g_nOutputHeight * kCompCnt ); for (uint32_t y = 0; y < g_nOutputHeight; y++) { for (uint32_t x = 0; x < g_nOutputWidth; x++) { uint32_t *pInPixel = (uint32_t *)&mappedData[(y * pScreenshotTexture->rowPitch()) + x * (32 / 8)]; uint32_t uInPixel = *pInPixel; imageData[y * g_nOutputWidth * kCompCnt + x * kCompCnt + 0] = (uInPixel & (0b1111111111 << 20)) >> 20; imageData[y * g_nOutputWidth * kCompCnt + x * kCompCnt + 1] = (uInPixel & (0b1111111111 << 10)) >> 10; imageData[y * g_nOutputWidth * kCompCnt + x * kCompCnt + 2] = (uInPixel & (0b1111111111 << 0)) >> 0; } } assert( HAVE_AVIF ); #if HAVE_AVIF avifResult avifResult = AVIF_RESULT_OK; avifImage *pAvifImage = avifImageCreate( g_nOutputWidth, g_nOutputHeight, 10, AVIF_PIXEL_FORMAT_YUV444 ); defer( avifImageDestroy( pAvifImage ) ); pAvifImage->yuvRange = AVIF_RANGE_FULL; pAvifImage->colorPrimaries = bHDRScreenshot ? AVIF_COLOR_PRIMARIES_BT2020 : AVIF_COLOR_PRIMARIES_BT709; pAvifImage->transferCharacteristics = bHDRScreenshot ? AVIF_TRANSFER_CHARACTERISTICS_SMPTE2084 : AVIF_TRANSFER_CHARACTERISTICS_SRGB; // We are not actually using YUV, but storing raw GBR (yes not RGB) data // This does not compress as well, but is always lossless! pAvifImage->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY; if ( oScreenshotInfo->eScreenshotType == GAMESCOPE_CONTROL_SCREENSHOT_TYPE_SCREEN_BUFFER ) { // When dumping the screen output buffer for debugging, // mark the primaries as UNKNOWN as stuff has likely been transformed // to native if HDR on Deck OLED etc. // We want everything to be seen unadulterated by a viewer/image editor. pAvifImage->colorPrimaries = AVIF_COLOR_PRIMARIES_UNKNOWN; } if ( bHDRScreenshot ) { pAvifImage->clli.maxCLL = maxCLLNits; pAvifImage->clli.maxPALL = maxFALLNits; } avifRGBImage rgbAvifImage{}; avifRGBImageSetDefaults( &rgbAvifImage, pAvifImage ); rgbAvifImage.format = AVIF_RGB_FORMAT_RGB; rgbAvifImage.ignoreAlpha = AVIF_TRUE; rgbAvifImage.pixels = (uint8_t *)imageData.data(); rgbAvifImage.rowBytes = g_nOutputWidth * kCompCnt * sizeof( uint16_t ); if ( ( avifResult = avifImageRGBToYUV( pAvifImage, &rgbAvifImage ) ) != AVIF_RESULT_OK ) // Not really! See Matrix Coefficients IDENTITY above. { xwm_log.errorf( "Failed to convert RGB to YUV: %u", avifResult ); return; } avifEncoder *pEncoder = avifEncoderCreate(); defer( avifEncoderDestroy( pEncoder ) ); pEncoder->quality = AVIF_QUALITY_LOSSLESS; pEncoder->qualityAlpha = AVIF_QUALITY_LOSSLESS; pEncoder->speed = AVIF_SPEED_FASTEST; if ( ( avifResult = avifEncoderAddImage( pEncoder, pAvifImage, 1, AVIF_ADD_IMAGE_FLAG_SINGLE ) ) != AVIF_RESULT_OK ) { xwm_log.errorf( "Failed to add image to avif encoder: %u", avifResult ); return; } avifRWData avifOutput = AVIF_DATA_EMPTY; defer( avifRWDataFree( &avifOutput ) ); if ( ( avifResult = avifEncoderFinish( pEncoder, &avifOutput ) ) != AVIF_RESULT_OK ) { xwm_log.errorf( "Failed to finish encoder: %u", avifResult ); return; } FILE *pScreenshotFile = nullptr; if ( ( pScreenshotFile = fopen( oScreenshotInfo->szScreenshotPath.c_str(), "wb" ) ) == nullptr ) { xwm_log.errorf( "Failed to fopen file: %s", oScreenshotInfo->szScreenshotPath.c_str() ); return; } fwrite( avifOutput.data, 1, avifOutput.size, pScreenshotFile ); fclose( pScreenshotFile ); xwm_log.infof( "Screenshot saved to %s", oScreenshotInfo->szScreenshotPath.c_str() ); bScreenshotSuccess = true; #endif } else if (pScreenshotTexture->format() == VK_FORMAT_B8G8R8A8_UNORM) { // Make our own copy of the image to remove the alpha channel. auto imageData = std::vector(currentOutputWidth * currentOutputHeight * 4); const uint32_t comp = 4; const uint32_t pitch = currentOutputWidth * comp; for (uint32_t y = 0; y < currentOutputHeight; y++) { for (uint32_t x = 0; x < currentOutputWidth; x++) { // BGR... imageData[y * pitch + x * comp + 0] = mappedData[y * pScreenshotTexture->rowPitch() + x * comp + 2]; imageData[y * pitch + x * comp + 1] = mappedData[y * pScreenshotTexture->rowPitch() + x * comp + 1]; imageData[y * pitch + x * comp + 2] = mappedData[y * pScreenshotTexture->rowPitch() + x * comp + 0]; imageData[y * pitch + x * comp + 3] = 255; } } if ( stbi_write_png( oScreenshotInfo->szScreenshotPath.c_str(), currentOutputWidth, currentOutputHeight, 4, imageData.data(), pitch ) ) { xwm_log.infof( "Screenshot saved to %s", oScreenshotInfo->szScreenshotPath.c_str() ); bScreenshotSuccess = true; } else { xwm_log.errorf( "Failed to save screenshot to %s", oScreenshotInfo->szScreenshotPath.c_str() ); } } else if (pScreenshotTexture->format() == VK_FORMAT_G8_B8R8_2PLANE_420_UNORM) { FILE *file = fopen( oScreenshotInfo->szScreenshotPath.c_str(), "wb" ); if (file) { fwrite(mappedData, 1, pScreenshotTexture->totalSize(), file ); fclose(file); char cmd[4096]; sprintf(cmd, "ffmpeg -f rawvideo -pixel_format nv12 -video_size %dx%d -i %s %s_encoded.png", pScreenshotTexture->width(), pScreenshotTexture->height(), oScreenshotInfo->szScreenshotPath.c_str(), oScreenshotInfo->szScreenshotPath.c_str() ); int ret = system(cmd); /* Above call may fail, ffmpeg returns 0 on success */ if (ret) { xwm_log.infof("Ffmpeg call return status %i", ret); xwm_log.errorf( "Failed to save screenshot to %s", oScreenshotInfo->szScreenshotPath.c_str() ); } else { xwm_log.infof("Screenshot saved to %s", oScreenshotInfo->szScreenshotPath.c_str()); bScreenshotSuccess = true; } } else { xwm_log.errorf( "Failed to save screenshot to %s", oScreenshotInfo->szScreenshotPath.c_str() ); } } if ( oScreenshotInfo->bX11PropertyRequested ) { XDeleteProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeScreenShotAtom ); XDeleteProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeDebugScreenShotAtom ); } if ( bScreenshotSuccess && oScreenshotInfo->bWaylandRequested ) { wlserver_lock(); for ( const auto &control : wlserver.gamescope_controls ) { gamescope_control_send_screenshot_taken( control, oScreenshotInfo->szScreenshotPath.c_str() ); } wlserver_unlock(); } }); screenshotThread.detach(); } else { xwm_log.errorf( "Oh no, we ran out of screenshot images. Not actually writing a screenshot." ); if ( oScreenshotInfo->bX11PropertyRequested ) { XDeleteProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeScreenShotAtom ); XDeleteProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeDebugScreenShotAtom ); } } } gpuvis_trace_end_ctx_printf( paintID, "paint_all" ); gpuvis_trace_printf( "paint_all %i layers", (int)frameInfo.layerCount ); } /* Get prop from window * not found: default * otherwise the value */ __attribute__((__no_sanitize_address__)) // x11 broken, returns format 32 even when it only malloc'ed one byte. :( static unsigned int get_prop(xwayland_ctx_t *ctx, Window win, Atom prop, unsigned int def, bool *found = nullptr ) { Atom actual; int format; unsigned long n, left; unsigned char *data; int result = XGetWindowProperty(ctx->dpy, win, prop, 0L, 1L, false, XA_CARDINAL, &actual, &format, &n, &left, &data); if (result == Success && data != NULL) { unsigned int i; memcpy(&i, data, sizeof(unsigned int)); XFree((void *) data); if ( found != nullptr ) { *found = true; } return i; } if ( found != nullptr ) { *found = false; } return def; } // vectored version, return value is whether anything was found __attribute__((__no_sanitize_address__)) // x11 broken :( bool get_prop( xwayland_ctx_t *ctx, Window win, Atom prop, std::vector< uint32_t > &vecResult ) { Atom actual; int format; unsigned long n, left; vecResult.clear(); uint64_t *data; int result = XGetWindowProperty(ctx->dpy, win, prop, 0L, ~0UL, false, XA_CARDINAL, &actual, &format, &n, &left, ( unsigned char** )&data); if (result == Success && data != NULL) { for ( uint32_t i = 0; i < n; i++ ) { vecResult.push_back( data[ i ] ); } XFree((void *) data); return true; } return false; } std::string get_string_prop( xwayland_ctx_t *ctx, Window win, Atom prop ) { XTextProperty tp; if ( !XGetTextProperty( ctx->dpy, win, &tp, prop ) ) return ""; std::string value = reinterpret_cast( tp.value ); XFree( tp.value ); return value; } void set_string_prop( xwayland_ctx_t *ctx, Atom prop, const std::string &value ) { XTextProperty text_property = { .value = ( unsigned char * )value.c_str(), .encoding = ctx->atoms.utf8StringAtom, .format = 8, .nitems = strlen( value.c_str() ) }; XSetTextProperty( ctx->dpy, ctx->root, &text_property, prop); XFlush( ctx->dpy ); } void clear_prop( xwayland_ctx_t *ctx, Atom prop ) { XDeleteProperty( ctx->dpy, ctx->root, prop ); XFlush( ctx->dpy ); } static bool win_has_game_id( steamcompmgr_win_t *w ) { return w->appID != 0; } static bool win_is_useless( steamcompmgr_win_t *w ) { // Windows that are 1x1 are pretty useless for override redirects. // Just ignore them. // Fixes the Xbox Login in Age of Empires 2: DE. return w->GetGeometry().nWidth == 1 && w->GetGeometry().nHeight == 1; } static bool win_is_override_redirect( steamcompmgr_win_t *w ) { if (w->type != steamcompmgr_win_type_t::XWAYLAND) return false; return w->xwayland().a.override_redirect && !w->ignoreOverrideRedirect && !win_is_useless( w ); } static bool win_skip_taskbar_and_pager( steamcompmgr_win_t *w ) { return w->skipTaskbar && w->skipPager; } static bool win_skip_and_not_fullscreen( steamcompmgr_win_t *w ) { return win_skip_taskbar_and_pager( w ) && !w->isFullscreen; } static bool win_maybe_a_dropdown( steamcompmgr_win_t *w ) { if ( w->type != steamcompmgr_win_type_t::XWAYLAND ) return false; // Josh: // Right now we don't get enough info from Wine // about the true nature of windows to distringuish // something like the Fallout 4 Options menu from the // Warframe language dropdown. Until we get more stuff // exposed for that, there is this workaround to let that work. if ( w->appID == 230410 && w->maybe_a_dropdown && w->xwayland().transientFor && ( w->skipPager || w->skipTaskbar ) ) return !win_is_useless( w ); // Work around Antichamber splash screen until we hook up // the Proton window style deduction. if ( w->appID == 219890 ) return false; // The Launcher in Witcher 2 (20920) has a clear window with WS_EX_LAYERED on top of it. // // The Age of Empires 2 Launcher also has a WS_EX_LAYERED window to separate controls // from its backing, which this seems to handle, although we seemingly don't handle // it's transparency yet, which I do not understand. // // Layered windows are windows that are meant to be transparent // with alpha blending + visual fx. // https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features // // TODO: Come back to me for original Age of Empires HD launcher. // Does that use it? It wants blending! // // Only do this if we have CONTROLPARENT right now. Some other apps, such as the // Street Fighter V (310950) Splash Screen also use LAYERED and TOOLWINDOW, and we don't // want that to be overlayed. // Ignore LAYERED if it's marked as top-level with WS_EX_APPWINDOW. // TODO: Find more apps using LAYERED. const uint32_t validLayered = WS_EX_CONTROLPARENT | WS_EX_LAYERED; const uint32_t invalidLayered = WS_EX_APPWINDOW; if ( w->hasHwndStyleEx && ( ( w->hwndStyleEx & validLayered ) == validLayered ) && ( ( w->hwndStyleEx & invalidLayered ) == 0 ) ) return true; // Josh: // The logic here is as follows. The window will be treated as a dropdown if: // // If this window has a fixed position on the screen + static gravity: // - If the window has either skipPage or skipTaskbar // - If the window isn't a dialog, always treat it as a dropdown, as it's // probably meant to be some form of popup. // - If the window is a dialog // - If the window has transient for, disregard it, as it is trying to redirecting us elsewhere // ie. a settings menu dialog popup or something. // - If the window has both skip taskbar and pager, treat it as a dialog. bool valid_maybe_a_dropdown = w->maybe_a_dropdown && ( ( !w->is_dialog || ( !w->xwayland().transientFor && win_skip_and_not_fullscreen( w ) ) ) && ( w->skipPager || w->skipTaskbar ) ); return ( valid_maybe_a_dropdown || win_is_override_redirect( w ) ) && !win_is_useless( w ); } static bool win_is_disabled( steamcompmgr_win_t *w ) { if ( !w->hasHwndStyle ) return false; return !!(w->hwndStyle & WS_DISABLED); } /* Returns true if a's focus priority > b's. * * This function establishes a list of criteria to decide which window should * have focus. The first criteria has higher priority. If the first criteria * is a tie, fallback to the second one, then the third, and so on. * * The general workflow is: * * if ( windows don't have the same criteria value ) * return true if a should be focused; * // This is a tie, fallback to the next criteria */ static bool is_focus_priority_greater( steamcompmgr_win_t *a, steamcompmgr_win_t *b ) { if ( win_has_game_id( a ) != win_has_game_id( b ) ) return win_has_game_id( a ); // We allow using an override redirect window in some cases, but if we have // a choice between two windows we always prefer the non-override redirect // one. if ( win_is_override_redirect( a ) != win_is_override_redirect( b ) ) return !win_is_override_redirect( a ); // If the window is 1x1 then prefer anything else we have. if ( win_is_useless( a ) != win_is_useless( b ) ) return !win_is_useless( a ); if ( win_maybe_a_dropdown( a ) != win_maybe_a_dropdown( b ) ) return !win_maybe_a_dropdown( a ); if ( win_is_disabled( a ) != win_is_disabled( b ) ) return !win_is_disabled( a ); // Wine sets SKIP_TASKBAR and SKIP_PAGER hints for WS_EX_NOACTIVATE windows. // See https://github.com/Plagman/gamescope/issues/87 if ( win_skip_and_not_fullscreen( a ) != win_skip_and_not_fullscreen( b ) ) return !win_skip_and_not_fullscreen( a ); // Prefer normal windows over dialogs // if we are an override redirect/dropdown window. if ( win_maybe_a_dropdown( a ) && win_maybe_a_dropdown( b ) && a->is_dialog != b->is_dialog ) return !a->is_dialog; if (a->type != steamcompmgr_win_type_t::XWAYLAND) { return true; } // Attempt to tie-break dropdowns by transient-for. if ( win_maybe_a_dropdown( a ) && win_maybe_a_dropdown( b ) && !a->xwayland().transientFor != !b->xwayland().transientFor ) return !a->xwayland().transientFor; if ( win_has_game_id( a ) && a->xwayland().map_sequence != b->xwayland().map_sequence ) return a->xwayland().map_sequence > b->xwayland().map_sequence; // The damage sequences are only relevant for game windows. if ( win_has_game_id( a ) && a->xwayland().damage_sequence != b->xwayland().damage_sequence ) return a->xwayland().damage_sequence > b->xwayland().damage_sequence; return false; } static bool is_good_override_candidate( steamcompmgr_win_t *override, steamcompmgr_win_t* focus ) { // Some Chrome/Edge dropdowns (ie. FH5 xbox login) will automatically close themselves if you // focus them while they are meant to be offscreen (-1,-1 and 1x1) so check that the // override's position is on-screen. if ( !focus ) return false; return override != focus && override->GetGeometry().nX >= 0 && override->GetGeometry().nY >= 0; } static bool pick_primary_focus_and_override( focus_t *out, Window focusControlWindow, const std::vector& vecPossibleFocusWindows, bool globalFocus, const std::vector& ctxFocusControlAppIDs, uint64_t ulVirtualFocusKey, gamescope::VirtualConnectorStrategy eStrategy ) { bool localGameFocused = false; steamcompmgr_win_t *focus = NULL, *override_focus = NULL; bool controlledFocus = eStrategy != gamescope::VirtualConnectorStrategies::SingleApplication || focusControlWindow != None || !ctxFocusControlAppIDs.empty(); if ( controlledFocus ) { if ( eStrategy == gamescope::VirtualConnectorStrategies::SteamControlled ) { if ( focusControlWindow != None ) { for ( steamcompmgr_win_t *focusable_window : vecPossibleFocusWindows ) { if ( focusable_window->type != steamcompmgr_win_type_t::XWAYLAND ) continue; if ( focusable_window->xwayland().id == focusControlWindow ) { focus = focusable_window; localGameFocused = true; goto found; } } } for ( auto focusable_appid : ctxFocusControlAppIDs ) { for ( steamcompmgr_win_t *focusable_window : vecPossibleFocusWindows ) { if ( focusable_window->appID == focusable_appid ) { focus = focusable_window; localGameFocused = true; goto found; } } } } else { for ( steamcompmgr_win_t *focusable_window : vecPossibleFocusWindows ) { if ( focusable_window->GetVirtualConnectorKey( eStrategy ) == ulVirtualFocusKey ) { focus = focusable_window; localGameFocused = true; goto found; } } } found:; } if ( !focus && ( !globalFocus || !controlledFocus ) ) { if ( !vecPossibleFocusWindows.empty() ) { focus = vecPossibleFocusWindows[ 0 ]; localGameFocused = focus->appID != 0; } } auto resolveTransientOverrides = [&](bool maybe) { if ( !focus || focus->type != steamcompmgr_win_type_t::XWAYLAND ) return; // Do some searches to find transient links to override redirects too. while ( true ) { bool bFoundTransient = false; for ( steamcompmgr_win_t *candidate : vecPossibleFocusWindows ) { if ( candidate->type != steamcompmgr_win_type_t::XWAYLAND ) continue; bool is_dropdown = maybe ? win_maybe_a_dropdown( candidate ) : win_is_override_redirect( candidate ); if ( ( !override_focus || candidate != override_focus ) && candidate != focus && ( ( !override_focus && candidate->xwayland().transientFor == focus->xwayland().id ) || ( override_focus && candidate->xwayland().transientFor == override_focus->xwayland().id ) ) && is_dropdown) { bFoundTransient = true; override_focus = candidate; break; } } // Hopefully we can't have transient cycles or we'll have to maintain a list of visited windows here if ( bFoundTransient == false ) break; } }; if ( focus && focus->type == steamcompmgr_win_type_t::XWAYLAND ) { if ( !focusControlWindow ) { // Do some searches through game windows to follow transient links if needed while ( true ) { bool bFoundTransient = false; for ( steamcompmgr_win_t *candidate : vecPossibleFocusWindows ) { if ( candidate->type != steamcompmgr_win_type_t::XWAYLAND ) continue; if ( candidate != focus && candidate->xwayland().transientFor == focus->xwayland().id && !win_maybe_a_dropdown( candidate ) ) { bFoundTransient = true; focus = candidate; break; } } // Hopefully we can't have transient cycles or we'll have to maintain a list of visited windows here if ( bFoundTransient == false ) break; } } if ( !override_focus ) { if ( !ctxFocusControlAppIDs.empty() ) { for ( steamcompmgr_win_t *override : vecPossibleFocusWindows ) { if ( win_is_override_redirect(override) && is_good_override_candidate(override, focus) && override->appID == focus->appID ) { override_focus = override; break; } } } else if ( !vecPossibleFocusWindows.empty() ) { for ( steamcompmgr_win_t *override : vecPossibleFocusWindows ) { if ( win_is_override_redirect(override) && is_good_override_candidate(override, focus) ) { override_focus = override; break; } } } resolveTransientOverrides( false ); } } if ( focus ) { if ( window_has_commits( focus ) ) out->focusWindow = focus; else focus->outdatedInteractiveFocus = true; // Always update X's idea of focus, but still dirty // the it being outdated so we can resolve that globally later. // // Only affecting X and not the WL idea of focus here, // we always want to think the window is focused. // but our real presenting focus and input focus can be elsewhere. if ( !globalFocus ) out->focusWindow = focus; } if ( !override_focus && focus ) { if ( controlledFocus ) { for ( auto focusable_appid : ctxFocusControlAppIDs ) { for ( steamcompmgr_win_t *fake_override : vecPossibleFocusWindows ) { if ( fake_override->appID == focusable_appid ) { if ( win_maybe_a_dropdown( fake_override ) && is_good_override_candidate( fake_override, focus ) && fake_override->appID == focus->appID ) { override_focus = fake_override; goto found2; } } } } } else { for ( steamcompmgr_win_t *fake_override : vecPossibleFocusWindows ) { if ( win_maybe_a_dropdown( fake_override ) && is_good_override_candidate( fake_override, focus ) ) { override_focus = fake_override; goto found2; } } } found2:; resolveTransientOverrides( true ); } out->overrideWindow = override_focus; return localGameFocused; } std::vector< steamcompmgr_win_t* > xwayland_ctx_t::GetPossibleFocusWindows() { std::vector vecPossibleFocusWindows; for (steamcompmgr_win_t *w = this->list; w; w = w->xwayland().next) { // Always skip system tray icons and overlays if ( w->isSysTrayIcon || w->isOverlay || w->isExternalOverlay ) { continue; } if ( w->xwayland().a.map_state == IsViewable && w->xwayland().a.c_class == InputOutput && ( win_has_game_id( w ) || window_is_steam( w ) || w->isSteamStreamingClient ) && (w->opacity > TRANSLUCENT || w->isSteamStreamingClient ) ) { vecPossibleFocusWindows.push_back( w ); } } std::stable_sort( vecPossibleFocusWindows.begin(), vecPossibleFocusWindows.end(), is_focus_priority_greater ); return vecPossibleFocusWindows; } static void set_wm_state( xwayland_ctx_t *ctx, Window win, uint32_t state ) { uint32_t wmState[] = { state, None }; XChangeProperty(ctx->dpy, win, ctx->atoms.WMStateAtom, ctx->atoms.WMStateAtom, 32, PropModeReplace, (unsigned char *)wmState, sizeof(wmState) / sizeof(wmState[0])); } void xwayland_ctx_t::DetermineAndApplyFocus( const std::vector< steamcompmgr_win_t* > &vecPossibleFocusWindows ) { xwayland_ctx_t *ctx = this; steamcompmgr_win_t *inputFocus = NULL; steamcompmgr_win_t *prevFocusWindow = ctx->focus.focusWindow; ctx->focus.overlayWindow = nullptr; ctx->focus.notificationWindow = nullptr; ctx->focus.overrideWindow = nullptr; ctx->focus.externalOverlayWindow = nullptr; unsigned int maxOpacity = 0; unsigned int maxOpacityExternal = 0; for (steamcompmgr_win_t *w = ctx->list; w; w = w->xwayland().next) { if (w->isOverlay) { if (w->GetGeometry().nWidth > 1200 && w->opacity >= maxOpacity) { ctx->focus.overlayWindow = w; maxOpacity = w->opacity; } else { ctx->focus.notificationWindow = w; } } if (w->isExternalOverlay) { if (w->opacity > maxOpacityExternal) { ctx->focus.externalOverlayWindow = w; maxOpacityExternal = w->opacity; } } if ( gamescope::VirtualConnectorIsSingleOutput() ) { if ( w->isOverlay && w->inputFocusMode ) { inputFocus = w; } } } gamescope::VirtualConnectorStrategy eStrategy = gamescope::cv_backend_virtual_connector_strategy == gamescope::VirtualConnectorStrategies::SteamControlled ? gamescope::VirtualConnectorStrategies::SteamControlled : gamescope::VirtualConnectorStrategies::PerWindow; pick_primary_focus_and_override( &ctx->focus, ctx->focusControlWindow, vecPossibleFocusWindows, false, vecFocuscontrolAppIDs, 0, eStrategy ); if ( inputFocus == NULL ) { inputFocus = ctx->focus.focusWindow; } if ( !ctx->focus.focusWindow ) { return; } if ( prevFocusWindow != ctx->focus.focusWindow ) { /* Some games (e.g. DOOM Eternal) don't react well to being put back as * iconic, so never do that. Only take them out of iconic. */ set_wm_state( ctx, ctx->focus.focusWindow->xwayland().id, ICCCM_NORMAL_STATE ); gpuvis_trace_printf( "determine_and_apply_focus focus %lu", ctx->focus.focusWindow->xwayland().id ); if ( debugFocus == true ) { xwm_log.debugf( "determine_and_apply_focus focus %lu", ctx->focus.focusWindow->xwayland().id ); char buf[512]; sprintf( buf, "xwininfo -id 0x%lx; xprop -id 0x%lx; xwininfo -root -tree", ctx->focus.focusWindow->xwayland().id, ctx->focus.focusWindow->xwayland().id ); system( buf ); } } steamcompmgr_win_t *keyboardFocusWin = inputFocus; if ( gamescope::VirtualConnectorIsSingleOutput() ) { if ( inputFocus && inputFocus->inputFocusMode == 2 ) keyboardFocusWin = ctx->focus.focusWindow; } Window keyboardFocusWindow = keyboardFocusWin ? keyboardFocusWin->xwayland().id : None; // If the top level parent of our current keyboard window is the same as our target (top level) input focus window // then keep focus on that and don't yank it away to the top level input focus window. // Fixes dropdowns in Steam CEF. if ( keyboardFocusWindow && ctx->currentKeyboardFocusWindow && find_win( ctx, ctx->currentKeyboardFocusWindow ) == keyboardFocusWin ) keyboardFocusWindow = ctx->currentKeyboardFocusWindow; if ( ctx->focus.inputFocusWindow != inputFocus || ctx->focus.inputFocusMode != inputFocus->inputFocusMode || ctx->currentKeyboardFocusWindow != keyboardFocusWindow ) { if ( debugFocus == true ) { xwm_log.debugf( "determine_and_apply_focus inputFocus %lu", inputFocus->xwayland().id ); } if ( !ctx->focus.overrideWindow || ctx->focus.overrideWindow != keyboardFocusWin ) XSetInputFocus(ctx->dpy, keyboardFocusWin->xwayland().id, RevertToNone, CurrentTime); if ( ctx->focus.inputFocusWindow != inputFocus || ctx->focus.inputFocusMode != inputFocus->inputFocusMode ) { // If the window doesn't want focus when hidden, move it away // as we are going to hide it straight after. // otherwise, if we switch from wanting it to not // (steam -> game) // put us back in the centre of the screen. if (window_wants_no_focus_when_mouse_hidden(inputFocus)) ctx->focus.bResetToCorner = true; else if ( window_wants_no_focus_when_mouse_hidden(inputFocus) != window_wants_no_focus_when_mouse_hidden(ctx->focus.inputFocusWindow) ) ctx->focus.bResetToCenter = true; // cursor is likely not interactable anymore in its original context, hide // don't care if we change kb focus window due to that happening when // going from override -> focus and we don't want to hide then as it's probably a dropdown. ctx->cursor->hide(); } ctx->focus.inputFocusWindow = inputFocus; ctx->focus.inputFocusMode = inputFocus->inputFocusMode; ctx->currentKeyboardFocusWindow = keyboardFocusWindow; } steamcompmgr_win_t *w; w = ctx->focus.focusWindow; if ( inputFocus == ctx->focus.focusWindow && ctx->focus.overrideWindow ) { if ( ctx->list[0].xwayland().id != ctx->focus.overrideWindow->xwayland().id ) { XRaiseWindow(ctx->dpy, ctx->focus.overrideWindow->xwayland().id); } } else { if ( ctx->list[0].xwayland().id != inputFocus->xwayland().id ) { XRaiseWindow(ctx->dpy, inputFocus->xwayland().id); } } if (!ctx->focus.focusWindow->nudged) { XMoveWindow(ctx->dpy, ctx->focus.focusWindow->xwayland().id, 1, 1); ctx->focus.focusWindow->nudged = true; } if (w->GetGeometry().nX != 0 || w->GetGeometry().nY != 0) XMoveWindow(ctx->dpy, ctx->focus.focusWindow->xwayland().id, 0, 0); if ( window_is_fullscreen( ctx->focus.focusWindow ) || ctx->force_windows_fullscreen ) { bool bIsSteam = window_is_steam( ctx->focus.focusWindow ); int fs_width = ctx->root_width; int fs_height = ctx->root_height; if ( bIsSteam && g_nSteamMaxHeight && ctx->root_height > g_nSteamMaxHeight ) { float steam_height_scale = g_nSteamMaxHeight / (float)ctx->root_height; fs_height = g_nSteamMaxHeight; fs_width = ctx->root_width * steam_height_scale; } if ( w->GetGeometry().nWidth != fs_width || w->GetGeometry().nHeight != fs_height || globalScaleRatio != 1.0f ) XResizeWindow(ctx->dpy, ctx->focus.focusWindow->xwayland().id, fs_width, fs_height); } else { if (ctx->focus.focusWindow->sizeHintsSpecified && ((unsigned)ctx->focus.focusWindow->GetGeometry().nWidth != ctx->focus.focusWindow->requestedWidth || (unsigned)ctx->focus.focusWindow->GetGeometry().nHeight != ctx->focus.focusWindow->requestedHeight)) { XResizeWindow(ctx->dpy, ctx->focus.focusWindow->xwayland().id, ctx->focus.focusWindow->requestedWidth, ctx->focus.focusWindow->requestedHeight); } } Window root_return = None, parent_return = None; Window *children = NULL; unsigned int nchildren = 0; unsigned int i = 0; XQueryTree(ctx->dpy, w->xwayland().id, &root_return, &parent_return, &children, &nchildren); while (i < nchildren) { XSelectInput( ctx->dpy, children[i], FocusChangeMask ); i++; } XFree(children); ctx->focus.ulCurrentFocusSerial = GetFocusSerial(); } wlr_surface *win_surface(steamcompmgr_win_t *window) { if (!window) return nullptr; return window->main_surface(); } const char *get_win_display_name(steamcompmgr_win_t *window) { if ( window->type == steamcompmgr_win_type_t::XWAYLAND ) return window->xwayland().ctx->xwayland_server->get_nested_display_name(); else if ( window->type == steamcompmgr_win_type_t::XDG ) return wlserver_get_wl_display_name(); else return ""; } static std::vector< steamcompmgr_win_t* > steamcompmgr_xdg_get_possible_focus_windows() { std::vector< steamcompmgr_win_t* > windows; for ( auto &win : g_steamcompmgr_xdg_wins ) { // Always skip system tray icons and overlays if ( win->isSysTrayIcon || win->isOverlay || win->isExternalOverlay ) { continue; } windows.emplace_back( win.get() ); } return windows; } static std::vector< steamcompmgr_win_t* > GetGlobalPossibleFocusWindows() { std::vector< steamcompmgr_win_t* > vecPossibleFocusWindows; { gamescope_xwayland_server_t *server = NULL; for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++) { std::vector< steamcompmgr_win_t* > vecLocalPossibleFocusWindows = server->ctx->GetPossibleFocusWindows(); vecPossibleFocusWindows.insert( vecPossibleFocusWindows.end(), vecLocalPossibleFocusWindows.begin(), vecLocalPossibleFocusWindows.end() ); } } { std::vector< steamcompmgr_win_t* > vecLocalPossibleFocusWindows = steamcompmgr_xdg_get_possible_focus_windows(); vecPossibleFocusWindows.insert( vecPossibleFocusWindows.end(), vecLocalPossibleFocusWindows.begin(), vecLocalPossibleFocusWindows.end() ); } // Determine global primary focus std::stable_sort( vecPossibleFocusWindows.begin(), vecPossibleFocusWindows.end(), is_focus_priority_greater ); return vecPossibleFocusWindows; } static void steamcompmgr_xdg_determine_and_apply_focus( const std::vector< steamcompmgr_win_t* > &vecPossibleFocusWindows ) { for ( auto &window : g_steamcompmgr_xdg_wins ) { if (window->isOverlay) g_steamcompmgr_xdg_focus.overlayWindow = window.get(); if (window->isExternalOverlay) g_steamcompmgr_xdg_focus.externalOverlayWindow = window.get(); } gamescope::VirtualConnectorStrategy eStrategy = gamescope::cv_backend_virtual_connector_strategy == gamescope::VirtualConnectorStrategies::SteamControlled ? gamescope::VirtualConnectorStrategies::SteamControlled : gamescope::VirtualConnectorStrategies::PerWindow; pick_primary_focus_and_override( &g_steamcompmgr_xdg_focus, None, vecPossibleFocusWindows, false, vecFocuscontrolAppIDs, 0, eStrategy ); } uint32_t g_focusedBaseAppId = 0; static void determine_and_apply_focus( global_focus_t *pFocus ) { gamescope_xwayland_server_t *root_server = wlserver_get_xwayland_server(0); xwayland_ctx_t *root_ctx = root_server->ctx.get(); global_focus_t previousLocalFocus = *pFocus; *pFocus = global_focus_t{}; pFocus->focusWindow = previousLocalFocus.focusWindow; pFocus->cursor = root_ctx->cursor.get(); pFocus->ulVirtualFocusKey = previousLocalFocus.ulVirtualFocusKey; pFocus->pVirtualConnector = previousLocalFocus.pVirtualConnector; gameFocused = false; std::vector< unsigned long > focusable_appids; std::vector< unsigned long > focusable_windows; // Apply focus to the XWayland contexts. { gamescope_xwayland_server_t *server = NULL; for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++) { std::vector< steamcompmgr_win_t* > vecLocalPossibleFocusWindows = server->ctx->GetPossibleFocusWindows(); if ( server->ctx->focus.IsDirty() ) server->ctx->DetermineAndApplyFocus( vecLocalPossibleFocusWindows ); } } // Apply focus to XDG contexts (TODO merge me with some nice abstraction of "environments") { std::vector< steamcompmgr_win_t* > vecLocalPossibleFocusWindows = steamcompmgr_xdg_get_possible_focus_windows(); if ( g_steamcompmgr_xdg_focus.IsDirty() ) steamcompmgr_xdg_determine_and_apply_focus( vecLocalPossibleFocusWindows ); } // Determine local context focuses std::vector vecPossibleFocusWindows = GetGlobalPossibleFocusWindows(); for ( steamcompmgr_win_t *focusable_window : vecPossibleFocusWindows ) { if ( focusable_window->type != steamcompmgr_win_type_t::XWAYLAND ) continue; // Exclude windows that are useless (1x1), skip taskbar + pager or override redirect windows // from the reported focusable windows to Steam. if ( win_is_useless( focusable_window ) || win_skip_and_not_fullscreen( focusable_window ) || focusable_window->xwayland().a.override_redirect ) continue; unsigned int unAppID = focusable_window->appID; if ( unAppID != 0 ) { unsigned long j; for( j = 0; j < focusable_appids.size(); j++ ) { if ( focusable_appids[ j ] == unAppID ) { break; } } if ( j == focusable_appids.size() ) { focusable_appids.push_back( unAppID ); } } // list of [window, appid, pid] triplets focusable_windows.push_back( focusable_window->xwayland().id ); focusable_windows.push_back( focusable_window->appID ); focusable_windows.push_back( focusable_window->pid ); } XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeFocusableAppsAtom, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)focusable_appids.data(), focusable_appids.size() ); XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeFocusableWindowsAtom, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)focusable_windows.data(), focusable_windows.size() ); gameFocused = pick_primary_focus_and_override( pFocus, root_ctx->focusControlWindow, vecPossibleFocusWindows, true, vecFocuscontrolAppIDs, pFocus->ulVirtualFocusKey, gamescope::cv_backend_virtual_connector_strategy ); // Pick overlay/notifications from root ctx pFocus->overlayWindow = root_ctx->focus.overlayWindow; pFocus->externalOverlayWindow = root_ctx->focus.externalOverlayWindow; pFocus->notificationWindow = root_ctx->focus.notificationWindow; if ( !pFocus->overlayWindow ) { pFocus->overlayWindow = g_steamcompmgr_xdg_focus.overlayWindow; } if ( !pFocus->externalOverlayWindow ) { pFocus->externalOverlayWindow = g_steamcompmgr_xdg_focus.externalOverlayWindow; } bool bUseOverlay = gamescope::VirtualConnectorIsSingleOutput() || gamescope::VirtualConnectorKeyIsSteam( pFocus->ulVirtualFocusKey ); if ( !bUseOverlay ) { pFocus->overlayWindow = nullptr; pFocus->notificationWindow = nullptr; } // Pick inputFocusWindow if ( gamescope::VirtualConnectorIsSingleOutput() && pFocus->overlayWindow && pFocus->overlayWindow->inputFocusMode ) { pFocus->inputFocusWindow = pFocus->overlayWindow; pFocus->keyboardFocusWindow = pFocus->overlayWindow; } else { pFocus->inputFocusWindow = pFocus->focusWindow; pFocus->keyboardFocusWindow = pFocus->overrideWindow ? pFocus->overrideWindow : pFocus->focusWindow; } // Pick cursor from our input focus window // Initially pick cursor from the ctx of our input focus. if (pFocus->inputFocusWindow) { if (pFocus->inputFocusWindow->type == steamcompmgr_win_type_t::XWAYLAND) pFocus->cursor = pFocus->inputFocusWindow->xwayland().ctx->cursor.get(); else { // TODO XDG: // Implement cursor support. // Probably want some form of abstraction here for // wl cursor vs x11 cursor given we have virtual cursors. // wlserver should update wl cursor pos xy directly. static bool s_once = false; if (!s_once) { xwm_log.errorf("NO CURSOR IMPL XDG"); s_once = true; } } } if (pFocus->inputFocusWindow) pFocus->inputFocusMode = pFocus->inputFocusWindow->inputFocusMode; if ( gamescope::VirtualConnectorIsSingleOutput() && pFocus->inputFocusMode == 2 ) { pFocus->keyboardFocusWindow = pFocus->overrideWindow ? pFocus->overrideWindow : pFocus->focusWindow; } // TODO(strategy): multi-seat on Wayland side if ( pFocus == GetCurrentFocus() ) { static gamescope::VirtualConnectorKey_t s_ulPreviousGlobalFocusKey; // Tell wlserver about our keyboard/mouse focus. if ( pFocus->inputFocusWindow != previousLocalFocus.inputFocusWindow || pFocus->keyboardFocusWindow != previousLocalFocus.keyboardFocusWindow || pFocus->overrideWindow != previousLocalFocus.overrideWindow || pFocus->ulVirtualFocusKey != s_ulPreviousGlobalFocusKey ) { if ( win_surface(pFocus->inputFocusWindow) != nullptr || win_surface(pFocus->keyboardFocusWindow) != nullptr ) { wlserver_lock(); wlserver_clear_dropdowns(); if ( win_surface( pFocus->overrideWindow ) != nullptr ) wlserver_notify_dropdown( pFocus->overrideWindow->main_surface(), pFocus->overrideWindow->xwayland().a.x, pFocus->overrideWindow->xwayland().a.y ); if ( win_surface(pFocus->inputFocusWindow) != nullptr && pFocus->cursor ) wlserver_mousefocus( pFocus->inputFocusWindow->main_surface(), pFocus->cursor->x(), pFocus->cursor->y() ); if ( win_surface(pFocus->keyboardFocusWindow) != nullptr ) wlserver_keyboardfocus( pFocus->keyboardFocusWindow->main_surface() ); wlserver_unlock(); } // Hide cursor on transitioning between xwaylands // We already do this when transitioning input focus inside of an // xwayland ctx. // don't care if we change kb focus window due to that happening when // going from override -> focus and we don't want to hide then as it's probably a dropdown. if ( pFocus->cursor && pFocus->inputFocusWindow != previousLocalFocus.inputFocusWindow ) pFocus->cursor->hide(); } if ( pFocus->inputFocusWindow ) { // Cannot simply XWarpPointer here as we immediately go on to // do wlserver_mousefocus and need to update m_x and m_y of the cursor. if ( pFocus->inputFocusWindow->GetFocus()->bResetToCorner ) { wlserver_lock(); wlserver_mousewarp( pFocus->inputFocusWindow->GetGeometry().nWidth / 2, pFocus->inputFocusWindow->GetGeometry().nHeight / 2, 0, true ); wlserver_fake_mouse_pos( pFocus->inputFocusWindow->GetGeometry().nWidth - 1, pFocus->inputFocusWindow->GetGeometry().nHeight - 1 ); wlserver_unlock(); } else if ( pFocus->inputFocusWindow->GetFocus()->bResetToCenter ) { wlserver_lock(); wlserver_mousewarp( pFocus->inputFocusWindow->GetGeometry().nWidth / 2, pFocus->inputFocusWindow->GetGeometry().nHeight / 2, 0, true ); wlserver_unlock(); } pFocus->inputFocusWindow->GetFocus()->bResetToCorner = false; pFocus->inputFocusWindow->GetFocus()->bResetToCenter = false; } s_ulPreviousGlobalFocusKey = pFocus->ulVirtualFocusKey; } #if HAVE_LIBSYSTEMD pid_t newFocusedWindowPID = pFocus->focusWindow ? pFocus->focusWindow->pid : 0; if (g_dbus && focusWindow_pid != newFocusedWindowPID) { const char *unfocusedWindowUnit = unit_from_pid(focusWindow_pid); const char *focusedWindowUnit = unit_from_pid(newFocusedWindowPID); bool sameUnit = unfocusedWindowUnit && focusedWindowUnit && !strcmp(unfocusedWindowUnit, focusedWindowUnit); if (unfocusedWindowUnit && !sameUnit) set_memory_low(unfocusedWindowUnit, false); if (focusedWindowUnit && !sameUnit) set_memory_low(focusedWindowUnit, true); } #endif // Backchannel to Steam unsigned long focusedWindow = 0; unsigned long focusedAppId = 0; unsigned long focusedBaseAppId = 0; const char *focused_display = root_ctx->xwayland_server->get_nested_display_name(); const char *focused_keyboard_display = root_ctx->xwayland_server->get_nested_display_name(); const char *focused_mouse_display = root_ctx->xwayland_server->get_nested_display_name(); if ( pFocus->focusWindow ) { focusedWindow = (unsigned long)pFocus->focusWindow->id(); focusedBaseAppId = pFocus->focusWindow->appID; focusedAppId = pFocus->inputFocusWindow->appID; focused_display = get_win_display_name(pFocus->focusWindow); focusWindow_pid = pFocus->focusWindow->pid; } g_focusedBaseAppId = (uint32_t)focusedAppId; if ( pFocus->inputFocusWindow ) { focused_mouse_display = get_win_display_name(pFocus->inputFocusWindow); } if ( pFocus->keyboardFocusWindow ) { focused_keyboard_display = get_win_display_name(pFocus->keyboardFocusWindow); } if ( pFocus == GetCurrentFocus() ) { if ( steamMode ) { XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeFocusedAppAtom, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&focusedAppId, focusedAppId != 0 ? 1 : 0 ); XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeFocusedAppGfxAtom, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&focusedBaseAppId, focusedBaseAppId != 0 ? 1 : 0 ); } XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeFocusedWindowAtom, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&focusedWindow, focusedWindow != 0 ? 1 : 0 ); XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeFocusDisplay, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)focused_display, strlen(focused_display) + 1 ); XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeMouseFocusDisplay, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)focused_mouse_display, strlen(focused_mouse_display) + 1 ); XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeKeyboardFocusDisplay, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)focused_keyboard_display, strlen(focused_keyboard_display) + 1 ); XFlush( root_ctx->dpy ); } // Sort out fading. if (pFocus->focusWindow && previousLocalFocus.focusWindow != pFocus->focusWindow) { if ( g_FadeOutDuration != 0 && !g_bFirstFrame ) { if ( g_HeldCommits[ HELD_COMMIT_FADE ] == nullptr ) { pFocus->fadeWindow = previousLocalFocus.focusWindow; g_HeldCommits[ HELD_COMMIT_FADE ] = g_HeldCommits[ HELD_COMMIT_BASE ]; g_bPendingFade = true; } else { // If we end up fading back to what we were going to fade to, cancel the fade. if ( pFocus->fadeWindow != nullptr && pFocus->focusWindow == pFocus->fadeWindow ) { g_HeldCommits[ HELD_COMMIT_FADE ] = nullptr; g_bPendingFade = false; fadeOutStartTime = 0; pFocus->fadeWindow = nullptr; } } } } if ( !cv_paint_debug_pause_base_plane ) { // Update last focus commit if ( pFocus->focusWindow && previousLocalFocus.focusWindow != pFocus->focusWindow && !pFocus->focusWindow->isSteamStreamingClient ) { get_window_last_done_commit( pFocus->focusWindow, g_HeldCommits[ HELD_COMMIT_BASE ] ); } } // Set SDL window title if ( pFocus->GetNestedHints() ) { if ( pFocus->focusWindow ) { pFocus->GetNestedHints()->SetVisible( true ); if ( previousLocalFocus.focusWindow != pFocus->focusWindow ) { pFocus->GetNestedHints()->SetTitle( pFocus->focusWindow->title ); pFocus->GetNestedHints()->SetIcon( pFocus->focusWindow->icon ); } } else { pFocus->GetNestedHints()->SetVisible( false ); } } // Some games such as Disgaea PC (405900) don't take controller input until // the window is first clicked on despite it having focus. if ( pFocus->inputFocusWindow && pFocus->inputFocusWindow->appID == 405900 ) { auto now = get_time_in_milliseconds(); wlserver_lock(); wlserver_touchdown( 0.5, 0.5, 0, now ); wlserver_touchup( 0, now + 1 ); wlserver_mousehide(); wlserver_unlock(); } pFocus->ulCurrentFocusSerial = GetFocusSerial(); } static void get_win_type(xwayland_ctx_t *ctx, steamcompmgr_win_t *w) { w->is_dialog = !!w->xwayland().transientFor; std::vector atoms; if ( get_prop( ctx, w->xwayland().id, ctx->atoms.winTypeAtom, atoms ) ) { for ( unsigned int atom : atoms ) { if ( atom == ctx->atoms.winDialogAtom ) { w->is_dialog = true; } if ( atom == ctx->atoms.winNormalAtom ) { w->is_dialog = false; } } } } static void get_size_hints(xwayland_ctx_t *ctx, steamcompmgr_win_t *w) { XSizeHints hints; long hintsSpecified = 0; XGetWMNormalHints(ctx->dpy, w->xwayland().id, &hints, &hintsSpecified); const bool bHasPositionAndGravityHints = ( hintsSpecified & ( PPosition | PWinGravity ) ) == ( PPosition | PWinGravity ); if ( bHasPositionAndGravityHints && hints.x && hints.y && hints.win_gravity == StaticGravity ) { w->maybe_a_dropdown = true; } else { w->maybe_a_dropdown = false; } if (hintsSpecified & (PMaxSize | PMinSize) && hints.max_width && hints.max_height && hints.min_width && hints.min_height && hints.max_width == hints.min_width && hints.min_height == hints.max_height) { w->requestedWidth = hints.max_width; w->requestedHeight = hints.max_height; w->sizeHintsSpecified = true; } else { w->sizeHintsSpecified = false; // Below block checks for a pattern that matches old SDL fullscreen applications; // SDL creates a fullscreen overrride-redirect window and reparents the game // window under it, centered. We get rid of the modeswitch and also want that // black border gone. if (w->xwayland().a.override_redirect) { Window root_return = None, parent_return = None; Window *children = NULL; unsigned int nchildren = 0; XQueryTree(ctx->dpy, w->xwayland().id, &root_return, &parent_return, &children, &nchildren); if (nchildren == 1) { XWindowAttributes attribs; XGetWindowAttributes(ctx->dpy, children[0], &attribs); // If we have a unique children that isn't override-reidrect that is // contained inside this fullscreen window, it's probably it. if (attribs.override_redirect == false && attribs.width <= w->GetGeometry().nWidth && attribs.height <= w->GetGeometry().nHeight) { w->sizeHintsSpecified = true; w->requestedWidth = attribs.width; w->requestedHeight = attribs.height; XMoveWindow(ctx->dpy, children[0], 0, 0); w->ignoreOverrideRedirect = true; } } XFree(children); } } } static void get_win_title(xwayland_ctx_t *ctx, steamcompmgr_win_t *w, Atom atom) { assert(atom == XA_WM_NAME || atom == ctx->atoms.netWMNameAtom); // Allocates a title we are meant to free, // let's re-use this allocation for w->title :) XTextProperty tp; XGetTextProperty( ctx->dpy, w->xwayland().id, &tp, atom ); bool is_utf8; if (tp.encoding == ctx->atoms.utf8StringAtom) { is_utf8 = true; } else if (tp.encoding == XA_STRING) { is_utf8 = false; } else { return; } if (!is_utf8 && w->utf8_title) { /* Clients usually set both the non-UTF8 title and the UTF8 title * properties. If the client has set the UTF8 title prop, ignore the * non-UTF8 one. */ return; } if (tp.nitems > 0) { // Ride off the allocation from XGetTextProperty. w->title = std::make_shared((const char *)tp.value); } else { w->title = NULL; } w->utf8_title = is_utf8; } static void get_net_wm_state(xwayland_ctx_t *ctx, steamcompmgr_win_t *w) { Atom type; int format; unsigned long nitems; unsigned long bytesAfter; unsigned char *data; if (XGetWindowProperty(ctx->dpy, w->xwayland().id, ctx->atoms.netWMStateAtom, 0, 2048, false, AnyPropertyType, &type, &format, &nitems, &bytesAfter, &data) != Success) { return; } Atom *props = (Atom *)data; for (size_t i = 0; i < nitems; i++) { if (props[i] == ctx->atoms.netWMStateFullscreenAtom) { w->isFullscreen = true; } else if (props[i] == ctx->atoms.netWMStateSkipTaskbarAtom) { w->skipTaskbar = true; } else if (props[i] == ctx->atoms.netWMStateSkipPagerAtom) { w->skipPager = true; } else { xwm_log.debugf("Unhandled initial NET_WM_STATE property: %s", XGetAtomName(ctx->dpy, props[i])); } } XFree(data); } static void get_win_icon(xwayland_ctx_t* ctx, steamcompmgr_win_t* w) { w->icon = std::make_shared>(); get_prop(ctx, w->xwayland().id, ctx->atoms.netWMIcon, *w->icon.get()); } static void map_win(xwayland_ctx_t* ctx, Window id, unsigned long sequence) { steamcompmgr_win_t *w = find_win(ctx, id); if (!w) return; w->xwayland().a.map_state = IsViewable; /* This needs to be here or else we lose transparency messages */ XSelectInput(ctx->dpy, id, PropertyChangeMask | SubstructureNotifyMask | LeaveWindowMask | FocusChangeMask); XFlush(ctx->dpy); /* This needs to be here since we don't get PropertyNotify when unmapped */ w->opacity = get_prop(ctx, w->xwayland().id, ctx->atoms.opacityAtom, OPAQUE); w->isSteamLegacyBigPicture = get_prop(ctx, w->xwayland().id, ctx->atoms.steamAtom, 0); /* First try to read the UTF8 title prop, then fallback to the non-UTF8 one */ get_win_title( ctx, w, ctx->atoms.netWMNameAtom ); get_win_title( ctx, w, XA_WM_NAME ); get_win_icon( ctx, w ); w->inputFocusMode = get_prop(ctx, w->xwayland().id, ctx->atoms.steamInputFocusAtom, 0); w->isSteamStreamingClient = get_prop(ctx, w->xwayland().id, ctx->atoms.steamStreamingClientAtom, 0); w->isSteamStreamingClientVideo = get_prop(ctx, w->xwayland().id, ctx->atoms.steamStreamingClientVideoAtom, 0); if ( steamMode == true ) { uint32_t appID = get_prop(ctx, w->xwayland().id, ctx->atoms.gameAtom, 0); if ( w->appID != 0 && appID != 0 && w->appID != appID ) { xwm_log.errorf( "appid clash was %u now %u", w->appID, appID ); } // Let the appID property be authoritative for now if ( appID != 0 ) { w->appID = appID; } } else { w->appID = w->xwayland().id; } w->isOverlay = get_prop(ctx, w->xwayland().id, ctx->atoms.overlayAtom, 0); w->isExternalOverlay = get_prop(ctx, w->xwayland().id, ctx->atoms.externalOverlayAtom, 0); // misyl: Disable appID for overlay types, as parts of the code don't expect that focus-wise. // Fixes mangoapp usage when nested, and not in SteamOS. if ( w->isExternalOverlay ) w->appID = 0; get_size_hints(ctx, w); get_net_wm_state(ctx, w); XWMHints *wmHints = XGetWMHints( ctx->dpy, w->xwayland().id ); if ( wmHints != nullptr ) { if ( wmHints->flags & (InputHint | StateHint ) && wmHints->input == true && wmHints->initial_state == NormalState ) { XRaiseWindow( ctx->dpy, w->xwayland().id ); } XFree( wmHints ); } Window transientFor = None; if ( XGetTransientForHint( ctx->dpy, w->xwayland().id, &transientFor ) ) { w->xwayland().transientFor = transientFor; } else { w->xwayland().transientFor = None; } get_win_type( ctx, w ); w->xwayland().damage_sequence = 0; w->xwayland().map_sequence = sequence; if ( w == ctx->focus.inputFocusWindow || w->xwayland().id == ctx->currentKeyboardFocusWindow ) { XSetInputFocus(ctx->dpy, w->xwayland().id, RevertToNone, CurrentTime); } MakeFocusDirty(); set_wm_state( ctx, w->xwayland().id, ICCCM_NORMAL_STATE ); } static void finish_unmap_win(xwayland_ctx_t *ctx, steamcompmgr_win_t *w) { // TODO clear done commits here? /* don't care about properties anymore */ XSelectInput(ctx->dpy, w->xwayland().id, 0); ctx->clipChanged = true; } static void unmap_win(xwayland_ctx_t *ctx, Window id, bool fade) { steamcompmgr_win_t *w = find_win(ctx, id); if (!w) return; w->xwayland().a.map_state = IsUnmapped; MakeFocusDirty(); finish_unmap_win(ctx, w); set_wm_state( ctx, w->xwayland().id, ICCCM_WITHDRAWN_STATE ); } uint32_t get_appid_from_pid( pid_t pid ) { uint32_t unFoundAppId = 0; char filename[256]; pid_t next_pid = pid; while ( 1 ) { snprintf( filename, sizeof( filename ), "/proc/%i/stat", next_pid ); std::ifstream proc_stat_file( filename ); if (!proc_stat_file.is_open() || proc_stat_file.bad()) break; std::string proc_stat; std::getline( proc_stat_file, proc_stat ); char *procName = nullptr; char *lastParens = nullptr; for ( uint32_t i = 0; i < proc_stat.length(); i++ ) { if ( procName == nullptr && proc_stat[ i ] == '(' ) { procName = &proc_stat[ i + 1 ]; } if ( proc_stat[ i ] == ')' ) { lastParens = &proc_stat[ i ]; } } if (!lastParens) break; *lastParens = '\0'; char state; int parent_pid = -1; sscanf( lastParens + 1, " %c %d", &state, &parent_pid ); if ( strcmp( "reaper", procName ) == 0 ) { snprintf( filename, sizeof( filename ), "/proc/%i/cmdline", next_pid ); std::ifstream proc_cmdline_file( filename ); std::string proc_cmdline; bool bSteamLaunch = false; uint32_t unAppId = 0; std::getline( proc_cmdline_file, proc_cmdline ); for ( uint32_t j = 0; j < proc_cmdline.length(); j++ ) { if ( proc_cmdline[ j ] == '\0' && j + 1 < proc_cmdline.length() ) { if ( strcmp( "SteamLaunch", &proc_cmdline[ j + 1 ] ) == 0 ) { bSteamLaunch = true; } else if ( sscanf( &proc_cmdline[ j + 1 ], "AppId=%u", &unAppId ) == 1 && unAppId != 0 ) { if ( bSteamLaunch == true ) { unFoundAppId = unAppId; } } else if ( strcmp( "--", &proc_cmdline[ j + 1 ] ) == 0 ) { break; } } } } if ( parent_pid == -1 || parent_pid == 0 ) { break; } else { next_pid = parent_pid; } } return unFoundAppId; } static pid_t get_win_pid(xwayland_ctx_t *ctx, Window id) { XResClientIdSpec client_spec = { .client = id, .mask = XRES_CLIENT_ID_PID_MASK, }; long num_ids = 0; XResClientIdValue *client_ids = NULL; XResQueryClientIds(ctx->dpy, 1, &client_spec, &num_ids, &client_ids); pid_t pid = -1; for (long i = 0; i < num_ids; i++) { pid = XResGetClientPid(&client_ids[i]); if (pid > 0) break; } XResClientIdsDestroy(num_ids, client_ids); if (pid <= 0) xwm_log.errorf("Failed to find PID for window 0x%lx", id); return pid; } static void add_win(xwayland_ctx_t *ctx, Window id, Window prev, unsigned long sequence) { steamcompmgr_win_t *new_win = new steamcompmgr_win_t{}; steamcompmgr_win_t **p; if (!new_win) return; new_win->seq = ++g_lastWinSeq; new_win->type = steamcompmgr_win_type_t::XWAYLAND; new_win->_window_types.emplace(); if (prev) { for (p = &ctx->list; *p; p = &(*p)->xwayland().next) if ((*p)->xwayland().id == prev) break; } else p = &ctx->list; new_win->xwayland().id = id; if (!XGetWindowAttributes(ctx->dpy, id, &new_win->xwayland().a)) { delete new_win; return; } new_win->xwayland().ctx = ctx; new_win->xwayland().damage_sequence = 0; new_win->xwayland().map_sequence = 0; if (new_win->xwayland().a.c_class == InputOnly) new_win->xwayland().damage = None; else { new_win->xwayland().damage = XDamageCreate(ctx->dpy, id, XDamageReportRawRectangles); } new_win->opacity = OPAQUE; if ( useXRes == true ) { new_win->pid = get_win_pid(ctx, id); } else { new_win->pid = -1; } new_win->isOverlay = false; new_win->isExternalOverlay = false; new_win->isSteamLegacyBigPicture = false; new_win->isSteamStreamingClient = false; new_win->isSteamStreamingClientVideo = false; new_win->inputFocusMode = 0; new_win->is_dialog = false; new_win->maybe_a_dropdown = false; new_win->hasHwndStyle = false; new_win->hwndStyle = 0; new_win->hasHwndStyleEx = false; new_win->hwndStyleEx = 0; if ( steamMode == true ) { if ( new_win->pid != -1 ) { new_win->appID = get_appid_from_pid( new_win->pid ); } else { new_win->appID = 0; } } else { new_win->appID = id; } if ( new_win->isExternalOverlay ) new_win->appID = 0; Window transientFor = None; if ( XGetTransientForHint( ctx->dpy, id, &transientFor ) ) { new_win->xwayland().transientFor = transientFor; } else { new_win->xwayland().transientFor = None; } get_win_type( ctx, new_win ); new_win->title = NULL; new_win->utf8_title = false; new_win->isFullscreen = false; new_win->isSysTrayIcon = false; new_win->sizeHintsSpecified = false; new_win->skipTaskbar = false; new_win->skipPager = false; new_win->requestedWidth = 0; new_win->requestedHeight = 0; new_win->nudged = false; new_win->ignoreOverrideRedirect = false; wlserver_x11_surface_info_init( &new_win->xwayland().surface, ctx->xwayland_server, id ); { std::unique_lock lock( ctx->list_mutex ); new_win->xwayland().next = *p; *p = new_win; } if (new_win->xwayland().a.map_state == IsViewable) map_win(ctx, id, sequence); MakeFocusDirty(); } static void restack_win(xwayland_ctx_t *ctx, steamcompmgr_win_t *w, Window new_above) { Window old_above; if (w->xwayland().next) old_above = w->xwayland().next->xwayland().id; else old_above = None; if (old_above != new_above) { std::unique_lock lock( ctx->list_mutex ); steamcompmgr_win_t **prev; /* unhook */ for (prev = &ctx->list; *prev; prev = &(*prev)->xwayland().next) { if ((*prev) == w) break; } *prev = w->xwayland().next; /* rehook */ for (prev = &ctx->list; *prev; prev = &(*prev)->xwayland().next) { if ((*prev)->xwayland().id == new_above) break; } w->xwayland().next = *prev; *prev = w; MakeFocusDirty(); } } static void configure_win(xwayland_ctx_t *ctx, XConfigureEvent *ce) { steamcompmgr_win_t *w = find_win(ctx, ce->window); if (!w || w->xwayland().id != ce->window) { if (ce->window == ctx->root) { ctx->root_width = ce->width; ctx->root_height = ce->height; MakeFocusDirty(); gamescope_xwayland_server_t *root_server = wlserver_get_xwayland_server(0); xwayland_ctx_t *root_ctx = root_server->ctx.get(); XDeleteProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeXWaylandModeControl ); XFlush( root_ctx->dpy ); } return; } w->xwayland().a.x = ce->x; w->xwayland().a.y = ce->y; w->xwayland().a.width = ce->width; w->xwayland().a.height = ce->height; w->xwayland().a.border_width = ce->border_width; w->xwayland().a.override_redirect = ce->override_redirect; restack_win(ctx, w, ce->above); MakeFocusDirty(); } static void circulate_win(xwayland_ctx_t *ctx, XCirculateEvent *ce) { steamcompmgr_win_t *w = find_win(ctx, ce->window); Window new_above; if (!w || w->xwayland().id != ce->window) return; if (ce->place == PlaceOnTop) new_above = ctx->list->xwayland().id; else new_above = None; restack_win(ctx, w, new_above); ctx->clipChanged = true; } static void map_request(xwayland_ctx_t *ctx, XMapRequestEvent *mapRequest) { XMapWindow( ctx->dpy, mapRequest->window ); } static void configure_request(xwayland_ctx_t *ctx, XConfigureRequestEvent *configureRequest) { XWindowChanges changes = { .x = configureRequest->x, .y = configureRequest->y, .width = configureRequest->width, .height = configureRequest->height, .border_width = configureRequest->border_width, .sibling = configureRequest->above, .stack_mode = configureRequest->detail }; XConfigureWindow( ctx->dpy, configureRequest->window, configureRequest->value_mask, &changes ); } static void circulate_request( xwayland_ctx_t *ctx, XCirculateRequestEvent *circulateRequest ) { XCirculateSubwindows( ctx->dpy, circulateRequest->window, circulateRequest->place ); } static void finish_destroy_win(xwayland_ctx_t *ctx, Window id, bool gone) { steamcompmgr_win_t **prev, *w; for (prev = &ctx->list; (w = *prev); prev = &w->xwayland().next) if (w->xwayland().id == id) { if (gone) finish_unmap_win (ctx, w); { std::unique_lock lock( ctx->list_mutex ); *prev = w->xwayland().next; } if (w->xwayland().damage != None) { XDamageDestroy(ctx->dpy, w->xwayland().damage); w->xwayland().damage = None; } if (gone) { // release all commits now we are closed. w->commit_queue.clear(); } wlserver_lock(); wlserver_x11_surface_info_finish( &w->xwayland().surface ); wlserver_unlock(); delete w; break; } } static void destroy_win(xwayland_ctx_t *ctx, Window id, bool gone, bool fade) { // Context if (x11_win(ctx->focus.focusWindow) == id && gone) ctx->focus.focusWindow = nullptr; if (x11_win(ctx->focus.inputFocusWindow) == id && gone) ctx->focus.inputFocusWindow = nullptr; if (x11_win(ctx->focus.overlayWindow) == id && gone) ctx->focus.overlayWindow = nullptr; if (x11_win(ctx->focus.externalOverlayWindow) == id && gone) ctx->focus.externalOverlayWindow = nullptr; if (x11_win(ctx->focus.notificationWindow) == id && gone) ctx->focus.notificationWindow = nullptr; if (x11_win(ctx->focus.overrideWindow) == id && gone) ctx->focus.overrideWindow = nullptr; if (ctx->currentKeyboardFocusWindow == id && gone) ctx->currentKeyboardFocusWindow = None; for ( auto &iter : g_VirtualConnectorFocuses ) { global_focus_t *pFocus = &iter.second; // Global Focus if (x11_win(pFocus->focusWindow) == id && gone) pFocus->focusWindow = nullptr; if (x11_win(pFocus->inputFocusWindow) == id && gone) pFocus->inputFocusWindow = nullptr; if (x11_win(pFocus->overlayWindow) == id && gone) pFocus->overlayWindow = nullptr; if (x11_win(pFocus->notificationWindow) == id && gone) pFocus->notificationWindow = nullptr; if (x11_win(pFocus->overrideWindow) == id && gone) pFocus->overrideWindow = nullptr; if (x11_win(pFocus->fadeWindow) == id && gone) pFocus->fadeWindow = nullptr; } MakeFocusDirty(); finish_destroy_win(ctx, id, gone); } static void damage_win(xwayland_ctx_t *ctx, XDamageNotifyEvent *de) { steamcompmgr_win_t *w = find_win(ctx, de->drawable); steamcompmgr_win_t *focus = ctx->focus.focusWindow; if (!w) return; if (w->IsAnyOverlay() && !w->opacity) return; // First damage event we get, compute focus; we only want to focus damaged // windows to have meaningful frames. if (w->appID && w->xwayland().damage_sequence == 0) MakeFocusDirty(); w->xwayland().damage_sequence = damageSequence++; // If we just passed the focused window, we might be eliglible to take over if ( focus && focus != w && w->appID && w->xwayland().damage_sequence > focus->xwayland().damage_sequence) MakeFocusDirty(); // Josh: This will sometimes cause a BadDamage error. // I looked around at different compositors to see what // they do here and they just seem to ignore it. if (w->xwayland().damage) { XDamageSubtract(ctx->dpy, w->xwayland().damage, None, None); } gpuvis_trace_printf( "damage_win win %lx appID %u", w->xwayland().id, w->appID ); } static void handle_wl_surface_id(xwayland_ctx_t *ctx, steamcompmgr_win_t *w, uint32_t surfaceID) { struct wlr_surface *current_surface = NULL; struct wlr_surface *main_surface = NULL; wlserver_lock(); ctx->xwayland_server->set_wl_id( &w->xwayland().surface, surfaceID ); current_surface = w->xwayland().surface.current_surface(); main_surface = w->xwayland().surface.main_surface; if ( current_surface == NULL ) { wlserver_unlock(); return; } global_focus_t *pCurrentFocus = GetCurrentFocus(); if ( pCurrentFocus ) { // If we already focused on our side and are handling this late, // let wayland know now. if ( w == pCurrentFocus->inputFocusWindow ) wlserver_mousefocus( main_surface, INT32_MAX, INT32_MAX ); steamcompmgr_win_t *keyboardFocusWindow = pCurrentFocus->inputFocusWindow; if ( gamescope::VirtualConnectorIsSingleOutput() && keyboardFocusWindow && keyboardFocusWindow->inputFocusMode == 2 ) keyboardFocusWindow = pCurrentFocus->focusWindow; if ( w == keyboardFocusWindow ) wlserver_keyboardfocus( main_surface ); } // Pull the first buffer out of that window, if needed xwayland_surface_commit( current_surface ); wlserver_unlock(); } static void update_net_wm_state(uint32_t action, bool *value) { switch (action) { case NET_WM_STATE_REMOVE: *value = false; break; case NET_WM_STATE_ADD: *value = true; break; case NET_WM_STATE_TOGGLE: *value = !*value; break; default: xwm_log.debugf("Unknown NET_WM_STATE action: %" PRIu32, action); } } static void handle_net_wm_state(xwayland_ctx_t *ctx, steamcompmgr_win_t *w, XClientMessageEvent *ev) { uint32_t action = (uint32_t)ev->data.l[0]; Atom *props = (Atom *)&ev->data.l[1]; for (size_t i = 0; i < 2; i++) { if (props[i] == ctx->atoms.netWMStateFullscreenAtom) { update_net_wm_state(action, &w->isFullscreen); MakeFocusDirty(); } else if (props[i] == ctx->atoms.netWMStateSkipTaskbarAtom) { update_net_wm_state(action, &w->skipTaskbar); MakeFocusDirty(); } else if (props[i] == ctx->atoms.netWMStateSkipPagerAtom) { update_net_wm_state(action, &w->skipPager); MakeFocusDirty(); } else if (props[i] != None) { xwm_log.debugf("Unhandled NET_WM_STATE property change: %s", XGetAtomName(ctx->dpy, props[i])); } } } bool g_bLowLatency = false; static void handle_system_tray_opcode(xwayland_ctx_t *ctx, XClientMessageEvent *ev) { long opcode = ev->data.l[1]; switch (opcode) { case SYSTEM_TRAY_REQUEST_DOCK: { Window embed_id = ev->data.l[2]; /* At this point we're supposed to initiate the XEmbed lifecycle by * sending XEMBED_EMBEDDED_NOTIFY. However we don't actually need to * render the systray, we just want to recognize and blacklist these * icons. So for now do nothing. */ steamcompmgr_win_t *w = find_win(ctx, embed_id); if (w) { w->isSysTrayIcon = true; } break; } default: xwm_log.debugf("Unhandled _NET_SYSTEM_TRAY_OPCODE %ld", opcode); } } /* See http://tronche.com/gui/x/icccm/sec-4.html#s-4.1.4 */ static void handle_wm_change_state(xwayland_ctx_t *ctx, steamcompmgr_win_t *w, XClientMessageEvent *ev) { long state = ev->data.l[0]; if (state == ICCCM_ICONIC_STATE) { xwm_log.debugf("Faking WM_CHANGE_STATE to ICONIC for window 0x%lx", w->xwayland().id); set_wm_state( ctx, w->xwayland().id, ICCCM_ICONIC_STATE ); } else { xwm_log.debugf("Unhandled WM_CHANGE_STATE to %ld for window 0x%lx", state, w->xwayland().id); } } static void handle_client_message(xwayland_ctx_t *ctx, XClientMessageEvent *ev) { if (ev->window == ctx->ourWindow && ev->message_type == ctx->atoms.netSystemTrayOpcodeAtom) { handle_system_tray_opcode( ctx, ev ); return; } steamcompmgr_win_t *w = find_win(ctx, ev->window); if (w) { if (ev->message_type == ctx->atoms.WLSurfaceIDAtom) { handle_wl_surface_id( ctx, w, uint32_t(ev->data.l[0])); } else if ( ev->message_type == ctx->atoms.activeWindowAtom ) { XRaiseWindow( ctx->dpy, w->xwayland().id ); } else if ( ev->message_type == ctx->atoms.netWMStateAtom ) { handle_net_wm_state( ctx, w, ev ); } else if ( ev->message_type == ctx->atoms.WMChangeStateAtom ) { handle_wm_change_state( ctx, w, ev ); } else if ( ev->message_type != 0 ) { xwm_log.debugf( "Unhandled client message: %s", XGetAtomName( ctx->dpy, ev->message_type ) ); } } } static void x11_set_selection_owner(xwayland_ctx_t *ctx, std::string contents, GamescopeSelection eSelectionTarget) { Atom target; if (eSelectionTarget == GAMESCOPE_SELECTION_CLIPBOARD) { target = ctx->atoms.clipboard; } else if (eSelectionTarget == GAMESCOPE_SELECTION_PRIMARY) { target = ctx->atoms.primarySelection; } else { return; } XSetSelectionOwner(ctx->dpy, target, ctx->ourWindow, CurrentTime); } void gamescope_set_selection(std::string contents, GamescopeSelection eSelection) { if (eSelection == GAMESCOPE_SELECTION_CLIPBOARD) { clipboard = contents; } else if (eSelection == GAMESCOPE_SELECTION_PRIMARY) { primarySelection = contents; } gamescope_xwayland_server_t *server = NULL; for (int i = 0; (server = wlserver_get_xwayland_server(i)); i++) { x11_set_selection_owner(server->ctx.get(), contents, eSelection); } } void gamescope_set_reshade_effect(std::string effect_path) { gamescope_xwayland_server_t *server = wlserver_get_xwayland_server(0); set_string_prop(server->ctx.get(), server->ctx->atoms.gamescopeReshadeEffect, effect_path); } void gamescope_clear_reshade_effect() { gamescope_xwayland_server_t *server = wlserver_get_xwayland_server(0); clear_prop(server->ctx.get(), server->ctx->atoms.gamescopeReshadeEffect); } static void handle_selection_request(xwayland_ctx_t *ctx, XSelectionRequestEvent *ev) { std::string *selection = ev->selection == ctx->atoms.primarySelection ? &primarySelection : &clipboard; const char *targetString = XGetAtomName(ctx->dpy, ev->target); XEvent response; response.xselection.type = SelectionNotify; response.xselection.selection = ev->selection; response.xselection.requestor = ev->requestor; response.xselection.time = ev->time; response.xselection.property = None; response.xselection.target = None; if (ev->requestor == ctx->ourWindow) { return; } if (ev->target == ctx->atoms.targets) { Atom targetList[] = { ctx->atoms.targets, ctx->atoms.utf8StringAtom, }; XChangeProperty(ctx->dpy, ev->requestor, ev->property, XA_ATOM, 32, PropModeReplace, (unsigned char *)&targetList, sizeof(targetList) / sizeof(targetList[0])); response.xselection.property = ev->property; response.xselection.target = ev->target; } else if (!strcmp(targetString, "text/plain;charset=utf-8") || !strcmp(targetString, "text/plain") || !strcmp(targetString, "TEXT") || !strcmp(targetString, "UTF8_STRING") || !strcmp(targetString, "STRING")) { XChangeProperty(ctx->dpy, ev->requestor, ev->property, ev->target, 8, PropModeReplace, (unsigned char *)selection->c_str(), selection->length()); response.xselection.property = ev->property; response.xselection.target = ev->target; } else { xwm_log.debugf("Unsupported clipboard type: %s. Ignoring", targetString); } XSendEvent(ctx->dpy, ev->requestor, False, NoEventMask, &response); XFlush(ctx->dpy); } static void handle_selection_notify(xwayland_ctx_t *ctx, XSelectionEvent *ev) { Atom actual_type; int actual_format; unsigned long nitems; unsigned long bytes_after; unsigned char *data = NULL; XGetWindowProperty(ctx->dpy, ev->requestor, ev->property, 0, 0, False, AnyPropertyType, &actual_type, &actual_format, &nitems, &bytes_after, &data); if (data) { XFree(data); } if (actual_type == ctx->atoms.utf8StringAtom && actual_format == 8) { XGetWindowProperty(ctx->dpy, ev->requestor, ev->property, 0, bytes_after, False, AnyPropertyType, &actual_type, &actual_format, &nitems, &bytes_after, &data); if (data) { const char *contents = (const char *) data; auto szContents = std::make_shared(contents); defer( XFree( data ); ); gamescope::INestedHints *hints = nullptr; if (auto connector = GetBackend()->GetCurrentConnector()) hints = connector->GetNestedHints(); if (ev->selection == ctx->atoms.clipboard) { if ( hints ) { hints->SetSelection( szContents, GAMESCOPE_SELECTION_CLIPBOARD ); } else { gamescope_set_selection( contents, GAMESCOPE_SELECTION_CLIPBOARD ); } } else if (ev->selection == ctx->atoms.primarySelection) { if ( hints ) { hints->SetSelection( szContents, GAMESCOPE_SELECTION_PRIMARY ); } else { gamescope_set_selection( contents, GAMESCOPE_SELECTION_PRIMARY ); } } else { xwm_log.errorf( "Selection '%s' not supported. Ignoring", XGetAtomName(ctx->dpy, ev->selection) ); } } } } template T bit_cast(const J& src) { T dst; memcpy(&dst, &src, sizeof(T)); return dst; } static void update_runtime_info() { if ( g_nRuntimeInfoFd < 0 ) return; uint32_t limiter_enabled = g_nSteamCompMgrTargetFPS != 0 ? 1 : 0; pwrite( g_nRuntimeInfoFd, &limiter_enabled, sizeof( limiter_enabled ), 0 ); } static void init_runtime_info() { const char *path = getenv( "GAMESCOPE_LIMITER_FILE" ); if ( !path ) return; g_nRuntimeInfoFd = open( path, O_CREAT | O_RDWR , 0644 ); update_runtime_info(); } static void steamcompmgr_flush_frame_done( steamcompmgr_win_t *w ) { wlr_surface *current_surface = w->current_surface(); if ( current_surface && w->unlockedForFrameCallback && w->receivedDoneCommit ) { // TODO: Look into making this _RAW // wlroots, seems to just use normal MONOTONIC // all over so this may be problematic to just change. struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); wlr_surface *main_surface = w->main_surface(); w->unlockedForFrameCallback = false; w->receivedDoneCommit = false; w->last_commit_first_latch_time = timespec_to_nanos(now); // Acknowledge commit once. wlserver_lock(); if ( main_surface != nullptr ) { wlserver_send_frame_done(main_surface, &now); } if ( current_surface != nullptr && main_surface != current_surface ) { wlserver_send_frame_done(current_surface, &now); } wlserver_unlock(); } } static std::optional s_oLowestFPSLimitScheduleVRR; static bool steamcompmgr_should_vblank_window( bool bShouldLimitFPS, uint64_t vblank_idx, steamcompmgr_win_t *w = nullptr, uint64_t now = 0 ) { bool bSendCallback = true; int nRefreshHz = gamescope::ConvertmHzToHz( g_nNestedRefresh ? g_nNestedRefresh : g_nOutputRefresh ); int nTargetFPS = g_nSteamCompMgrTargetFPS; if ( GetBackend()->GetCurrentConnector() && GetBackend()->GetCurrentConnector()->IsVRRActive() ) { bool bCloseEnough = std::abs( g_nSteamCompMgrTargetFPS - nRefreshHz ) < 2; if ( g_nSteamCompMgrTargetFPS && bShouldLimitFPS && w && !bCloseEnough ) { uint64_t schedule = w->last_commit_first_latch_time + g_SteamCompMgrLimitedAppRefreshCycle; static constexpr uint64_t k_ulVRRScheduleFudge = 200'000; // 0.2ms if ( now + k_ulVRRScheduleFudge < schedule ) { bSendCallback = false; if ( !s_oLowestFPSLimitScheduleVRR ) s_oLowestFPSLimitScheduleVRR = schedule; else s_oLowestFPSLimitScheduleVRR = std::min( *s_oLowestFPSLimitScheduleVRR, schedule ); } } } else { if ( g_nSteamCompMgrTargetFPS && bShouldLimitFPS && nRefreshHz > nTargetFPS ) { int nVblankDivisor = nRefreshHz / nTargetFPS; if ( vblank_idx % nVblankDivisor != 0 ) bSendCallback = false; } } return bSendCallback; } static bool steamcompmgr_should_vblank_window( steamcompmgr_win_t *w, uint64_t vblank_idx, uint64_t now ) { return steamcompmgr_should_vblank_window( steamcompmgr_window_should_limit_fps( w ), vblank_idx, w, now ); } static void steamcompmgr_latch_frame_done( steamcompmgr_win_t *w, uint64_t vblank_idx, uint64_t now ) { if ( steamcompmgr_should_vblank_window( w, vblank_idx, now ) ) { w->unlockedForFrameCallback = true; } } static inline float santitize_float( float f ) { #ifndef __FAST_MATH__ return ( std::isfinite( f ) ? f : 0.f ); #else return f; #endif } static void handle_property_notify(xwayland_ctx_t *ctx, XPropertyEvent *ev) { /* check if Trans property was changed */ if (ev->atom == ctx->atoms.opacityAtom) { /* reset mode and redraw window */ steamcompmgr_win_t * w = find_win(ctx, ev->window); if ( w != nullptr ) { unsigned int newOpacity = get_prop(ctx, w->xwayland().id, ctx->atoms.opacityAtom, OPAQUE); if (newOpacity != w->opacity) { w->opacity = newOpacity; if ( gameFocused && ( w == ctx->focus.overlayWindow || w == ctx->focus.notificationWindow ) ) { hasRepaintNonBasePlane = true; } if ( w == ctx->focus.externalOverlayWindow ) { hasRepaint = true; } } unsigned int maxOpacity = 0; unsigned int maxOpacityExternal = 0; for (w = ctx->list; w; w = w->xwayland().next) { if (w->isOverlay) { if (w->GetGeometry().nWidth > 1200 && w->opacity >= maxOpacity) { ctx->focus.overlayWindow = w; maxOpacity = w->opacity; } } if (w->isExternalOverlay) { if (w->opacity >= maxOpacityExternal) { ctx->focus.externalOverlayWindow = w; maxOpacityExternal = w->opacity; } } } } } if (ev->atom == ctx->atoms.steamAtom) { steamcompmgr_win_t * w = find_win(ctx, ev->window); if (w) { w->isSteamLegacyBigPicture = get_prop(ctx, w->xwayland().id, ctx->atoms.steamAtom, 0); MakeFocusDirty(); } } if (ev->atom == ctx->atoms.steamInputFocusAtom ) { steamcompmgr_win_t * w = find_win(ctx, ev->window); if (w) { w->inputFocusMode = get_prop(ctx, w->xwayland().id, ctx->atoms.steamInputFocusAtom, 0); MakeFocusDirty(); } } if (ev->atom == ctx->atoms.steamTouchClickModeAtom ) { gamescope::cv_touch_click_mode = (gamescope::TouchClickMode) get_prop(ctx, ctx->root, ctx->atoms.steamTouchClickModeAtom, 0u ); } if (ev->atom == ctx->atoms.steamStreamingClientAtom) { steamcompmgr_win_t * w = find_win(ctx, ev->window); if (w) { w->isSteamStreamingClient = get_prop(ctx, w->xwayland().id, ctx->atoms.steamStreamingClientAtom, 0); MakeFocusDirty(); } } if (ev->atom == ctx->atoms.steamStreamingClientVideoAtom) { steamcompmgr_win_t * w = find_win(ctx, ev->window); if (w) { w->isSteamStreamingClientVideo = get_prop(ctx, w->xwayland().id, ctx->atoms.steamStreamingClientVideoAtom, 0); MakeFocusDirty(); } } if (ev->atom == ctx->atoms.gamescopeCtrlAppIDAtom ) { get_prop( ctx, ctx->root, ctx->atoms.gamescopeCtrlAppIDAtom, vecFocuscontrolAppIDs ); MakeFocusDirty(); } if (ev->atom == ctx->atoms.gamescopeCtrlWindowAtom ) { ctx->focusControlWindow = get_prop( ctx, ctx->root, ctx->atoms.gamescopeCtrlWindowAtom, None ); MakeFocusDirty(); } if ( ev->atom == ctx->atoms.gamescopeScreenShotAtom ) { if ( ev->state == PropertyNewValue ) { gamescope::CScreenshotManager::Get().TakeScreenshot( gamescope::GamescopeScreenshotInfo { .szScreenshotPath = "/tmp/gamescope.png", .eScreenshotType = (gamescope_control_screenshot_type) get_prop( ctx, ctx->root, ctx->atoms.gamescopeScreenShotAtom, None ), .uScreenshotFlags = 0, .bX11PropertyRequested = true, } ); } } if ( ev->atom == ctx->atoms.gamescopeDebugScreenShotAtom ) { if ( ev->state == PropertyNewValue ) { gamescope::CScreenshotManager::Get().TakeScreenshot( gamescope::GamescopeScreenshotInfo { .szScreenshotPath = "/tmp/gamescope.png", .eScreenshotType = (gamescope_control_screenshot_type) get_prop( ctx, ctx->root, ctx->atoms.gamescopeDebugScreenShotAtom, None ), .uScreenshotFlags = 0, .bX11PropertyRequested = true, } ); } } if (ev->atom == ctx->atoms.gameAtom) { steamcompmgr_win_t * w = find_win(ctx, ev->window); if (w) { uint32_t appID = get_prop(ctx, w->xwayland().id, ctx->atoms.gameAtom, 0); if ( w->appID != 0 && appID != 0 && w->appID != appID ) { xwm_log.errorf( "appid clash was %u now %u", w->appID, appID ); } w->appID = appID; if ( w->isExternalOverlay ) w->appID = 0; MakeFocusDirty(); } } if (ev->atom == ctx->atoms.overlayAtom) { steamcompmgr_win_t * w = find_win(ctx, ev->window); if (w) { w->isOverlay = get_prop(ctx, w->xwayland().id, ctx->atoms.overlayAtom, 0); if ( w->isExternalOverlay ) w->appID = 0; MakeFocusDirty(); } } if (ev->atom == ctx->atoms.externalOverlayAtom) { steamcompmgr_win_t * w = find_win(ctx, ev->window); if (w) { w->isExternalOverlay = get_prop(ctx, w->xwayland().id, ctx->atoms.externalOverlayAtom, 0); if ( w->isExternalOverlay ) w->appID = 0; MakeFocusDirty(); } } if (ev->atom == ctx->atoms.winTypeAtom) { steamcompmgr_win_t * w = find_win(ctx, ev->window); if (w) { get_win_type(ctx, w); MakeFocusDirty(); } } if (ev->atom == ctx->atoms.sizeHintsAtom) { steamcompmgr_win_t * w = find_win(ctx, ev->window); if (w) { get_size_hints(ctx, w); MakeFocusDirty(); } } if (ev->atom == ctx->atoms.gamesRunningAtom) { gamesRunningCount = get_prop(ctx, ctx->root, ctx->atoms.gamesRunningAtom, 0); MakeFocusDirty(); } if (ev->atom == ctx->atoms.screenScaleAtom) { overscanScaleRatio = get_prop(ctx, ctx->root, ctx->atoms.screenScaleAtom, 0xFFFFFFFF) / (double)0xFFFFFFFF; globalScaleRatio = overscanScaleRatio * zoomScaleRatio; for ( auto &iter : g_VirtualConnectorFocuses ) { global_focus_t *pFocus = &iter.second; if (pFocus->focusWindow) { hasRepaint = true; } } MakeFocusDirty(); } if (ev->atom == ctx->atoms.screenZoomAtom) { zoomScaleRatio = get_prop(ctx, ctx->root, ctx->atoms.screenZoomAtom, 0xFFFF) / (double)0xFFFF; globalScaleRatio = overscanScaleRatio * zoomScaleRatio; for ( auto &iter : g_VirtualConnectorFocuses ) { global_focus_t *pFocus = &iter.second; if (pFocus->focusWindow) { hasRepaint = true; } } MakeFocusDirty(); } if (ev->atom == ctx->atoms.WMTransientForAtom) { steamcompmgr_win_t * w = find_win(ctx, ev->window); if (w) { Window transientFor = None; if ( XGetTransientForHint( ctx->dpy, ev->window, &transientFor ) ) { w->xwayland().transientFor = transientFor; } else { w->xwayland().transientFor = None; } get_win_type( ctx, w ); MakeFocusDirty(); } } if (ev->atom == XA_WM_NAME || ev->atom == ctx->atoms.netWMNameAtom) { steamcompmgr_win_t *w = find_win(ctx, ev->window); if (w) { get_win_title(ctx, w, ev->atom); for ( auto &iter : g_VirtualConnectorFocuses ) { global_focus_t *pFocus = &iter.second; if (ev->window == x11_win(pFocus->focusWindow)) { if ( pFocus->GetNestedHints() ) pFocus->GetNestedHints()->SetTitle( w->title ); } } } } if (ev->atom == ctx->atoms.netWMIcon) { steamcompmgr_win_t *w = find_win(ctx, ev->window); if (w) { get_win_icon(ctx, w); for ( auto &iter : g_VirtualConnectorFocuses ) { global_focus_t *pFocus = &iter.second; if (ev->window == x11_win(pFocus->focusWindow)) { if ( pFocus->GetNestedHints() ) pFocus->GetNestedHints()->SetIcon( w->icon ); } } } } #if 0 if ( ev->atom == ctx->atoms.gamescopeTuneableVBlankRedZone ) { g_uVblankDrawBufferRedZoneNS = (uint64_t)get_prop( ctx, ctx->root, ctx->atoms.gamescopeTuneableVBlankRedZone, g_uDefaultVBlankRedZone ); } if ( ev->atom == ctx->atoms.gamescopeTuneableRateOfDecay ) { g_uVBlankRateOfDecayPercentage = (uint64_t)get_prop( ctx, ctx->root, ctx->atoms.gamescopeTuneableRateOfDecay, g_uDefaultVBlankRateOfDecayPercentage ); } #endif if ( ev->atom == ctx->atoms.gamescopeScalingFilter ) { int nScalingMode = get_prop( ctx, ctx->root, ctx->atoms.gamescopeScalingFilter, 0 ); switch ( nScalingMode ) { default: case 0: g_wantedUpscaleScaler = GamescopeUpscaleScaler::AUTO; g_wantedUpscaleFilter = GamescopeUpscaleFilter::LINEAR; break; case 1: g_wantedUpscaleScaler = GamescopeUpscaleScaler::AUTO; g_wantedUpscaleFilter = GamescopeUpscaleFilter::NEAREST; break; case 2: g_wantedUpscaleScaler = GamescopeUpscaleScaler::INTEGER; g_wantedUpscaleFilter = GamescopeUpscaleFilter::NEAREST; break; case 3: g_wantedUpscaleScaler = GamescopeUpscaleScaler::AUTO; g_wantedUpscaleFilter = GamescopeUpscaleFilter::FSR; break; case 4: g_wantedUpscaleScaler = GamescopeUpscaleScaler::AUTO; g_wantedUpscaleFilter = GamescopeUpscaleFilter::NIS; break; } hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeFSRSharpness || ev->atom == ctx->atoms.gamescopeSharpness ) { g_upscaleFilterSharpness = (int)clamp( get_prop( ctx, ctx->root, ev->atom, 2 ), 0u, 20u ); if ( g_upscaleFilter == GamescopeUpscaleFilter::FSR || g_upscaleFilter == GamescopeUpscaleFilter::NIS ) hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeXWaylandModeControl ) { std::vector< uint32_t > xwayland_mode_ctl; bool hasModeCtrl = get_prop( ctx, ctx->root, ctx->atoms.gamescopeXWaylandModeControl, xwayland_mode_ctl ); if ( hasModeCtrl && xwayland_mode_ctl.size() == 4 ) { size_t server_idx = size_t{ xwayland_mode_ctl[ 0 ] }; int width = xwayland_mode_ctl[ 1 ]; int height = xwayland_mode_ctl[ 2 ]; bool allowSuperRes = !!xwayland_mode_ctl[ 3 ]; if ( !allowSuperRes ) { width = std::min(width, currentOutputWidth); height = std::min(height, currentOutputHeight); } gamescope_xwayland_server_t *server = wlserver_get_xwayland_server( server_idx ); if ( server ) { bool root_size_identical = server->ctx->root_width == width && server->ctx->root_height == height; wlserver_lock(); wlserver_set_xwayland_server_mode( server_idx, width, height, g_nOutputRefresh ); wlserver_unlock(); if ( root_size_identical ) { gamescope_xwayland_server_t *root_server = wlserver_get_xwayland_server(0); xwayland_ctx_t *root_ctx = root_server->ctx.get(); XDeleteProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeXWaylandModeControl ); XFlush( root_ctx->dpy ); } } } } if ( ev->atom == ctx->atoms.gamescopeFPSLimit ) { g_nSteamCompMgrTargetFPS = get_prop( ctx, ctx->root, ctx->atoms.gamescopeFPSLimit, 0 ); update_runtime_info(); } for (int i = 0; i < gamescope::GAMESCOPE_SCREEN_TYPE_COUNT; i++) { if ( ev->atom == ctx->atoms.gamescopeDynamicRefresh[i] ) { g_nDynamicRefreshRate[i] = get_prop( ctx, ctx->root, ctx->atoms.gamescopeDynamicRefresh[i], 0 ); } } if ( ev->atom == ctx->atoms.gamescopeLowLatency ) { g_bLowLatency = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeLowLatency, 0 ); } if ( ev->atom == ctx->atoms.gamescopeBlurMode ) { BlurMode newBlur = (BlurMode)get_prop( ctx, ctx->root, ctx->atoms.gamescopeBlurMode, 0 ); if (newBlur < BLUR_MODE_OFF || newBlur > BLUR_MODE_ALWAYS) newBlur = BLUR_MODE_OFF; if (newBlur != g_BlurMode) { g_BlurFadeStartTime = get_time_in_milliseconds(); g_BlurModeOld = g_BlurMode; g_BlurMode = newBlur; hasRepaint = true; } } if ( ev->atom == ctx->atoms.gamescopeBlurRadius ) { unsigned int pixel = get_prop( ctx, ctx->root, ctx->atoms.gamescopeBlurRadius, 0 ); g_BlurRadius = (int)clamp((pixel / 2) + 1, 1u, kMaxBlurRadius - 1); if ( g_BlurMode ) hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeBlurFadeDuration ) { g_BlurFadeDuration = get_prop( ctx, ctx->root, ctx->atoms.gamescopeBlurFadeDuration, 0 ); } if ( ev->atom == ctx->atoms.gamescopeCompositeForce ) { cv_composite_force = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeCompositeForce, 0 ); hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeCompositeDebug ) { cv_composite_debug = get_prop( ctx, ctx->root, ctx->atoms.gamescopeCompositeDebug, 0 ); hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeAllowTearing ) { cv_tearing_enabled = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeAllowTearing, 0 ); } if ( ev->atom == ctx->atoms.gamescopeSteamMaxHeight ) { g_nSteamMaxHeight = get_prop( ctx, ctx->root, ctx->atoms.gamescopeSteamMaxHeight, 0 ); MakeFocusDirty(); } if ( ev->atom == ctx->atoms.gamescopeVRREnabled ) { bool enabled = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeVRREnabled, 0 ); cv_adaptive_sync = enabled; } if ( ev->atom == ctx->atoms.gamescopeDisplayForceInternal ) { g_bForceInternal = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeDisplayForceInternal, 0 ); GetBackend()->DirtyState(); } if ( ev->atom == ctx->atoms.gamescopeDisplayModeNudge ) { GetBackend()->DirtyState( true ); XDeleteProperty( ctx->dpy, ctx->root, ctx->atoms.gamescopeDisplayModeNudge ); } if ( ev->atom == ctx->atoms.gamescopeNewScalingFilter ) { GamescopeUpscaleFilter nScalingFilter = ( GamescopeUpscaleFilter ) get_prop( ctx, ctx->root, ctx->atoms.gamescopeNewScalingFilter, 0 ); if (g_wantedUpscaleFilter != nScalingFilter) { g_wantedUpscaleFilter = nScalingFilter; hasRepaint = true; } } if ( ev->atom == ctx->atoms.gamescopeNewScalingScaler ) { GamescopeUpscaleScaler nScalingScaler = ( GamescopeUpscaleScaler ) get_prop( ctx, ctx->root, ctx->atoms.gamescopeNewScalingScaler, 0 ); if (g_wantedUpscaleScaler != nScalingScaler) { g_wantedUpscaleScaler = nScalingScaler; hasRepaint = true; } } if ( ev->atom == ctx->atoms.gamescopeDisplayHDREnabled ) { cv_hdr_enabled = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeDisplayHDREnabled, 0 ); hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeDebugForceHDR10Output ) { g_bForceHDR10OutputDebug = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeDebugForceHDR10Output, 0 ); hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeDebugForceHDRSupport ) { g_bForceHDRSupportDebug = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeDebugForceHDRSupport, 0 ); GetBackend()->HackUpdatePatchedEdid(); hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeDebugHDRHeatmap ) { uint32_t heatmap = get_prop( ctx, ctx->root, ctx->atoms.gamescopeDebugHDRHeatmap, 0 ); cv_composite_debug &= ~CompositeDebugFlag::Heatmap; cv_composite_debug &= ~CompositeDebugFlag::Heatmap_MSWCG; cv_composite_debug &= ~CompositeDebugFlag::Heatmap_Hard; if (heatmap != 0) cv_composite_debug |= CompositeDebugFlag::Heatmap; if (heatmap == 2) cv_composite_debug |= CompositeDebugFlag::Heatmap_MSWCG; if (heatmap == 3) cv_composite_debug |= CompositeDebugFlag::Heatmap_Hard; hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeHDRTonemapOperator ) { g_ColorMgmt.pending.hdrTonemapOperator = (ETonemapOperator) get_prop( ctx, ctx->root, ctx->atoms.gamescopeHDRTonemapOperator, 0 ); hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeHDRTonemapDisplayMetadata ) { std::vector< uint32_t > user_vec; if ( get_prop( ctx, ctx->root, ctx->atoms.gamescopeHDRTonemapDisplayMetadata, user_vec ) && user_vec.size() >= 2 ) { g_ColorMgmt.pending.hdrTonemapDisplayMetadata.flBlackPointNits = bit_cast( user_vec[0] ); g_ColorMgmt.pending.hdrTonemapDisplayMetadata.flWhitePointNits = bit_cast( user_vec[1] ); } else { g_ColorMgmt.pending.hdrTonemapDisplayMetadata.reset(); } hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeHDRTonemapSourceMetadata ) { std::vector< uint32_t > user_vec; if ( get_prop( ctx, ctx->root, ctx->atoms.gamescopeHDRTonemapSourceMetadata, user_vec ) && user_vec.size() >= 2 ) { g_ColorMgmt.pending.hdrTonemapSourceMetadata.flBlackPointNits = bit_cast( user_vec[0] ); g_ColorMgmt.pending.hdrTonemapSourceMetadata.flWhitePointNits = bit_cast( user_vec[1] ); } else { g_ColorMgmt.pending.hdrTonemapSourceMetadata.reset(); } hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeSDROnHDRContentBrightness ) { uint32_t val = get_prop( ctx, ctx->root, ctx->atoms.gamescopeSDROnHDRContentBrightness, 0 ); if ( set_sdr_on_hdr_brightness( bit_cast(val) ) ) hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeHDRItmEnable ) { g_bHDRItmEnable = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeHDRItmEnable, 0 ); hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeHDRItmSDRNits ) { g_flHDRItmSdrNits = get_prop( ctx, ctx->root, ctx->atoms.gamescopeHDRItmSDRNits, 0 ); if ( g_flHDRItmSdrNits < 1.f ) g_flHDRItmSdrNits = 100.f; else if ( g_flHDRItmSdrNits > 1000.f) g_flHDRItmSdrNits = 1000.f; hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeHDRItmTargetNits ) { g_flHDRItmTargetNits = get_prop( ctx, ctx->root, ctx->atoms.gamescopeHDRItmTargetNits, 0 ); if ( g_flHDRItmTargetNits < 1.f ) g_flHDRItmTargetNits = 1000.f; else if ( g_flHDRItmTargetNits > 10000.f) g_flHDRItmTargetNits = 10000.f; hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeColorLookPQ ) { std::string path = get_string_prop( ctx, ctx->root, ctx->atoms.gamescopeColorLookPQ ); if ( set_color_look_pq( path.c_str() ) ) hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeColorLookG22 ) { std::string path = get_string_prop( ctx, ctx->root, ctx->atoms.gamescopeColorLookG22 ); if ( set_color_look_g22( path.c_str() ) ) hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeColorOutputVirtualWhite ) { std::vector< uint32_t > user_vec; if ( get_prop( ctx, ctx->root, ctx->atoms.gamescopeColorOutputVirtualWhite, user_vec ) && user_vec.size() >= 2 ) { g_ColorMgmt.pending.outputVirtualWhite.x = santitize_float( bit_cast( user_vec[0] ) ); g_ColorMgmt.pending.outputVirtualWhite.y = santitize_float( bit_cast( user_vec[1] ) ); } else { g_ColorMgmt.pending.outputVirtualWhite.x = 0.f; g_ColorMgmt.pending.outputVirtualWhite.y = 0.f; } hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeHDRInputGain ) { uint32_t val = get_prop( ctx, ctx->root, ctx->atoms.gamescopeHDRInputGain, 0 ); if ( set_hdr_input_gain( bit_cast(val) ) ) hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeSDRInputGain ) { uint32_t val = get_prop( ctx, ctx->root, ctx->atoms.gamescopeSDRInputGain, 0 ); if ( set_sdr_input_gain( bit_cast(val) ) ) hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeForceWindowsFullscreen ) { ctx->force_windows_fullscreen = !!get_prop( ctx, ctx->root, ctx->atoms.gamescopeForceWindowsFullscreen, 0 ); MakeFocusDirty(); } if ( ev->atom == ctx->atoms.gamescopeColorLut3DOverride ) { std::string path = get_string_prop( ctx, ctx->root, ctx->atoms.gamescopeColorLut3DOverride ); if ( set_color_3dlut_override( path.c_str() ) ) hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeColorShaperLutOverride ) { std::string path = get_string_prop( ctx, ctx->root, ctx->atoms.gamescopeColorShaperLutOverride ); if ( set_color_shaperlut_override( path.c_str() ) ) hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeColorSDRGamutWideness ) { uint32_t val = get_prop(ctx, ctx->root, ctx->atoms.gamescopeColorSDRGamutWideness, 0); if ( set_color_sdr_gamut_wideness( bit_cast(val) ) ) hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeColorNightMode ) { std::vector< uint32_t > user_vec; bool bHasVec = get_prop( ctx, ctx->root, ctx->atoms.gamescopeColorNightMode, user_vec ); // identity float vec[3] = { 0.0f, 0.0f, 0.0f }; if ( bHasVec && user_vec.size() == 3 ) { for (int i = 0; i < 3; i++) vec[i] = bit_cast( user_vec[i] ); } nightmode_t nightmode; nightmode.amount = vec[0]; nightmode.hue = vec[1]; nightmode.saturation = vec[2]; if ( set_color_nightmode( nightmode ) ) hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeColorManagementDisable ) { uint32_t val = get_prop(ctx, ctx->root, ctx->atoms.gamescopeColorManagementDisable, 0); if ( set_color_mgmt_enabled( !val ) ) hasRepaint = true; } if ( ev->atom == ctx->atoms.gamescopeColorSliderInUse ) { uint32_t val = get_prop(ctx, ctx->root, ctx->atoms.gamescopeColorSliderInUse, 0); g_bColorSliderInUse = !!val; } if ( ev->atom == ctx->atoms.gamescopeColorChromaticAdaptationMode ) { uint32_t val = get_prop(ctx, ctx->root, ctx->atoms.gamescopeColorChromaticAdaptationMode, 0); g_ColorMgmt.pending.chromaticAdaptationMode = ( EChromaticAdaptationMethod ) val; } // TODO: Hook up gamescopeColorMuraCorrectionImage for external. if ( ev->atom == ctx->atoms.gamescopeColorMuraCorrectionImage[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL] ) { std::string path = get_string_prop( ctx, ctx->root, ctx->atoms.gamescopeColorMuraCorrectionImage[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL] ); if ( set_mura_overlay( path.c_str() ) ) hasRepaint = true; } // TODO: Hook up gamescopeColorMuraScale for external. if ( ev->atom == ctx->atoms.gamescopeColorMuraScale[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL] ) { uint32_t val = get_prop(ctx, ctx->root, ctx->atoms.gamescopeColorMuraScale[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL], 0); float new_scale = bit_cast(val); if ( set_mura_scale( new_scale ) ) hasRepaint = true; } // TODO: Hook up gamescopeColorMuraCorrectionDisabled for external. if ( ev->atom == ctx->atoms.gamescopeColorMuraCorrectionDisabled[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL] ) { bool disabled = !!get_prop(ctx, ctx->root, ctx->atoms.gamescopeColorMuraCorrectionDisabled[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL], 0); if ( g_bMuraCompensationDisabled != disabled ) { g_bMuraCompensationDisabled = disabled; hasRepaint = true; } } if (ev->atom == ctx->atoms.gamescopeCreateXWaylandServer) { uint32_t identifier = get_prop(ctx, ctx->root, ctx->atoms.gamescopeCreateXWaylandServer, 0); if (identifier) { wlserver_lock(); uint32_t server_id = (uint32_t)wlserver_make_new_xwayland_server(); assert(server_id != ~0u); gamescope_xwayland_server_t *server = wlserver_get_xwayland_server(server_id); init_xwayland_ctx(server_id, server); char propertyString[256]; snprintf(propertyString, sizeof(propertyString), "%u %u %s", identifier, server_id, server->get_nested_display_name()); XTextProperty text_property = { .value = (unsigned char *)propertyString, .encoding = ctx->atoms.utf8StringAtom, .format = 8, .nitems = strlen(propertyString), }; g_SteamCompMgrWaiter.AddWaitable( server->ctx.get() ); XSetTextProperty( ctx->dpy, ctx->root, &text_property, ctx->atoms.gamescopeCreateXWaylandServerFeedback ); wlserver_unlock(); } } if (ev->atom == ctx->atoms.gamescopeDestroyXWaylandServer) { uint32_t server_id = get_prop(ctx, ctx->root, ctx->atoms.gamescopeDestroyXWaylandServer, 0); gamescope_xwayland_server_t *server = wlserver_get_xwayland_server(server_id); if (server) { for ( auto &iter : g_VirtualConnectorFocuses ) { global_focus_t *pFocus = &iter.second; if (pFocus->focusWindow && pFocus->focusWindow->type == steamcompmgr_win_type_t::XWAYLAND && pFocus->focusWindow->xwayland().ctx == server->ctx.get()) pFocus->focusWindow = nullptr; if (pFocus->inputFocusWindow && pFocus->inputFocusWindow->type == steamcompmgr_win_type_t::XWAYLAND && pFocus->inputFocusWindow->xwayland().ctx == server->ctx.get()) pFocus->inputFocusWindow = nullptr; if (pFocus->overlayWindow && pFocus->overlayWindow->type == steamcompmgr_win_type_t::XWAYLAND && pFocus->overlayWindow->xwayland().ctx == server->ctx.get()) pFocus->overlayWindow = nullptr; if (pFocus->externalOverlayWindow && pFocus->externalOverlayWindow->type == steamcompmgr_win_type_t::XWAYLAND && pFocus->externalOverlayWindow->xwayland().ctx == server->ctx.get()) pFocus->externalOverlayWindow = nullptr; if (pFocus->notificationWindow && pFocus->notificationWindow->type == steamcompmgr_win_type_t::XWAYLAND && pFocus->notificationWindow->xwayland().ctx == server->ctx.get()) pFocus->notificationWindow = nullptr; if (pFocus->overrideWindow && pFocus->overrideWindow->type == steamcompmgr_win_type_t::XWAYLAND && pFocus->overrideWindow->xwayland().ctx == server->ctx.get()) pFocus->overrideWindow = nullptr; if (pFocus->keyboardFocusWindow && pFocus->keyboardFocusWindow->type == steamcompmgr_win_type_t::XWAYLAND && pFocus->keyboardFocusWindow->xwayland().ctx == server->ctx.get()) pFocus->keyboardFocusWindow = nullptr; if (pFocus->fadeWindow && pFocus->fadeWindow->type == steamcompmgr_win_type_t::XWAYLAND && pFocus->fadeWindow->xwayland().ctx == server->ctx.get()) pFocus->fadeWindow = nullptr; if (pFocus->cursor && pFocus->cursor->getCtx() == server->ctx.get()) pFocus->cursor = nullptr; } wlserver_lock(); g_SteamCompMgrWaiter.RemoveWaitable( server->ctx.get() ); wlserver_destroy_xwayland_server(server); wlserver_unlock(); MakeFocusDirty(); } } if (ev->atom == ctx->atoms.gamescopeReshadeTechniqueIdx) { uint32_t technique_idx = get_prop(ctx, ctx->root, ctx->atoms.gamescopeReshadeTechniqueIdx, 0); g_reshade_technique_idx = technique_idx; } if (ev->atom == ctx->atoms.gamescopeReshadeEffect) { std::string path = get_string_prop( ctx, ctx->root, ctx->atoms.gamescopeReshadeEffect ); g_reshade_effect = path; } if (ev->atom == ctx->atoms.gamescopeDisplayDynamicRefreshBasedOnGamePresence) { g_bChangeDynamicRefreshBasedOnGameOpenRatherThanActive = !!get_prop(ctx, ctx->root, ctx->atoms.gamescopeDisplayDynamicRefreshBasedOnGamePresence, 0); } if (ev->atom == ctx->atoms.wineHwndStyle) { steamcompmgr_win_t * w = find_win(ctx, ev->window); if (w) { w->hasHwndStyle = true; w->hwndStyle = get_prop(ctx, w->xwayland().id, ctx->atoms.wineHwndStyle, 0); MakeFocusDirty(); } } if (ev->atom == ctx->atoms.wineHwndStyleEx) { steamcompmgr_win_t * w = find_win(ctx, ev->window); if (w) { w->hasHwndStyleEx = true; w->hwndStyleEx = get_prop(ctx, w->xwayland().id, ctx->atoms.wineHwndStyleEx, 0); MakeFocusDirty(); } } } static int error(Display *dpy, XErrorEvent *ev) { // Do nothing. XErrors are usually benign. return 0; } static void steamcompmgr_exit(void) { g_ImageWaiter.Shutdown(); // Clean up any commits. { gamescope_xwayland_server_t *server = NULL; for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++) { for ( steamcompmgr_win_t *w = server->ctx->list; w; w = w->xwayland().next ) w->commit_queue.clear(); } } g_steamcompmgr_xdg_wins.clear(); g_HeldCommits[ HELD_COMMIT_BASE ] = nullptr; g_HeldCommits[ HELD_COMMIT_FADE ] = nullptr; for ( auto &lut : g_ColorMgmtLuts ) lut.shutdown(); for ( auto &lut : g_ColorMgmtLutsOverride ) lut.shutdown(); for ( auto &lut : g_ScreenshotColorMgmtLuts ) lut.shutdown(); for ( auto &lut : g_ScreenshotColorMgmtLutsHDR ) lut.shutdown(); if ( statsThreadRun == true ) { statsThreadRun = false; statsThreadSem.signal(); } { g_ColorMgmt.pending.appHDRMetadata = nullptr; g_ColorMgmt.current.appHDRMetadata = nullptr; s_scRGB709To2020Matrix = nullptr; for (int i = 0; i < gamescope::GAMESCOPE_SCREEN_TYPE_COUNT; i++) { s_MuraCorrectionImage[i] = nullptr; s_MuraCTMBlob[i] = nullptr; } } g_VirtualConnectorFocuses.clear(); gamescope::IBackend::Set( nullptr ); wlserver_lock(); wlserver_shutdown(); wlserver_unlock(false); } [[noreturn]] static int handle_io_error(Display *dpy) { xwm_log.errorf("X11 I/O error"); steamcompmgr_exit(); g_SteamCompMgrXWaylandServerMutex.unlock(); pthread_exit(NULL); } static bool register_cm(xwayland_ctx_t *ctx) { Window w; Atom a; static char net_wm_cm[] = "_NET_WM_CM_Sxx"; snprintf(net_wm_cm, sizeof(net_wm_cm), "_NET_WM_CM_S%d", ctx->scr); a = XInternAtom(ctx->dpy, net_wm_cm, false); w = XGetSelectionOwner(ctx->dpy, a); if (w != None) { XTextProperty tp; char **strs; int count; Atom winNameAtom = XInternAtom(ctx->dpy, "_NET_WM_NAME", false); if (!XGetTextProperty(ctx->dpy, w, &tp, winNameAtom) && !XGetTextProperty(ctx->dpy, w, &tp, XA_WM_NAME)) { xwm_log.errorf("Another composite manager is already running (0x%lx)", (unsigned long) w); return false; } if (XmbTextPropertyToTextList(ctx->dpy, &tp, &strs, &count) == Success) { xwm_log.errorf("Another composite manager is already running (%s)", strs[0]); XFreeStringList(strs); } XFree(tp.value); return false; } w = XCreateSimpleWindow(ctx->dpy, RootWindow(ctx->dpy, ctx->scr), 0, 0, 1, 1, 0, None, None); Xutf8SetWMProperties(ctx->dpy, w, "steamcompmgr", "steamcompmgr", NULL, 0, NULL, NULL, NULL); Atom atomWmCheck = XInternAtom(ctx->dpy, "_NET_SUPPORTING_WM_CHECK", false); XChangeProperty(ctx->dpy, ctx->root, atomWmCheck, XA_WINDOW, 32, PropModeReplace, (unsigned char *)&w, 1); XChangeProperty(ctx->dpy, w, atomWmCheck, XA_WINDOW, 32, PropModeReplace, (unsigned char *)&w, 1); Atom supportedAtoms[] = { XInternAtom(ctx->dpy, "_NET_WM_STATE", false), XInternAtom(ctx->dpy, "_NET_WM_STATE_FULLSCREEN", false), XInternAtom(ctx->dpy, "_NET_WM_STATE_SKIP_TASKBAR", false), XInternAtom(ctx->dpy, "_NET_WM_STATE_SKIP_PAGER", false), XInternAtom(ctx->dpy, "_NET_ACTIVE_WINDOW", false), }; XChangeProperty(ctx->dpy, ctx->root, XInternAtom(ctx->dpy, "_NET_SUPPORTED", false), XA_ATOM, 32, PropModeAppend, (unsigned char *)supportedAtoms, sizeof(supportedAtoms) / sizeof(supportedAtoms[0])); XSetSelectionOwner(ctx->dpy, a, w, 0); ctx->ourWindow = w; return true; } static void register_systray(xwayland_ctx_t *ctx) { static char net_system_tray_name[] = "_NET_SYSTEM_TRAY_Sxx"; snprintf(net_system_tray_name, sizeof(net_system_tray_name), "_NET_SYSTEM_TRAY_S%d", ctx->scr); Atom net_system_tray = XInternAtom(ctx->dpy, net_system_tray_name, false); XSetSelectionOwner(ctx->dpy, net_system_tray, ctx->ourWindow, 0); } bool handle_done_commit( steamcompmgr_win_t *w, xwayland_ctx_t *ctx, uint64_t commitID, uint64_t earliestPresentTime, uint64_t earliestLatchTime ) { bool bFoundWindow = false; uint32_t j; for ( j = 0; j < w->commit_queue.size(); j++ ) { if (w->commit_queue[ j ]->feedback.has_value()) w->engineName = w->commit_queue[ j ]->feedback->vk_engine_name; if ( w->commit_queue[ j ]->commitID == commitID ) { gpuvis_trace_printf( "commit %lu done", w->commit_queue[ j ]->commitID ); w->commit_queue[ j ]->done = true; w->commit_queue[ j ]->earliest_present_time = earliestPresentTime; w->commit_queue[ j ]->present_margin = earliestPresentTime - earliestLatchTime; bFoundWindow = true; // Window just got a new available commit, determine if that's worth a repaint for ( auto &iter : g_VirtualConnectorFocuses ) { global_focus_t *pFocus = &iter.second; // If this is an overlay that we're presenting, repaint if ( w == pFocus->overlayWindow && w->opacity != TRANSLUCENT ) { hasRepaintNonBasePlane = true; } if ( w == pFocus->notificationWindow && w->opacity != TRANSLUCENT ) { hasRepaintNonBasePlane = true; } // matt: the performance overlay in Steam will interfere with VRR if we let this repaint. // This has been broken since the logic for external overlay repaints was moved out of // outdatedInteractiveFocus. It can cause displays to jump between the focused app's // refresh rate and the maximum panel refresh rate when presenting any type of overlay, // creating noticeable VRR flicker in the process. // TODO: fix this properly for all overlays, including Steam notifications and QAM // HACK: If VRR is active, prevent external overlays, i.e. mangoapp, from repainting the base plane if ( ( w == pFocus->externalOverlayWindow && w->opacity != TRANSLUCENT ) && ( GetBackend()->GetCurrentConnector() && !GetBackend()->GetCurrentConnector()->IsVRRActive() ) ) { hasRepaintNonBasePlane = true; } // If this is the main plane, repaint if ( w == pFocus->focusWindow && !w->isSteamStreamingClient ) { if ( !cv_paint_debug_pause_base_plane ) g_HeldCommits[ HELD_COMMIT_BASE ] = w->commit_queue[ j ]; hasRepaint = true; focusWindow_engine = w->engineName; } if ( w == pFocus->overrideWindow ) { hasRepaintNonBasePlane = true; } if ( w->isSteamStreamingClientVideo && pFocus->focusWindow && pFocus->focusWindow->isSteamStreamingClient ) { if ( !cv_paint_debug_pause_base_plane ) g_HeldCommits[ HELD_COMMIT_BASE ] = w->commit_queue[ j ]; hasRepaint = true; } } if ( w->outdatedInteractiveFocus ) { MakeFocusDirty(); w->outdatedInteractiveFocus = false; } break; } } if ( bFoundWindow == true ) { if ( j > 0 ) w->commit_queue.erase( w->commit_queue.begin(), w->commit_queue.begin() + j ); w->receivedDoneCommit = true; return true; } return false; } // TODO: Merge these two functions. void handle_done_commits_xwayland( xwayland_ctx_t *ctx, bool vblank, uint64_t vblank_idx ) { std::lock_guard lock( ctx->doneCommits.listCommitsDoneLock ); uint64_t next_refresh_time = g_SteamCompMgrVBlankTime.schedule.ulTargetVBlank; // commits that were not ready to be presented based on their display timing. static std::vector< CommitDoneEntry_t > commits_before_their_time; commits_before_their_time.clear(); commits_before_their_time.reserve( 32 ); // windows in FIFO mode we got a new frame to present for this vblank static std::unordered_set< uint64_t > fifo_win_seqs; fifo_win_seqs.clear(); fifo_win_seqs.reserve( 32 ); uint64_t now = get_time_in_nanos(); // very fast loop yes for ( auto& entry : ctx->doneCommits.listCommitsDone ) { bool entry_vblank = vblank; if ( GetBackend()->GetCurrentConnector() && GetBackend()->GetCurrentConnector()->IsVRRActive() ) { for ( steamcompmgr_win_t *w = ctx->list; w; w = w->xwayland().next ) { if (w->seq != entry.winSeq) continue; entry_vblank = entry_vblank && steamcompmgr_should_vblank_window( true, vblank_idx, w, now ); } } else { entry_vblank = entry_vblank && steamcompmgr_should_vblank_window( true, vblank_idx ); } if (entry.fifo && (!entry_vblank || fifo_win_seqs.count(entry.winSeq) > 0)) { commits_before_their_time.push_back( entry ); continue; } if (!entry.earliestPresentTime) { entry.earliestPresentTime = next_refresh_time; entry.earliestLatchTime = now; } if ( entry.desiredPresentTime > next_refresh_time ) { commits_before_their_time.push_back( entry ); continue; } for ( steamcompmgr_win_t *w = ctx->list; w; w = w->xwayland().next ) { if (w->seq != entry.winSeq) continue; if (handle_done_commit(w, ctx, entry.commitID, entry.earliestPresentTime, entry.earliestLatchTime)) { if (entry.fifo) fifo_win_seqs.insert(entry.winSeq); break; } } } ctx->doneCommits.listCommitsDone.swap( commits_before_their_time ); } void handle_done_commits_xdg( bool vblank, uint64_t vblank_idx ) { std::lock_guard lock( g_steamcompmgr_xdg_done_commits.listCommitsDoneLock ); uint64_t next_refresh_time = g_SteamCompMgrVBlankTime.schedule.ulTargetVBlank; // commits that were not ready to be presented based on their display timing. static std::vector< CommitDoneEntry_t > commits_before_their_time; commits_before_their_time.clear(); commits_before_their_time.reserve( 32 ); // windows in FIFO mode we got a new frame to present for this vblank static std::unordered_set< uint64_t > fifo_win_seqs; fifo_win_seqs.clear(); fifo_win_seqs.reserve( 32 ); uint64_t now = get_time_in_nanos(); vblank = vblank && steamcompmgr_should_vblank_window( true, vblank_idx ); // very fast loop yes for ( auto& entry : g_steamcompmgr_xdg_done_commits.listCommitsDone ) { if (entry.fifo && (!vblank || fifo_win_seqs.count(entry.winSeq) > 0)) { commits_before_their_time.push_back( entry ); continue; } if (!entry.earliestPresentTime) { entry.earliestPresentTime = next_refresh_time; entry.earliestLatchTime = now; } if ( entry.desiredPresentTime > next_refresh_time ) { commits_before_their_time.push_back( entry ); break; } for (const auto& xdg_win : g_steamcompmgr_xdg_wins) { if (xdg_win->seq != entry.winSeq) continue; if (handle_done_commit(xdg_win.get(), nullptr, entry.commitID, entry.earliestPresentTime, entry.earliestLatchTime)) { if (entry.fifo) fifo_win_seqs.insert(entry.winSeq); break; } } } g_steamcompmgr_xdg_done_commits.listCommitsDone.swap( commits_before_their_time ); } void handle_presented_for_window( steamcompmgr_win_t* w ) { // wlserver_lock is held. uint64_t next_refresh_time = g_SteamCompMgrVBlankTime.schedule.ulTargetVBlank; uint64_t refresh_cycle = g_nSteamCompMgrTargetFPS && steamcompmgr_window_should_limit_fps( w ) ? g_SteamCompMgrLimitedAppRefreshCycle : g_SteamCompMgrAppRefreshCycle; commit_t *lastCommit = get_window_last_done_commit_peek(w); if (lastCommit) { if (!lastCommit->presentation_feedbacks.empty() || lastCommit->present_id) { if (!lastCommit->presentation_feedbacks.empty()) { wlserver_presentation_feedback_presented( lastCommit->surf, lastCommit->presentation_feedbacks, next_refresh_time, refresh_cycle); } if (lastCommit->present_id) { wlserver_past_present_timing( lastCommit->surf, *lastCommit->present_id, lastCommit->desired_present_time, next_refresh_time, lastCommit->earliest_present_time, lastCommit->present_margin); lastCommit->present_id = std::nullopt; } } } if (struct wlr_surface *surface = w->current_surface()) { auto info = get_wl_surface_info(surface); if (info != nullptr && info->last_refresh_cycle != refresh_cycle) { // Could have got the override set in this bubble. surface = w->current_surface(); if (info->last_refresh_cycle != refresh_cycle) { info->last_refresh_cycle = refresh_cycle; wlserver_refresh_cycle(surface, refresh_cycle); } } } } void handle_presented_xwayland( xwayland_ctx_t *ctx ) { for ( steamcompmgr_win_t *w = ctx->list; w; w = w->xwayland().next ) { handle_presented_for_window(w); } } void handle_presented_xdg() { for (const auto& xdg_win : g_steamcompmgr_xdg_wins) { handle_presented_for_window(xdg_win.get()); } } void nudge_steamcompmgr( void ) { g_SteamCompMgrWaiter.Nudge(); } void force_repaint( void ) { g_bForceRepaint = true; nudge_steamcompmgr(); } struct TempUpscaleImage_t { gamescope::OwningRc pTexture; // Timeline of upscale -> release, to be used as acquire for the commit. std::shared_ptr pReleaseTimeline; uint64_t ulLastPoint = 0ul; }; static std::vector g_pUpscaleImages; void ClearUpscaleImages() { g_pUpscaleImages.clear(); } static TempUpscaleImage_t *GetTempUpscaleImage( uint32_t uWidth, uint32_t uHeight, uint32_t uDrmFormat ) { if ( g_pUpscaleImages.size() ) { // Mixing and matching sizes to only do the min required would be nice // but massively complicates caching. if ( g_pUpscaleImages[0].pTexture->width() != uWidth || g_pUpscaleImages[0].pTexture->height() != uHeight || g_pUpscaleImages[0].pTexture->drmFormat() != uDrmFormat ) { g_pUpscaleImages.clear(); } } for ( TempUpscaleImage_t &image : g_pUpscaleImages ) { if ( !image.pTexture->IsInUse() ) return ℑ } if ( g_pUpscaleImages.size() > 8 ) { xwm_log.warnf( "No upscale images free!\n" ); return {}; } gamescope::OwningRc pTexture = new CVulkanTexture(); std::shared_ptr pTimeline = gamescope::CTimeline::Create(); if ( !pTimeline ) return nullptr; CVulkanTexture::createFlags imageFlags; imageFlags.bSampled = true; imageFlags.bStorage = true; imageFlags.bFlippable = true; pTexture->BInit( g_nOutputWidth, g_nOutputHeight, 1, uDrmFormat, imageFlags ); TempUpscaleImage_t &image = g_pUpscaleImages.emplace_back( std::move( pTexture ), std::move( pTimeline ) ); return ℑ } gamescope::ConVar cv_surface_update_force_only_current_surface( "surface_update_force_only_current_surface", false, "Force updates to apply only to the current surface, ignoring commits for other surfaces." ); void update_wayland_res(CommitDoneList_t *doneCommits, steamcompmgr_win_t *w, ResListEntry_t& reslistentry) { struct wlr_buffer *buf = reslistentry.buf; if ( w == nullptr ) { wlserver_lock(); wlr_buffer_unlock( buf ); wlserver_unlock(); // Make sure to send the discarded event if we hit this // to ensure forward progress. if (!reslistentry.presentation_feedbacks.empty()) { wlserver_presentation_feedback_discard( reslistentry.surf, reslistentry.presentation_feedbacks ); // presentation_feedbacks cleared by wlserver_presentation_feedback_discard } xwm_log.errorf( "waylandres but no win" ); return; } // If we ever use HDR on the surface, only ever accept flip commits from the WSI layer. if ( reslistentry.feedback && reslistentry.feedback->vk_colorspace != VK_COLOR_SPACE_SRGB_NONLINEAR_KHR ) { w->bHasHadNonSRGBColorSpace = true; } // If there are random commits that are really thin/small when we have the WSI layer active ever, // let's just ignore these as they are probably bogus commits from glamor. bool bPossiblyBogus = reslistentry.buf->width <= 2 || reslistentry.buf->height <= 2; // If the buffer has no damage, always prefer our override surface. bool bHasDamage = ( reslistentry.surf->buffer_damage.extents.x2 - reslistentry.surf->buffer_damage.extents.x1 ) > 2 && ( reslistentry.surf->buffer_damage.extents.y2 - reslistentry.surf->buffer_damage.extents.y1 ) > 2; // If we have an override surface, make sure this commit is for the current surface // or if the commit is probably bogus. bool bOnlyCurrentSurface = w->bHasHadNonSRGBColorSpace || bPossiblyBogus || !bHasDamage || cv_surface_update_force_only_current_surface; bool for_current_surface = !w->override_surface() || w->current_surface() == reslistentry.surf; if ( !for_current_surface ) { xwm_log.debugf( "Got commit not for current surface." ); } if ( bOnlyCurrentSurface && !for_current_surface ) { wlserver_lock(); wlr_buffer_unlock( buf ); wlserver_unlock(); w->receivedDoneCommit = true; return; } bool already_exists = false; for ( const auto& existing_commit : w->commit_queue ) { if (existing_commit->buf == buf) already_exists = true; } if ( already_exists && !reslistentry.feedback && reslistentry.presentation_feedbacks.empty() ) { wlserver_lock(); wlr_buffer_unlock( buf ); wlserver_unlock(); xwm_log.warnf( "got the same buffer committed twice, ignoring." ); // If we have a duplicated commit + frame callback, ensure that is signalled. // This matches Mutter and Weston behavior, so it's plausible that some application relies on forward progress. // We're essentially discarding the commit here, so consider it complete right away. w->receivedDoneCommit = true; return; } gamescope::Rc newCommit = import_commit( w, reslistentry.surf, buf, reslistentry.async, std::move(reslistentry.feedback), std::move(reslistentry.presentation_feedbacks), reslistentry.present_id, reslistentry.desired_present_time, reslistentry.fifo ); int fence = -1; if ( newCommit != nullptr ) { global_focus_t *pCurrentFocus = GetCurrentFocus(); static bool bMangoappSocketDisable = env_to_bool( getenv( "GAMESCOPE_MANGOAPP_SOCKET_DISABLE" )); // Whether or not to nudge mango app when this commit is done. const bool mango_nudge = pCurrentFocus && ( ( w == pCurrentFocus->focusWindow && !w->isSteamStreamingClient ) || ( pCurrentFocus->focusWindow && pCurrentFocus->focusWindow->isSteamStreamingClient && w->isSteamStreamingClientVideo ) ) && !bMangoappSocketDisable; bool bValidPreemptiveScale = reslistentry.pAcquirePoint && pCurrentFocus && w == pCurrentFocus->focusWindow && cv_upscale_preemptive; bool bPreemptiveUpscale = bValidPreemptiveScale && newCommit->ShouldPreemptivelyUpscale(); bool bKnownReady = false; std::pair eventFd = gamescope::CAcquireTimelinePoint::k_InvalidEvent; if ( bPreemptiveUpscale ) { FrameInfo_t upscaledFrameInfo{}; upscaledFrameInfo.applyOutputColorMgmt = true; upscaledFrameInfo.outputEncodingEOTF = ( newCommit->colorspace() == GAMESCOPE_APP_TEXTURE_COLORSPACE_LINEAR || newCommit->colorspace() == GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB ) ? EOTF_Gamma22 : EOTF_PQ; float flOldGlobalScale = globalScaleRatio; float flOldZoomScale = zoomScaleRatio; float flOldOverscanScale = overscanScaleRatio; overscanScaleRatio = 1.0f; zoomScaleRatio = 1.0f; globalScaleRatio = 1.0f; paint_window_commit( newCommit, w, w, &upscaledFrameInfo, nullptr ); upscaledFrameInfo.useFSRLayer0 = g_upscaleFilter == GamescopeUpscaleFilter::FSR; upscaledFrameInfo.useNISLayer0 = g_upscaleFilter == GamescopeUpscaleFilter::NIS; globalScaleRatio = flOldGlobalScale; zoomScaleRatio = flOldZoomScale; flOldOverscanScale = flOldOverscanScale; TempUpscaleImage_t *pTempImage = GetTempUpscaleImage( g_nOutputWidth, g_nOutputHeight, g_output.uOutputFormat ); if ( pTempImage ) { const uint64_t ulNextReleasePoint = ++pTempImage->ulLastPoint; std::unique_ptr pCommandBuffer = g_device.commandBuffer(); pCommandBuffer->AddDependency( reslistentry.pAcquirePoint->GetTimeline()->ToVkSemaphore(), reslistentry.pAcquirePoint->GetPoint() ); pCommandBuffer->AddSignal( pTempImage->pReleaseTimeline->ToVkSemaphore(), ulNextReleasePoint ); static std::optional s_ulLastPreemptiveUpscaleSeqNo; if ( s_ulLastPreemptiveUpscaleSeqNo ) { vulkan_wait( *s_ulLastPreemptiveUpscaleSeqNo, true ); } std::optional seqNo = vulkan_composite( &upscaledFrameInfo, nullptr, false, pTempImage->pTexture, false, std::move( pCommandBuffer ) ); if ( cv_upscale_preemptive_debug_force_sync ) { vulkan_wait( *seqNo, true ); } s_ulLastPreemptiveUpscaleSeqNo = seqNo; newCommit->upscaledTexture = std::optional { std::in_place_t{}, g_upscaleFilter, g_upscaleScaler, g_nOutputWidth, g_nOutputHeight, pTempImage->pTexture, upscaledFrameInfo.outputEncodingEOTF == EOTF_Gamma22 ? VK_COLOR_SPACE_SRGB_NONLINEAR_KHR : VK_COLOR_SPACE_HDR10_ST2084_EXT, }; // Manifest a new acquire timeline point with this inline work. eventFd = gamescope::CAcquireTimelinePoint( pTempImage->pReleaseTimeline, ulNextReleasePoint ).CreateEventFd(); //xwm_log.infof( "Pre-emptively upscaling!" ); } else { bPreemptiveUpscale = false; } } if ( !bPreemptiveUpscale ) { if ( bValidPreemptiveScale ) { ClearUpscaleImages(); } if ( reslistentry.pAcquirePoint ) { eventFd = reslistentry.pAcquirePoint->CreateEventFd(); } } if ( gamescope::IBackendFb *pBackendFb = newCommit->vulkanTex->GetBackendFb() ) { if ( reslistentry.pReleasePoint ) pBackendFb->SetReleasePoint( reslistentry.pReleasePoint ); else pBackendFb->SetBuffer( buf ); } if ( eventFd != gamescope::CAcquireTimelinePoint::k_InvalidEvent ) { fence = eventFd.first; bKnownReady = eventFd.second; } else { struct wlr_dmabuf_attributes dmabuf = {0}; if ( wlr_buffer_get_dmabuf( buf, &dmabuf ) ) { fence = dup( dmabuf.fd[0] ); } else { fence = newCommit->vulkanTex->memoryFence(); } } gpuvis_trace_printf( "pushing wait for commit %lu win %lx", newCommit->commitID, w->type == steamcompmgr_win_type_t::XWAYLAND ? w->xwayland().id : 0 ); { newCommit->SetFence( fence, mango_nudge, doneCommits ); if ( bKnownReady ) newCommit->Signal(); else g_ImageWaiter.AddWaitable( newCommit.get() ); } w->commit_queue.push_back( std::move(newCommit) ); } } void check_new_xwayland_res(xwayland_ctx_t *ctx) { // When importing buffer, we'll potentially need to perform operations with // a wlserver lock (e.g. wlr_buffer_lock). We can't do this with a // wayland_commit_queue lock because that causes deadlocks. std::vector& tmp_queue = ctx->xwayland_server->retrieve_commits(); for ( uint32_t i = 0; i < tmp_queue.size(); i++ ) { steamcompmgr_win_t *w = find_win( ctx, tmp_queue[ i ].surf ); update_wayland_res( &ctx->doneCommits, w, tmp_queue[ i ]); } } void check_new_xdg_res() { std::vector tmp_queue = wlserver_xdg_commit_queue(); for ( uint32_t i = 0; i < tmp_queue.size(); i++ ) { for ( const auto& xdg_win : g_steamcompmgr_xdg_wins ) { if ( xdg_win->xdg().surface.main_surface == tmp_queue[ i ].surf ) { update_wayland_res( &g_steamcompmgr_xdg_done_commits, xdg_win.get(), tmp_queue[ i ] ); break; } } } } static void handle_xfixes_selection_notify( xwayland_ctx_t *ctx, XFixesSelectionNotifyEvent *event ) { if (event->owner == ctx->ourWindow) { return; } XConvertSelection(ctx->dpy, event->selection, ctx->atoms.utf8StringAtom, event->selection, ctx->ourWindow, CurrentTime); XFlush(ctx->dpy); } void xwayland_ctx_t::Dispatch() { xwayland_ctx_t *ctx = this; MouseCursor *cursor = ctx->cursor.get(); bool bSetFocus = false; while (XPending(ctx->dpy)) { XEvent ev; int ret = XNextEvent(ctx->dpy, &ev); if (ret != 0) { xwm_log.errorf("XNextEvent failed"); break; } if (debugEvents) { gpuvis_trace_printf("event %d", ev.type); printf("event %d\n", ev.type); } switch (ev.type) { case CreateNotify: if (ev.xcreatewindow.parent == ctx->root) add_win(ctx, ev.xcreatewindow.window, 0, ev.xcreatewindow.serial); break; case ConfigureNotify: configure_win(ctx, &ev.xconfigure); break; case DestroyNotify: { steamcompmgr_win_t * w = find_win(ctx, ev.xdestroywindow.window); if (w && w->xwayland().id == ev.xdestroywindow.window) destroy_win(ctx, ev.xdestroywindow.window, true, true); break; } case MapNotify: { steamcompmgr_win_t * w = find_win(ctx, ev.xmap.window); if (w && w->xwayland().id == ev.xmap.window) map_win(ctx, ev.xmap.window, ev.xmap.serial); break; } case UnmapNotify: { steamcompmgr_win_t * w = find_win(ctx, ev.xunmap.window); if (w && w->xwayland().id == ev.xunmap.window) unmap_win(ctx, ev.xunmap.window, true); break; } case FocusOut: { steamcompmgr_win_t * w = find_win( ctx, ev.xfocus.window ); // If focus escaped the current desired keyboard focus window, check where it went if ( w && w->xwayland().id == ctx->currentKeyboardFocusWindow ) { Window newKeyboardFocus = None; int nRevertMode = 0; XGetInputFocus( ctx->dpy, &newKeyboardFocus, &nRevertMode ); // Find window or its toplevel parent steamcompmgr_win_t *kbw = find_win( ctx, newKeyboardFocus ); if ( kbw ) { if ( kbw->xwayland().id == ctx->currentKeyboardFocusWindow ) { // focus went to a child, this is fine, make note of it in case we need to fix it ctx->currentKeyboardFocusWindow = newKeyboardFocus; } else { // focus went elsewhere, correct it bSetFocus = true; } } } break; } case ReparentNotify: if (ev.xreparent.parent == ctx->root) add_win(ctx, ev.xreparent.window, 0, ev.xreparent.serial); else { steamcompmgr_win_t * w = find_win(ctx, ev.xreparent.window); if (w && w->xwayland().id == ev.xreparent.window) { destroy_win(ctx, ev.xreparent.window, false, true); } else { // If something got reparented _to_ a toplevel window, // go check for the fullscreen workaround again. w = find_win(ctx, ev.xreparent.parent); if (w) { get_size_hints(ctx, w); MakeFocusDirty(); } } } break; case CirculateNotify: circulate_win(ctx, &ev.xcirculate); break; case MapRequest: map_request(ctx, &ev.xmaprequest); break; case ConfigureRequest: configure_request(ctx, &ev.xconfigurerequest); break; case CirculateRequest: circulate_request(ctx, &ev.xcirculaterequest); break; case Expose: break; case PropertyNotify: handle_property_notify(ctx, &ev.xproperty); break; case ClientMessage: handle_client_message(ctx, &ev.xclient); break; case LeaveNotify: break; case SelectionNotify: handle_selection_notify(ctx, &ev.xselection); break; case SelectionRequest: handle_selection_request(ctx, &ev.xselectionrequest); break; default: if (ev.type == ctx->damage_event + XDamageNotify) { damage_win(ctx, (XDamageNotifyEvent *) &ev); } else if (ev.type == ctx->xfixes_event + XFixesCursorNotify) { cursor->setDirty(); } else if (ev.type == ctx->xfixes_event + XFixesSelectionNotify) { handle_xfixes_selection_notify(ctx, (XFixesSelectionNotifyEvent *) &ev); } break; } XFlush(ctx->dpy); } if ( bSetFocus ) { XSetInputFocus(ctx->dpy, ctx->currentKeyboardFocusWindow, RevertToNone, CurrentTime); } } struct rgba_t { uint8_t r,g,b,a; }; static bool load_mouse_cursor( MouseCursor *cursor, const char *path, int hx, int hy ) { int w, h, channels; rgba_t *data = (rgba_t *) stbi_load(path, &w, &h, &channels, STBI_rgb_alpha); if (!data) { xwm_log.errorf("Failed to open/load cursor file"); return false; } std::transform(data, data + w * h, data, [](rgba_t x) { if (x.a == 0) return rgba_t{}; return rgba_t{ uint8_t((x.b * x.a) / 255), uint8_t((x.g * x.a) / 255), uint8_t((x.r * x.a) / 255), x.a }; }); // Data is freed by XDestroyImage in setCursorImage. return cursor->setCursorImage((char *)data, w, h, hx, hy); } const char* g_customCursorPath = nullptr; int g_customCursorHotspotX = 0; int g_customCursorHotspotY = 0; xwayland_ctx_t g_ctx; static bool setup_error_handlers = false; void init_xwayland_ctx(uint32_t serverId, gamescope_xwayland_server_t *xwayland_server) { assert(!xwayland_server->ctx); xwayland_server->ctx = std::make_unique(); xwayland_ctx_t *ctx = xwayland_server->ctx.get(); int composite_major, composite_minor; int xres_major, xres_minor; ctx->xwayland_server = xwayland_server; ctx->dpy = xwayland_server->get_xdisplay(); if (!ctx->dpy) { xwm_log.errorf("Can't open display"); exit(1); } if (!setup_error_handlers) { XSetErrorHandler(error); XSetIOErrorHandler(handle_io_error); setup_error_handlers = true; } if (synchronize) XSynchronize(ctx->dpy, 1); ctx->scr = DefaultScreen(ctx->dpy); ctx->root = RootWindow(ctx->dpy, ctx->scr); if (!XRenderQueryExtension(ctx->dpy, &ctx->render_event, &ctx->render_error)) { xwm_log.errorf("No render extension"); exit(1); } if (!XQueryExtension(ctx->dpy, COMPOSITE_NAME, &ctx->composite_opcode, &ctx->composite_event, &ctx->composite_error)) { xwm_log.errorf("No composite extension"); exit(1); } XCompositeQueryVersion(ctx->dpy, &composite_major, &composite_minor); if (!XDamageQueryExtension(ctx->dpy, &ctx->damage_event, &ctx->damage_error)) { xwm_log.errorf("No damage extension"); exit(1); } if (!XFixesQueryExtension(ctx->dpy, &ctx->xfixes_event, &ctx->xfixes_error)) { xwm_log.errorf("No XFixes extension"); exit(1); } if (!XShapeQueryExtension(ctx->dpy, &ctx->xshape_event, &ctx->xshape_error)) { xwm_log.errorf("No XShape extension"); exit(1); } if (!XFixesQueryExtension(ctx->dpy, &ctx->xfixes_event, &ctx->xfixes_error)) { xwm_log.errorf("No XFixes extension"); exit(1); } if (!XResQueryVersion(ctx->dpy, &xres_major, &xres_minor)) { xwm_log.errorf("No XRes extension"); exit(1); } if (xres_major != 1 || xres_minor < 2) { xwm_log.errorf("Unsupported XRes version: have %d.%d, want 1.2", xres_major, xres_minor); exit(1); } if (!XQueryExtension(ctx->dpy, "XInputExtension", &ctx->xinput_opcode, &ctx->xinput_event, &ctx->xinput_error)) { xwm_log.errorf("No XInput extension"); exit(1); } int xi_major = 2; int xi_minor = 0; XIQueryVersion(ctx->dpy, &xi_major, &xi_minor); if (!register_cm(ctx)) { exit(1); } register_systray(ctx); /* get atoms */ ctx->atoms.steamAtom = XInternAtom(ctx->dpy, STEAM_PROP, false); ctx->atoms.steamInputFocusAtom = XInternAtom(ctx->dpy, "STEAM_INPUT_FOCUS", false); ctx->atoms.steamTouchClickModeAtom = XInternAtom(ctx->dpy, "STEAM_TOUCH_CLICK_MODE", false); ctx->atoms.gameAtom = XInternAtom(ctx->dpy, GAME_PROP, false); ctx->atoms.overlayAtom = XInternAtom(ctx->dpy, OVERLAY_PROP, false); ctx->atoms.externalOverlayAtom = XInternAtom(ctx->dpy, EXTERNAL_OVERLAY_PROP, false); ctx->atoms.opacityAtom = XInternAtom(ctx->dpy, OPACITY_PROP, false); ctx->atoms.gamesRunningAtom = XInternAtom(ctx->dpy, GAMES_RUNNING_PROP, false); ctx->atoms.screenScaleAtom = XInternAtom(ctx->dpy, SCREEN_SCALE_PROP, false); ctx->atoms.screenZoomAtom = XInternAtom(ctx->dpy, SCREEN_MAGNIFICATION_PROP, false); ctx->atoms.winTypeAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE", false); ctx->atoms.winDesktopAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_DESKTOP", false); ctx->atoms.winDockAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_DOCK", false); ctx->atoms.winToolbarAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_TOOLBAR", false); ctx->atoms.winMenuAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_MENU", false); ctx->atoms.winUtilAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_UTILITY", false); ctx->atoms.winSplashAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_SPLASH", false); ctx->atoms.winDialogAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_DIALOG", false); ctx->atoms.winNormalAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_NORMAL", false); ctx->atoms.sizeHintsAtom = XInternAtom(ctx->dpy, "WM_NORMAL_HINTS", false); ctx->atoms.netWMStateFullscreenAtom = XInternAtom(ctx->dpy, "_NET_WM_STATE_FULLSCREEN", false); ctx->atoms.activeWindowAtom = XInternAtom(ctx->dpy, "_NET_ACTIVE_WINDOW", false); ctx->atoms.netWMStateAtom = XInternAtom(ctx->dpy, "_NET_WM_STATE", false); ctx->atoms.WMTransientForAtom = XInternAtom(ctx->dpy, "WM_TRANSIENT_FOR", false); ctx->atoms.netWMStateHiddenAtom = XInternAtom(ctx->dpy, "_NET_WM_STATE_HIDDEN", false); ctx->atoms.netWMStateFocusedAtom = XInternAtom(ctx->dpy, "_NET_WM_STATE_FOCUSED", false); ctx->atoms.netWMStateSkipTaskbarAtom = XInternAtom(ctx->dpy, "_NET_WM_STATE_SKIP_TASKBAR", false); ctx->atoms.netWMStateSkipPagerAtom = XInternAtom(ctx->dpy, "_NET_WM_STATE_SKIP_PAGER", false); ctx->atoms.WLSurfaceIDAtom = XInternAtom(ctx->dpy, "WL_SURFACE_ID", false); ctx->atoms.WMStateAtom = XInternAtom(ctx->dpy, "WM_STATE", false); ctx->atoms.utf8StringAtom = XInternAtom(ctx->dpy, "UTF8_STRING", false); ctx->atoms.netWMNameAtom = XInternAtom(ctx->dpy, "_NET_WM_NAME", false); ctx->atoms.netWMIcon = XInternAtom(ctx->dpy, "_NET_WM_ICON", false); ctx->atoms.netSystemTrayOpcodeAtom = XInternAtom(ctx->dpy, "_NET_SYSTEM_TRAY_OPCODE", false); ctx->atoms.steamStreamingClientAtom = XInternAtom(ctx->dpy, "STEAM_STREAMING_CLIENT", false); ctx->atoms.steamStreamingClientVideoAtom = XInternAtom(ctx->dpy, "STEAM_STREAMING_CLIENT_VIDEO", false); ctx->atoms.gamescopeFocusableAppsAtom = XInternAtom(ctx->dpy, "GAMESCOPE_FOCUSABLE_APPS", false); ctx->atoms.gamescopeFocusableWindowsAtom = XInternAtom(ctx->dpy, "GAMESCOPE_FOCUSABLE_WINDOWS", false); ctx->atoms.gamescopeFocusedAppAtom = XInternAtom( ctx->dpy, "GAMESCOPE_FOCUSED_APP", false ); ctx->atoms.gamescopeFocusedAppGfxAtom = XInternAtom( ctx->dpy, "GAMESCOPE_FOCUSED_APP_GFX", false ); ctx->atoms.gamescopeFocusedWindowAtom = XInternAtom( ctx->dpy, "GAMESCOPE_FOCUSED_WINDOW", false ); ctx->atoms.gamescopeCtrlAppIDAtom = XInternAtom(ctx->dpy, "GAMESCOPECTRL_BASELAYER_APPID", false); ctx->atoms.gamescopeCtrlWindowAtom = XInternAtom(ctx->dpy, "GAMESCOPECTRL_BASELAYER_WINDOW", false); ctx->atoms.WMChangeStateAtom = XInternAtom(ctx->dpy, "WM_CHANGE_STATE", false); ctx->atoms.gamescopeInputCounterAtom = XInternAtom(ctx->dpy, "GAMESCOPE_INPUT_COUNTER", false); ctx->atoms.gamescopeScreenShotAtom = XInternAtom( ctx->dpy, "GAMESCOPECTRL_REQUEST_SCREENSHOT", false ); ctx->atoms.gamescopeDebugScreenShotAtom = XInternAtom( ctx->dpy, "GAMESCOPECTRL_DEBUG_REQUEST_SCREENSHOT", false ); ctx->atoms.gamescopeFocusDisplay = XInternAtom(ctx->dpy, "GAMESCOPE_FOCUS_DISPLAY", false); ctx->atoms.gamescopeMouseFocusDisplay = XInternAtom(ctx->dpy, "GAMESCOPE_MOUSE_FOCUS_DISPLAY", false); ctx->atoms.gamescopeKeyboardFocusDisplay = XInternAtom( ctx->dpy, "GAMESCOPE_KEYBOARD_FOCUS_DISPLAY", false ); // In nanoseconds... ctx->atoms.gamescopeTuneableVBlankRedZone = XInternAtom( ctx->dpy, "GAMESCOPE_TUNEABLE_VBLANK_REDZONE", false ); ctx->atoms.gamescopeTuneableRateOfDecay = XInternAtom( ctx->dpy, "GAMESCOPE_TUNEABLE_VBLANK_RATE_OF_DECAY_PERCENTAGE", false ); ctx->atoms.gamescopeScalingFilter = XInternAtom( ctx->dpy, "GAMESCOPE_SCALING_FILTER", false ); ctx->atoms.gamescopeFSRSharpness = XInternAtom( ctx->dpy, "GAMESCOPE_FSR_SHARPNESS", false ); ctx->atoms.gamescopeSharpness = XInternAtom( ctx->dpy, "GAMESCOPE_SHARPNESS", false ); ctx->atoms.gamescopeXWaylandModeControl = XInternAtom( ctx->dpy, "GAMESCOPE_XWAYLAND_MODE_CONTROL", false ); ctx->atoms.gamescopeFPSLimit = XInternAtom( ctx->dpy, "GAMESCOPE_FPS_LIMIT", false ); ctx->atoms.gamescopeDynamicRefresh[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL] = XInternAtom( ctx->dpy, "GAMESCOPE_DYNAMIC_REFRESH", false ); ctx->atoms.gamescopeDynamicRefresh[gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL] = XInternAtom( ctx->dpy, "GAMESCOPE_DYNAMIC_REFRESH_EXTERNAL", false ); ctx->atoms.gamescopeLowLatency = XInternAtom( ctx->dpy, "GAMESCOPE_LOW_LATENCY", false ); ctx->atoms.gamescopeFSRFeedback = XInternAtom( ctx->dpy, "GAMESCOPE_FSR_FEEDBACK", false ); ctx->atoms.gamescopeBlurMode = XInternAtom( ctx->dpy, "GAMESCOPE_BLUR_MODE", false ); ctx->atoms.gamescopeBlurRadius = XInternAtom( ctx->dpy, "GAMESCOPE_BLUR_RADIUS", false ); ctx->atoms.gamescopeBlurFadeDuration = XInternAtom( ctx->dpy, "GAMESCOPE_BLUR_FADE_DURATION", false ); ctx->atoms.gamescopeCompositeForce = XInternAtom( ctx->dpy, "GAMESCOPE_COMPOSITE_FORCE", false ); ctx->atoms.gamescopeCompositeDebug = XInternAtom( ctx->dpy, "GAMESCOPE_COMPOSITE_DEBUG", false ); ctx->atoms.gamescopeAllowTearing = XInternAtom( ctx->dpy, "GAMESCOPE_ALLOW_TEARING", false ); ctx->atoms.gamescopeDisplayForceInternal = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_FORCE_INTERNAL", false ); ctx->atoms.gamescopeDisplayModeNudge = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_MODE_NUDGE", false ); ctx->atoms.gamescopeDisplayIsExternal = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_IS_EXTERNAL", false ); ctx->atoms.gamescopeDisplayModeListExternal = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_MODE_LIST_EXTERNAL", false ); ctx->atoms.gamescopeCursorVisibleFeedback = XInternAtom( ctx->dpy, "GAMESCOPE_CURSOR_VISIBLE_FEEDBACK", false ); ctx->atoms.gamescopeSteamMaxHeight = XInternAtom( ctx->dpy, "GAMESCOPE_STEAM_MAX_HEIGHT", false ); ctx->atoms.gamescopeVRREnabled = XInternAtom( ctx->dpy, "GAMESCOPE_VRR_ENABLED", false ); ctx->atoms.gamescopeVRRCapable = XInternAtom( ctx->dpy, "GAMESCOPE_VRR_CAPABLE", false ); ctx->atoms.gamescopeVRRInUse = XInternAtom( ctx->dpy, "GAMESCOPE_VRR_FEEDBACK", false ); ctx->atoms.gamescopeNewScalingFilter = XInternAtom( ctx->dpy, "GAMESCOPE_NEW_SCALING_FILTER", false ); ctx->atoms.gamescopeNewScalingScaler = XInternAtom( ctx->dpy, "GAMESCOPE_NEW_SCALING_SCALER", false ); ctx->atoms.gamescopeDisplayEdidPath = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_EDID_PATH", false ); ctx->atoms.gamescopeXwaylandServerId = XInternAtom( ctx->dpy, "GAMESCOPE_XWAYLAND_SERVER_ID", false ); ctx->atoms.gamescopeDisplaySupportsHDR = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_SUPPORTS_HDR", false ); ctx->atoms.gamescopeDisplayHDREnabled = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_HDR_ENABLED", false ); ctx->atoms.gamescopeDebugForceHDR10Output = XInternAtom( ctx->dpy, "GAMESCOPE_DEBUG_FORCE_HDR10_PQ_OUTPUT", false ); ctx->atoms.gamescopeDebugForceHDRSupport = XInternAtom( ctx->dpy, "GAMESCOPE_DEBUG_FORCE_HDR_SUPPORT", false ); ctx->atoms.gamescopeDebugHDRHeatmap = XInternAtom( ctx->dpy, "GAMESCOPE_DEBUG_HDR_HEATMAP", false ); ctx->atoms.gamescopeHDROutputFeedback = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_OUTPUT_FEEDBACK", false ); ctx->atoms.gamescopeSDROnHDRContentBrightness = XInternAtom( ctx->dpy, "GAMESCOPE_SDR_ON_HDR_CONTENT_BRIGHTNESS", false ); ctx->atoms.gamescopeHDRInputGain = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_INPUT_GAIN", false ); ctx->atoms.gamescopeSDRInputGain = XInternAtom( ctx->dpy, "GAMESCOPE_SDR_INPUT_GAIN", false ); ctx->atoms.gamescopeHDRItmEnable = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_ITM_ENABLE", false ); ctx->atoms.gamescopeHDRItmSDRNits = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_ITM_SDR_NITS", false ); ctx->atoms.gamescopeHDRItmTargetNits = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_ITM_TARGET_NITS", false ); ctx->atoms.gamescopeColorLookPQ = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_LOOK_PQ", false ); ctx->atoms.gamescopeColorLookG22 = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_LOOK_G22", false ); ctx->atoms.gamescopeColorOutputVirtualWhite = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_VIRTUAL_WHITE", false ); ctx->atoms.gamescopeHDRTonemapDisplayMetadata = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_TONEMAP_DISPLAY_METADATA", false ); ctx->atoms.gamescopeHDRTonemapSourceMetadata = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_TONEMAP_SOURCE_METADATA", false ); ctx->atoms.gamescopeHDRTonemapOperator = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_TONEMAP_OPERATOR", false ); ctx->atoms.gamescopeForceWindowsFullscreen = XInternAtom( ctx->dpy, "GAMESCOPE_FORCE_WINDOWS_FULLSCREEN", false ); ctx->atoms.gamescopeColorLut3DOverride = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_3DLUT_OVERRIDE", false ); ctx->atoms.gamescopeColorShaperLutOverride = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_SHAPERLUT_OVERRIDE", false ); ctx->atoms.gamescopeColorSDRGamutWideness = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_SDR_GAMUT_WIDENESS", false ); ctx->atoms.gamescopeColorNightMode = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_NIGHT_MODE", false ); ctx->atoms.gamescopeColorManagementDisable = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_MANAGEMENT_DISABLE", false ); ctx->atoms.gamescopeColorAppWantsHDRFeedback = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_APP_WANTS_HDR_FEEDBACK", false ); ctx->atoms.gamescopeColorAppHDRMetadataFeedback = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_APP_HDR_METADATA_FEEDBACK", false ); ctx->atoms.gamescopeColorSliderInUse = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_MANAGEMENT_CHANGING_HINT", false ); ctx->atoms.gamescopeColorChromaticAdaptationMode = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_CHROMATIC_ADAPTATION_MODE", false ); ctx->atoms.gamescopeColorMuraCorrectionImage[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL] = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_MURA_CORRECTION_IMAGE", false ); ctx->atoms.gamescopeColorMuraCorrectionImage[gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL] = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_MURA_CORRECTION_IMAGE_EXTERNAL", false ); ctx->atoms.gamescopeColorMuraScale[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL] = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_MURA_SCALE", false ); ctx->atoms.gamescopeColorMuraScale[gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL] = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_MURA_SCALE_EXTERNAL", false ); ctx->atoms.gamescopeColorMuraCorrectionDisabled[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL] = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_MURA_CORRECTION_DISABLED", false ); ctx->atoms.gamescopeColorMuraCorrectionDisabled[gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL] = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_MURA_CORRECTION_DISABLED_EXTERNAL", false ); ctx->atoms.gamescopeCreateXWaylandServer = XInternAtom( ctx->dpy, "GAMESCOPE_CREATE_XWAYLAND_SERVER", false ); ctx->atoms.gamescopeCreateXWaylandServerFeedback = XInternAtom( ctx->dpy, "GAMESCOPE_CREATE_XWAYLAND_SERVER_FEEDBACK", false ); ctx->atoms.gamescopeDestroyXWaylandServer = XInternAtom( ctx->dpy, "GAMESCOPE_DESTROY_XWAYLAND_SERVER", false ); ctx->atoms.gamescopeReshadeEffect = XInternAtom( ctx->dpy, "GAMESCOPE_RESHADE_EFFECT", false ); ctx->atoms.gamescopeReshadeTechniqueIdx = XInternAtom( ctx->dpy, "GAMESCOPE_RESHADE_TECHNIQUE_IDX", false ); ctx->atoms.gamescopeDisplayRefreshRateFeedback = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_REFRESH_RATE_FEEDBACK", false ); ctx->atoms.gamescopeDisplayDynamicRefreshBasedOnGamePresence = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_DYNAMIC_REFRESH_BASED_ON_GAME_PRESENCE", false ); ctx->atoms.wineHwndStyle = XInternAtom( ctx->dpy, "_WINE_HWND_STYLE", false ); ctx->atoms.wineHwndStyleEx = XInternAtom( ctx->dpy, "_WINE_HWND_EXSTYLE", false ); ctx->atoms.clipboard = XInternAtom(ctx->dpy, "CLIPBOARD", false); ctx->atoms.primarySelection = XInternAtom(ctx->dpy, "PRIMARY", false); ctx->atoms.targets = XInternAtom(ctx->dpy, "TARGETS", false); ctx->root_width = DisplayWidth(ctx->dpy, ctx->scr); ctx->root_height = DisplayHeight(ctx->dpy, ctx->scr); ctx->allDamage = None; ctx->clipChanged = true; XChangeProperty(ctx->dpy, ctx->root, ctx->atoms.gamescopeXwaylandServerId, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&serverId, 1 ); XGrabServer(ctx->dpy); XCompositeRedirectSubwindows(ctx->dpy, ctx->root, CompositeRedirectManual); Window root_return, parent_return; Window *children; unsigned int nchildren; XSelectInput(ctx->dpy, ctx->root, SubstructureNotifyMask| ExposureMask| StructureNotifyMask| SubstructureRedirectMask| FocusChangeMask| PointerMotionMask| LeaveWindowMask| PropertyChangeMask); XShapeSelectInput(ctx->dpy, ctx->root, ShapeNotifyMask); XFixesSelectCursorInput(ctx->dpy, ctx->root, XFixesDisplayCursorNotifyMask); XFixesSelectSelectionInput(ctx->dpy, ctx->root, ctx->atoms.clipboard, XFixesSetSelectionOwnerNotifyMask); XFixesSelectSelectionInput(ctx->dpy, ctx->root, ctx->atoms.primarySelection, XFixesSetSelectionOwnerNotifyMask); XQueryTree(ctx->dpy, ctx->root, &root_return, &parent_return, &children, &nchildren); for (uint32_t i = 0; i < nchildren; i++) add_win(ctx, children[i], i ? children[i-1] : None, 0); XFree(children); XUngrabServer(ctx->dpy); XF86VidModeLockModeSwitch(ctx->dpy, ctx->scr, true); ctx->cursor = std::make_unique(ctx); if (g_customCursorPath) { if (!load_mouse_cursor(ctx->cursor.get(), g_customCursorPath, g_customCursorHotspotX, g_customCursorHotspotY)) xwm_log.errorf("Failed to load mouse cursor: %s", g_customCursorPath); } else { if ( std::shared_ptr pHostCursor = gamescope::GetX11HostCursor() ) { ctx->cursor->setCursorImage( reinterpret_cast( pHostCursor->pPixels.data() ), pHostCursor->uWidth, pHostCursor->uHeight, pHostCursor->uXHotspot, pHostCursor->uYHotspot ); } else { xwm_log.infof( "Embedded, no cursor set. Using left_ptr by default." ); if ( !ctx->cursor->setCursorImageByName( "left_ptr" ) ) xwm_log.errorf( "Failed to load mouse cursor: left_ptr" ); } } ctx->cursor->undirty(); XFlush(ctx->dpy); } void update_vrr_atoms(xwayland_ctx_t *root_ctx, bool force, bool* needs_flush = nullptr) { bool capable = GetBackend()->GetCurrentConnector() && GetBackend()->GetCurrentConnector()->SupportsVRR(); if ( capable != g_bVRRCapable_CachedValue || force ) { uint32_t capable_value = capable ? 1 : 0; XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeVRRCapable, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&capable_value, 1 ); g_bVRRCapable_CachedValue = capable; if (needs_flush) *needs_flush = true; } bool HDR = GetBackend()->GetCurrentConnector() && GetBackend()->GetCurrentConnector()->SupportsHDR(); if ( HDR != g_bSupportsHDR_CachedValue || force ) { uint32_t hdr_value = HDR ? 1 : 0; XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeDisplaySupportsHDR, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&hdr_value, 1 ); g_bSupportsHDR_CachedValue = HDR; if (needs_flush) *needs_flush = true; } bool in_use = GetBackend()->GetCurrentConnector() && GetBackend()->GetCurrentConnector()->IsVRRActive(); if ( in_use != g_bVRRInUse_CachedValue || force ) { uint32_t in_use_value = in_use ? 1 : 0; XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeVRRInUse, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&in_use_value, 1 ); g_bVRRInUse_CachedValue = in_use; if (needs_flush) *needs_flush = true; } if ( g_nOutputRefresh != g_nCurrentRefreshRate_CachedValue || force ) { int32_t nRefresh = gamescope::ConvertmHzToHz( g_nOutputRefresh ); XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeDisplayRefreshRateFeedback, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&nRefresh, 1 ); g_nCurrentRefreshRate_CachedValue = g_nOutputRefresh; if (needs_flush) *needs_flush = true; } // Don't update this in-sync with DRM vrr usage. // Keep this as a preference, starting with off. if ( force ) { bool wants_vrr = cv_adaptive_sync; uint32_t enabled_value = wants_vrr ? 1 : 0; XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeVRREnabled, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&enabled_value, 1 ); if (needs_flush) *needs_flush = true; } } void update_mode_atoms(xwayland_ctx_t *root_ctx, bool* needs_flush = nullptr) { if (needs_flush) *needs_flush = true; if ( GetBackend()->GetCurrentConnector() && GetBackend()->GetCurrentConnector()->GetScreenType() == gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL ) { XDeleteProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeDisplayModeListExternal); uint32_t zero = 0; XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeDisplayIsExternal, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&zero, 1 ); return; } if ( !GetBackend()->GetCurrentConnector() ) return; auto connectorModes = GetBackend()->GetCurrentConnector()->GetModes(); char modes[4096] = ""; int remaining_size = sizeof(modes) - 1; int len = 0; for (int i = 0; remaining_size > 0 && i < (int)connectorModes.size(); i++) { const auto& mode = connectorModes[i]; int mode_len = snprintf(&modes[len], remaining_size, "%s%dx%d@%d", i == 0 ? "" : " ", int(mode.uWidth), int(mode.uHeight), int(mode.uRefresh)); len += mode_len; remaining_size -= mode_len; } XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeDisplayModeListExternal, XA_STRING, 8, PropModeReplace, (unsigned char *)modes, strlen(modes) + 1 ); uint32_t one = 1; XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeDisplayIsExternal, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&one, 1 ); } extern int g_nPreferredOutputWidth; extern int g_nPreferredOutputHeight; static bool g_bWasFSRActive = false; bool g_bAppWantsHDRCached = false; void steamcompmgr_check_xdg(bool vblank, uint64_t vblank_idx) { if (wlserver_xdg_dirty()) { for ( auto &iter : g_VirtualConnectorFocuses ) { global_focus_t *pFocus = &iter.second; if (pFocus->focusWindow && pFocus->focusWindow->type == steamcompmgr_win_type_t::XDG) pFocus->focusWindow = nullptr; if (pFocus->inputFocusWindow && pFocus->inputFocusWindow->type == steamcompmgr_win_type_t::XDG) pFocus->inputFocusWindow = nullptr; if (pFocus->overlayWindow && pFocus->overlayWindow->type == steamcompmgr_win_type_t::XDG) pFocus->overlayWindow = nullptr; if (pFocus->notificationWindow && pFocus->notificationWindow->type == steamcompmgr_win_type_t::XDG) pFocus->notificationWindow = nullptr; if (pFocus->overrideWindow && pFocus->overrideWindow->type == steamcompmgr_win_type_t::XDG) pFocus->overrideWindow = nullptr; if (pFocus->fadeWindow && pFocus->fadeWindow->type == steamcompmgr_win_type_t::XDG) pFocus->fadeWindow = nullptr; } g_steamcompmgr_xdg_wins = wlserver_get_xdg_shell_windows(); MakeFocusDirty(); } handle_done_commits_xdg( vblank, vblank_idx ); // When we have observed both a complete commit and a VBlank, we should request a new frame. if (vblank) { for ( const auto& xdg_win : g_steamcompmgr_xdg_wins ) { steamcompmgr_flush_frame_done(xdg_win.get()); } handle_presented_xdg(); } check_new_xdg_res(); } void update_edid_prop() { const char *filename = gamescope::GetPatchedEdidPath(); if (!filename) return; gamescope_xwayland_server_t *server = NULL; for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++) { XTextProperty text_property = { .value = (unsigned char *)filename, .encoding = server->ctx->atoms.utf8StringAtom, .format = 8, .nitems = strlen(filename), }; XSetTextProperty( server->ctx->dpy, server->ctx->root, &text_property, server->ctx->atoms.gamescopeDisplayEdidPath ); } } extern bool g_bLaunchMangoapp; extern void ShutdownGamescope(); gamescope::ConVar cv_shutdown_on_primary_child_death( "shutdown_on_primary_child_death", true, "Should gamescope shutdown when the primary application launched in it was shut down?" ); static LogScope s_LaunchLogScope( "launch" ); static std::vector s_uRelativeMouseFilteredAppids; static gamescope::ConVar cv_mouse_relative_filter_appids( "mouse_relative_filter_appids", "8400" /* Geometry Wars: Retro Evolved */, "Comma separated appids to filter out using relative mouse mode for.", []( gamescope::ConVar &cvar ) { std::vector sFilterAppids = gamescope::Split( cvar, "," ); std::vector uFilterAppids; uFilterAppids.reserve( sFilterAppids.size() ); for ( auto &sFilterAppid : sFilterAppids ) { std::optional ouFilterAppid = gamescope::Parse( sFilterAppid ); uFilterAppids.push_back( *ouFilterAppid ); } s_uRelativeMouseFilteredAppids = std::move( uFilterAppids ); }, true); void LaunchNestedChildren( char **ppPrimaryChildArgv ) { std::string sNewPreload; { const char *pszCurrentPreload = getenv( "LD_PRELOAD" ); if ( pszCurrentPreload && *pszCurrentPreload ) { // Remove gameoverlayrenderer.so from the child if Gamescope // is running with a window + Vulkan swapchain (eg. SDL2 backend) if ( GetBackend()->UsesVulkanSwapchain() ) { std::vector svLibraries = gamescope::Split( pszCurrentPreload, " :" ); std::erase_if( svLibraries, []( std::string_view svPreload ) { return svPreload.find( "gameoverlayrenderer.so" ) != std::string_view::npos; }); bool bFirst = true; for ( std::string_view svLibrary : svLibraries ) { if ( !bFirst ) { sNewPreload.append( ":" ); } bFirst = false; sNewPreload.append( svLibrary ); } } else { sNewPreload = pszCurrentPreload; } } } // We could just run this inside the child process, // but we might as well just run it here at this point. // and affect all future child processes, without needing // a pre-amble inside of them. { if ( !sNewPreload.empty() ) setenv( "LD_PRELOAD", sNewPreload.c_str(), 1 ); else unsetenv( "LD_PRELOAD" ); unsetenv( "ENABLE_VKBASALT" ); // Enable Gamescope WSI by default for nested. setenv( "ENABLE_GAMESCOPE_WSI", "1", 0 ); // Unset this to avoid it leaking to Proton apps, etc. unsetenv( "SDL_VIDEODRIVER" ); // SDL3... unsetenv( "SDL_VIDEO_DRIVER" ); } // Gamescope itself does not set itself as a subreaper anymore. // It launches direct children that do, and manage that they kill themselves // when Gamescope dies. // This allows us to launch stuff alongside Gamescope if we ever wanted -- rather // than being under it. (eg. if we wanted a drm janitor or something.) if ( ppPrimaryChildArgv && *ppPrimaryChildArgv ) { pid_t nPrimaryChildPid = gamescope::Process::SpawnProcessInWatchdog( ppPrimaryChildArgv, false ); std::thread waitThread([ nPrimaryChildPid ]() { pthread_setname_np( pthread_self(), "gamescope-wait" ); gamescope::Process::WaitForChild( nPrimaryChildPid ); s_LaunchLogScope.infof( "Primary child shut down!" ); if ( cv_shutdown_on_primary_child_death ) ShutdownGamescope(); }); waitThread.detach(); } if ( g_bLaunchMangoapp ) { char *ppMangoappArgv[] = { (char *)"mangoapp", NULL }; gamescope::Process::SpawnProcessInWatchdog( ppMangoappArgv, true ); } } static gamescope::CTimerFunction g_FPSLimitVRRTimer{ [] { // do nothing. }}; void steamcompmgr_main(int argc, char **argv) { int readyPipeFD = -1; // Reset getopt() state optind = 1; int o; int opt_index = -1; bool bForceWindowsFullscreen = false; while ((o = getopt_long(argc, argv, gamescope_optstring, gamescope_options, &opt_index)) != -1) { const char *opt_name; switch (o) { case 'R': readyPipeFD = open( optarg, O_WRONLY | O_CLOEXEC ); break; case 'T': statsThreadPath = optarg; { statsThreadRun = true; std::thread statsThreads( statsThreadMain ); statsThreads.detach(); } break; case 'C': cursorHideTime = uint64_t( atoi( optarg ) ) * 1'000'000ul; break; case 'v': drawDebugInfo = true; break; case 'c': cv_composite_force = true; break; case 'x': useXRes = false; break; case 0: // long options without a short option opt_name = gamescope_options[opt_index].name; if (strcmp(opt_name, "debug-focus") == 0) { debugFocus = true; } else if (strcmp(opt_name, "synchronous-x11") == 0) { synchronize = true; } else if (strcmp(opt_name, "debug-events") == 0) { debugEvents = true; } else if (strcmp(opt_name, "cursor") == 0) { g_customCursorPath = optarg; } else if (strcmp(opt_name, "cursor-hotspot") == 0) { sscanf(optarg, "%d,%d", &g_customCursorHotspotX, &g_customCursorHotspotY); } else if (strcmp(opt_name, "fade-out-duration") == 0) { g_FadeOutDuration = atoi(optarg); } else if (strcmp(opt_name, "force-windows-fullscreen") == 0) { bForceWindowsFullscreen = true; } else if (strcmp(opt_name, "hdr-enabled") == 0 || strcmp(opt_name, "hdr-enable") == 0) { cv_hdr_enabled = true; } else if (strcmp(opt_name, "hdr-debug-force-support") == 0) { g_bForceHDRSupportDebug = true; } else if (strcmp(opt_name, "hdr-debug-force-output") == 0) { g_bForceHDR10OutputDebug = true; } else if (strcmp(opt_name, "hdr-itm-enabled") == 0 || strcmp(opt_name, "hdr-itm-enable") == 0) { g_bHDRItmEnable = true; } else if (strcmp(opt_name, "sdr-gamut-wideness") == 0) { g_ColorMgmt.pending.sdrGamutWideness = atof(optarg); } else if (strcmp(opt_name, "hdr-sdr-content-nits") == 0) { g_ColorMgmt.pending.flSDROnHDRBrightness = atof(optarg); } else if (strcmp(opt_name, "hdr-itm-sdr-nits") == 0) { g_flHDRItmSdrNits = atof(optarg); } else if (strcmp(opt_name, "hdr-itm-target-nits") == 0) { g_flHDRItmTargetNits = atof(optarg); } else if (strcmp(opt_name, "framerate-limit") == 0) { g_nSteamCompMgrTargetFPS = atoi(optarg); } else if (strcmp(opt_name, "reshade-effect") == 0) { g_reshade_effect = optarg; } else if (strcmp(opt_name, "reshade-technique-idx") == 0) { g_reshade_technique_idx = atoi(optarg); } else if (strcmp(opt_name, "mura-map") == 0) { set_mura_overlay(optarg); } break; case '?': assert(false); // unreachable } } int subCommandArg = -1; if ( optind < argc ) { subCommandArg = optind; } const char *pchEnableVkBasalt = getenv( "ENABLE_VKBASALT" ); if ( pchEnableVkBasalt != nullptr && pchEnableVkBasalt[0] == '1' ) { cv_composite_force = true; } // Enable color mgmt by default. g_ColorMgmt.pending.enabled = true; currentOutputWidth = g_nPreferredOutputWidth; currentOutputHeight = g_nPreferredOutputHeight; init_runtime_info(); std::unique_lock xwayland_server_guard(g_SteamCompMgrXWaylandServerMutex); // Initialize any xwayland ctxs we have { gamescope_xwayland_server_t *server = NULL; for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++) init_xwayland_ctx(i, server); } gamescope_xwayland_server_t *root_server = wlserver_get_xwayland_server(0); xwayland_ctx_t *root_ctx = root_server->ctx.get(); gamesRunningCount = get_prop(root_ctx, root_ctx->root, root_ctx->atoms.gamesRunningAtom, 0); overscanScaleRatio = get_prop(root_ctx, root_ctx->root, root_ctx->atoms.screenScaleAtom, 0xFFFFFFFF) / (double)0xFFFFFFFF; zoomScaleRatio = get_prop(root_ctx, root_ctx->root, root_ctx->atoms.screenZoomAtom, 0xFFFF) / (double)0xFFFF; globalScaleRatio = overscanScaleRatio * zoomScaleRatio; if ( gamescope::VirtualConnectorIsSingleOutput() ) { // misyl: Make the virtual connector up-front if we are in a single-output mode. // So we don't delay in getting display/output info to the game static constexpr uint64_t k_unSingleOutputVirtualConnectorKey = 0; g_VirtualConnectorFocuses[ k_unSingleOutputVirtualConnectorKey ] = global_focus_t { .ulVirtualFocusKey = k_unSingleOutputVirtualConnectorKey, .pVirtualConnector = GetBackend()->UsesVirtualConnectors() ? GetBackend()->CreateVirtualConnector( k_unSingleOutputVirtualConnectorKey ) : nullptr, }; } for ( auto &iter : g_VirtualConnectorFocuses ) { global_focus_t *pFocus = &iter.second; if ( pFocus->IsDirty() ) determine_and_apply_focus( pFocus ); } if ( readyPipeFD != -1 ) { dprintf( readyPipeFD, "%s %s\n", root_ctx->xwayland_server->get_nested_display_name(), wlserver_get_wl_display_name() ); close( readyPipeFD ); readyPipeFD = -1; } g_SteamCompMgrWaiter.AddWaitable( &GetVBlankTimer() ); g_SteamCompMgrWaiter.AddWaitable( &g_FPSLimitVRRTimer ); GetVBlankTimer().ArmNextVBlank( true ); { gamescope_xwayland_server_t *pServer = NULL; for (size_t i = 0; (pServer = wlserver_get_xwayland_server(i)); i++) { xwayland_ctx_t *pXWaylandCtx = pServer->ctx.get(); g_SteamCompMgrWaiter.AddWaitable( pXWaylandCtx ); pServer->ctx->force_windows_fullscreen = bForceWindowsFullscreen; } } update_vrr_atoms(root_ctx, true); update_mode_atoms(root_ctx); XFlush(root_ctx->dpy); if ( !GetBackend()->PostInit() ) return; update_edid_prop(); update_screenshot_color_mgmt(); LaunchNestedChildren( subCommandArg >= 0 ? &argv[ subCommandArg ] : nullptr ); // Transpose to get this 3x3 matrix into the right state for applying as a 3x4 // on DRM + the Vulkan side. // ie. color.rgb = color.rgba * u_ctm[offsetLayerIdx]; s_scRGB709To2020Matrix = GetBackend()->CreateBackendBlob( glm::mat3x4( glm::transpose( k_2020_from_709 ) ) ); #if HAVE_LIBSYSTEMD int res = sd_bus_default(&g_dbus); if (res < 0) { g_dbus = NULL; s_LaunchLogScope.warnf( "Failed to open systemd message bus, there will be no cgroup protection for focused windows.\n"); } FILE *sysfs_caps = fopen("/sys/fs/cgroup/dmem.capacity", "r"); if (sysfs_caps != nullptr) { char *line = NULL; size_t size = 0; while (getline(&line, &size, sysfs_caps) >= 0) { char *capacity = strstr(line, " "); if (capacity) ++capacity; else continue; uint64_t vramSize = strtoull(capacity, NULL, 10); std::string idString = std::string(line, (capacity - 1) - line); g_vramCapacities.emplace(idString, vramSize); } } #endif for (;;) { { gamescope_xwayland_server_t *server = NULL; for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++) { assert(server->ctx); if (server->ctx->HasQueuedEvents()) server->ctx->Dispatch(); } } g_SteamCompMgrWaiter.PollEvents(); bool vblank = false; if ( std::optional pendingVBlank = GetVBlankTimer().ProcessVBlank() ) { g_SteamCompMgrVBlankTime = *pendingVBlank; vblank = true; } if ( g_bRun == false ) { break; } // If this is from the timer or not. // Consider this to also be "is this vblank, the fastest refresh cycle after our last commit?" // as a question. const bool bIsVBlankFromTimer = vblank; // We can always vblank if VRR. const bool bVRR = GetBackend()->GetCurrentConnector() && GetBackend()->GetCurrentConnector()->IsVRRActive(); if ( bVRR ) vblank = true; bool flush_root = false; if ( inputCounter != lastPublishedInputCounter ) { XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeInputCounterAtom, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&inputCounter, 1 ); lastPublishedInputCounter = inputCounter; flush_root = true; } if ( g_bFSRActive != g_bWasFSRActive ) { uint32_t active = g_bFSRActive ? 1 : 0; XChangeProperty( root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeFSRFeedback, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&active, 1 ); g_bWasFSRActive = g_bFSRActive; flush_root = true; } static gamescope::VirtualConnectorStrategy s_eLastVirtualConnectorStrategy = gamescope::cv_backend_virtual_connector_strategy; gamescope::VirtualConnectorStrategy eVirtualConnectorStrategy = gamescope::cv_backend_virtual_connector_strategy; if ( eVirtualConnectorStrategy != s_eLastVirtualConnectorStrategy ) { // If our virtual connector strategy changes, clear out our virtual connector // global focuses. g_VirtualConnectorFocuses.clear(); s_eLastVirtualConnectorStrategy = eVirtualConnectorStrategy; } #if 0 bool bDirtyFocuses = false; for ( auto &iter : g_VirtualConnectorFocuses ) { global_focus_t *pFocus = &iter.second; if ( pFocus->IsDirty() ) { bDirtyFocuses = true; break; } } #endif // XXX: Need to look into why this doesn't work. // if ( bDirtyFocuses ) { // TODO(misyl): Improve this situation, it's kind of a mess. // We could/should make this event driven rather than solving // per-frame. if ( !gamescope::VirtualConnectorIsSingleOutput() ) { std::vector newKeys; auto focusWindows = GetGlobalPossibleFocusWindows(); for ( steamcompmgr_win_t *pWindow : focusWindows ) { gamescope::VirtualConnectorKey_t ulKey = pWindow->GetVirtualConnectorKey( eVirtualConnectorStrategy ); if ( !gamescope::Algorithm::Contains( newKeys, ulKey ) ) newKeys.emplace_back( ulKey ); } std::sort( newKeys.begin(), newKeys.end() ); std::vector oldKeys; for ( const auto &iter : g_VirtualConnectorFocuses ) oldKeys.emplace_back( iter.first ); std::sort( oldKeys.begin(), oldKeys.end() ); std::vector diffKeys; std::set_symmetric_difference(oldKeys.begin(), oldKeys.end(), newKeys.begin(), newKeys.end(), std::back_inserter(diffKeys), [](auto& a, auto& b) { return a < b; }); for ( gamescope::VirtualConnectorKey_t ulKey : diffKeys ) { bool bIsSteam = gamescope::VirtualConnectorKeyIsSteam( ulKey ); if ( gamescope::Algorithm::Contains( newKeys, ulKey ) ) { g_VirtualConnectorFocuses[ulKey] = global_focus_t { .ulVirtualFocusKey = ulKey, .pVirtualConnector = GetBackend()->UsesVirtualConnectors() ? GetBackend()->CreateVirtualConnector( ulKey ) : nullptr, }; } else if ( !bIsSteam ) // Never remove Steam's virtual conn ector. { g_VirtualConnectorFocuses.erase( ulKey ); } } } for ( auto &iter : g_VirtualConnectorFocuses ) { global_focus_t *pFocus = &iter.second; if ( pFocus->IsDirty() ) determine_and_apply_focus( pFocus ); } } // If our DRM state is out-of-date, refresh it. This might update // the output size. if ( GetBackend()->PollState() ) { hasRepaint = true; update_mode_atoms(root_ctx, &flush_root); } g_uCompositeDebug = cv_composite_debug; g_bOutputHDREnabled = (g_bSupportsHDR_CachedValue || g_bForceHDR10OutputDebug) && cv_hdr_enabled; // Pick our width/height for this potential frame, regardless of how it might change later // At some point we might even add proper locking so we get real updates atomically instead // of whatever jumble of races the below might cause over a couple of frames if ( currentOutputWidth != g_nOutputWidth || currentOutputHeight != g_nOutputHeight || currentHDROutput != g_bOutputHDREnabled || currentHDRForce != g_bForceHDRSupportDebug ) { if ( steamMode && g_nXWaylandCount > 1 ) { g_nNestedHeight = ( g_nNestedWidth * g_nOutputHeight ) / g_nOutputWidth; wlserver_lock(); // Update only Steam, the root ctx, with the new output size for now wlserver_set_xwayland_server_mode( 0, g_nOutputWidth, g_nOutputHeight, g_nOutputRefresh ); wlserver_unlock(); } // XXX(JoshA): Remake this. It sucks. if ( GetBackend()->UsesVulkanSwapchain() ) { vulkan_remake_swapchain(); while ( !acquire_next_image() ) vulkan_remake_swapchain(); } else { vulkan_remake_output_images(); } { gamescope_xwayland_server_t *server = NULL; for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++) { uint32_t hdr_value = ( g_bOutputHDREnabled || g_bForceHDRSupportDebug ) ? 1 : 0; XChangeProperty(server->ctx->dpy, server->ctx->root, server->ctx->atoms.gamescopeHDROutputFeedback, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&hdr_value, 1 ); server->ctx->cursor->setDirty(); if (server->ctx.get() == root_ctx) { flush_root = true; } else { XFlush(server->ctx->dpy); } } } currentOutputWidth = g_nOutputWidth; currentOutputHeight = g_nOutputHeight; currentHDROutput = g_bOutputHDREnabled; currentHDRForce = g_bForceHDRSupportDebug; #if HAVE_PIPEWIRE nudge_pipewire(); #endif } // Ask for a new surface every vblank // When we observe a new commit being complete for a surface, we ask for a new frame. // This ensures that FIFO works properly, since otherwise we might ask for a new frame // application can commit a new frame that completes before we ever displayed // the current pending commit. static uint64_t vblank_idx = 0; if ( vblank ) { { uint64_t now = get_time_in_nanos(); gamescope_xwayland_server_t *server = NULL; for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++) { for (steamcompmgr_win_t *w = server->ctx->list; w; w = w->xwayland().next) { steamcompmgr_latch_frame_done( w, vblank_idx, now ); } } for ( const auto& xdg_win : g_steamcompmgr_xdg_wins ) { steamcompmgr_latch_frame_done( xdg_win.get(), vblank_idx, now ); } } } { gamescope_xwayland_server_t *server = NULL; for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++) { handle_done_commits_xwayland(server->ctx.get(), vblank, vblank_idx); // When we have observed both a complete commit and a VBlank, we should request a new frame. if (vblank) { for (steamcompmgr_win_t *w = server->ctx->list; w; w = w->xwayland().next) { steamcompmgr_flush_frame_done(w); } } } } steamcompmgr_check_xdg(vblank, vblank_idx); if ( s_oLowestFPSLimitScheduleVRR ) { g_FPSLimitVRRTimer.ArmTimer( *s_oLowestFPSLimitScheduleVRR ); s_oLowestFPSLimitScheduleVRR = std::nullopt; } if ( vblank ) { vblank_idx++; int nRealRefreshmHz = g_nNestedRefresh ? g_nNestedRefresh : g_nOutputRefresh; g_SteamCompMgrAppRefreshCycle = gamescope::mHzToRefreshCycle( nRealRefreshmHz ); g_SteamCompMgrLimitedAppRefreshCycle = g_SteamCompMgrAppRefreshCycle; if ( g_nSteamCompMgrTargetFPS ) { int nRealRefreshHz = gamescope::ConvertmHzToHz( nRealRefreshmHz ); int nTargetFPS = g_nSteamCompMgrTargetFPS; nTargetFPS = std::min( nTargetFPS, nRealRefreshHz ); if ( GetBackend()->GetCurrentConnector() && GetBackend()->GetCurrentConnector()->IsVRRActive() ) { g_SteamCompMgrLimitedAppRefreshCycle = gamescope::mHzToRefreshCycle( gamescope::ConvertHztomHz( nTargetFPS ) ); } else { int nVblankDivisor = nRealRefreshHz / nTargetFPS; g_SteamCompMgrLimitedAppRefreshCycle = g_SteamCompMgrAppRefreshCycle * nVblankDivisor; } } } // Handle presentation-time stuff // // Notes: // // We send the presented event just after the latest latch time possible so PresentWait in Vulkan // still returns pretty optimally. The extra 2ms or so can be "display latency" // We still provide the predicted TTL refresh time in the presented event though. // // We ignore or lie most of the flags because they aren't particularly useful for a client // to know anyway and it would delay us sending this at an optimal time. // (particularly for DXGI frame latency handles under Proton.) // // The boat is still out as to whether we should do latest latch or pageflip/ttl for the event. // For now, going to keep this, and if we change our minds later, it's no big deal. // // It's a little strange, but we return `presented` for any window not visible // and `presented` for anything visible. It's a little disingenuous because we didn't // actually show a window if it wasn't visible, but we could! And that is the first // opportunity it had. It's confusing but we need this for forward progress. if ( vblank ) { wlserver_lock(); gamescope_xwayland_server_t *server = NULL; for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++) handle_presented_xwayland( server->ctx.get() ); wlserver_unlock(); } // { gamescope_xwayland_server_t *server = NULL; for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++) check_new_xwayland_res(server->ctx.get()); } { GamescopeAppTextureColorspace current_app_colorspace = GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB; std::shared_ptr app_hdr_metadata = nullptr; if ( g_HeldCommits[HELD_COMMIT_BASE] != nullptr ) { current_app_colorspace = g_HeldCommits[HELD_COMMIT_BASE]->colorspace(); if (g_HeldCommits[HELD_COMMIT_BASE]->feedback) app_hdr_metadata = g_HeldCommits[HELD_COMMIT_BASE]->feedback->hdr_metadata_blob; } bool app_wants_hdr = ColorspaceIsHDR( current_app_colorspace ); if ( app_wants_hdr != g_bAppWantsHDRCached ) { uint32_t app_wants_hdr_prop = app_wants_hdr ? 1 : 0; XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeColorAppWantsHDRFeedback, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&app_wants_hdr_prop, 1 ); g_bAppWantsHDRCached = app_wants_hdr; flush_root = true; } if ( app_hdr_metadata != g_ColorMgmt.pending.appHDRMetadata ) { if ( app_hdr_metadata ) { std::vector app_hdr_metadata_blob; app_hdr_metadata_blob.resize((sizeof(hdr_metadata_infoframe) + (sizeof(uint32_t) - 1)) / sizeof(uint32_t)); memset(app_hdr_metadata_blob.data(), 0, sizeof(uint32_t) * app_hdr_metadata_blob.size()); memcpy(app_hdr_metadata_blob.data(), &app_hdr_metadata->View().hdmi_metadata_type1, sizeof(hdr_metadata_infoframe)); XChangeProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeColorAppHDRMetadataFeedback, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)app_hdr_metadata_blob.data(), (int)app_hdr_metadata_blob.size() ); } else { XDeleteProperty(root_ctx->dpy, root_ctx->root, root_ctx->atoms.gamescopeColorAppHDRMetadataFeedback); } g_ColorMgmt.pending.appHDRMetadata = app_hdr_metadata; flush_root = true; } } // Handles if we got a commit for the window we want to focus // to switch to it for painting (outdatedInteractiveFocus) // Doesn't realllly matter but avoids an extra frame of being on the wrong window. for ( auto &iter : g_VirtualConnectorFocuses ) { global_focus_t *pFocus = &iter.second; if ( pFocus->IsDirty() ) determine_and_apply_focus( pFocus ); } // XXX(misyl): This is bad! We shouldnt change the upscaler like this at all!!! // We should move this to business logic in paint_window or something! if ( GetCurrentFocus() && window_is_steam( GetCurrentFocus()->focusWindow ) ) { g_bSteamIsActiveWindow = true; g_upscaleScaler = GamescopeUpscaleScaler::FIT; g_upscaleFilter = GamescopeUpscaleFilter::LINEAR; } else { g_bSteamIsActiveWindow = false; g_upscaleScaler = g_wantedUpscaleScaler; g_upscaleFilter = g_wantedUpscaleFilter; } // If we're in the middle of a fade, then keep us // as needing a repaint. if ( is_fading_out() ) hasRepaint = true; bool bPainted = false; static int nIgnoredOverlayRepaints = 0; if ( !hasRepaintNonBasePlane ) nIgnoredOverlayRepaints = 0; if ( cv_adaptive_sync_ignore_overlay ) nIgnoredOverlayRepaints = 0; for ( auto &iter : g_VirtualConnectorFocuses ) { global_focus_t *pPaintFocus = &iter.second; if ( vblank ) { if ( pPaintFocus->cursor ) pPaintFocus->cursor->UpdatePosition(); } if ( pPaintFocus->GetNestedHints() && !g_bForceRelativeMouse ) { const bool bImageEmpty = ( pPaintFocus->cursor && pPaintFocus->cursor->imageEmpty() ) && ( !window_is_steam( pPaintFocus->inputFocusWindow ) ); const bool bHasPointerConstraint = pPaintFocus->cursor && pPaintFocus->cursor->IsConstrained(); uint32_t uAppId = pPaintFocus->inputFocusWindow ? pPaintFocus->inputFocusWindow->appID : 0; const bool bExcludedAppId = uAppId && gamescope::Algorithm::Contains( s_uRelativeMouseFilteredAppids, uAppId ); const bool bRelativeMouseMode = bImageEmpty && bHasPointerConstraint && !bExcludedAppId; pPaintFocus->GetNestedHints()->SetRelativeMouseMode( bRelativeMouseMode ); } // HACK: Disable tearing if we have an overlay to avoid stutters right now // TODO: Fix properly. const bool bHasOverlay = ( pPaintFocus->overlayWindow && pPaintFocus->overlayWindow->opacity ) || ( pPaintFocus->externalOverlayWindow && pPaintFocus->externalOverlayWindow->opacity ) || ( pPaintFocus->overrideWindow && pPaintFocus->focusWindow && !pPaintFocus->focusWindow->isSteamStreamingClient && pPaintFocus->overrideWindow->opacity ); // If we are running behind, allow tearing. const bool bForceRepaint = g_bForceRepaint.exchange(false); const bool bForceSyncFlip = bForceRepaint || is_fading_out(); // If we are compositing, always force sync flips because we currently wait // for composition to finish before submitting. // If we want to do async + composite, we should set up syncfile stuff and have DRM wait on it. const bool bSurfaceWantsAsync = (g_HeldCommits[HELD_COMMIT_BASE] != nullptr && g_HeldCommits[HELD_COMMIT_BASE]->async); const bool bTearing = cv_tearing_enabled && GetBackend()->SupportsTearing() && bSurfaceWantsAsync; enum class FlipType { Normal, Async, VRR, }; FlipType eFlipType = FlipType::Normal; if ( bForceSyncFlip ) eFlipType = FlipType::Normal; else if ( bVRR ) eFlipType = FlipType::VRR; else if ( bTearing ) { eFlipType = FlipType::Async; if ( nIgnoredOverlayRepaints ) eFlipType = FlipType::Normal; if ( bHasOverlay ) // Don't tear if the Steam or perf overlay is up atm. eFlipType = FlipType::Normal; if ( GetVBlankTimer().WasCompositing() ) eFlipType = FlipType::Normal; } else eFlipType = FlipType::Normal; bool bShouldPaint = false; //if ( GetBackend()->IsVisible() ) if ( true ) { switch ( eFlipType ) { case FlipType::Normal: { bShouldPaint = vblank && ( hasRepaint || hasRepaintNonBasePlane || bForceSyncFlip ); break; } case FlipType::Async: { bShouldPaint = hasRepaint; if ( vblank && !bShouldPaint && hasRepaintNonBasePlane ) nIgnoredOverlayRepaints++; break; } case FlipType::VRR: { bShouldPaint = hasRepaint; if ( bIsVBlankFromTimer ) { if ( hasRepaintNonBasePlane ) { if ( nIgnoredOverlayRepaints >= cv_adaptive_sync_overlay_cycles ) { // If we hit vblank and we previously punted on drawing an overlay // we should go ahead and draw now. bShouldPaint = true; } else if ( !bShouldPaint ) { // If we hit vblank (ie. fastest refresh cycle since last commit), // and we aren't painting and we have a pending overlay, then: // defer it until the next game update or next true vblank. if ( !cv_adaptive_sync_ignore_overlay ) nIgnoredOverlayRepaints++; } } } // If we have a pending page flip and doing VRR, lets not do another... if ( GetBackend()->GetCurrentConnector()->PresentationFeedback().CurrentPresentsInFlight() != 0 ) bShouldPaint = false; break; } } } else { bShouldPaint = false; } if ( bShouldPaint ) { paint_all( pPaintFocus, eFlipType == FlipType::Async ); bPainted = true; } } if ( bPainted ) { hasRepaint = false; hasRepaintNonBasePlane = false; nIgnoredOverlayRepaints = 0; { gamescope::CScriptScopedLock script; script.Manager().CallHook( "OnPostPaint" ); } } if ( bIsVBlankFromTimer ) { // Pre-emptively re-arm the vblank timer if it // isn't already re-armed. // // Juuust in case pageflip handler doesn't happen // so we don't stop vblanking forever. GetVBlankTimer().ArmNextVBlank( true ); #if HAVE_PIPEWIRE if ( pipewire_is_streaming() ) paint_pipewire(); #endif } update_vrr_atoms(root_ctx, false, &flush_root); if (GetCurrentFocus() && GetCurrentFocus()->cursor) { GetCurrentFocus()->cursor->checkSuspension(); if (GetCurrentFocus()->cursor->needs_server_flush()) { flush_root = true; GetCurrentFocus()->cursor->inform_flush(); } } if (flush_root) { XFlush(root_ctx->dpy); } vulkan_garbage_collect(); vblank = false; } steamcompmgr_exit(); } struct wlr_surface *steamcompmgr_get_server_input_surface( size_t idx ) { gamescope_xwayland_server_t *server = wlserver_get_xwayland_server( idx ); if ( server && server->ctx && server->ctx->focus.inputFocusWindow && server->ctx->focus.inputFocusWindow->xwayland().surface.main_surface ) return server->ctx->focus.inputFocusWindow->xwayland().surface.main_surface; return NULL; } struct wlserver_x11_surface_info *lookup_x11_surface_info_from_xid( gamescope_xwayland_server_t *xwayland_server, uint32_t xid ) { if ( !xwayland_server ) return nullptr; if ( !xwayland_server->ctx ) return nullptr; // Lookup children too so we can get the window // and go back to it's top-level parent. // The xwayland bypass layer does this as we can have child windows // that cover the whole parent. std::unique_lock lock( xwayland_server->ctx->list_mutex ); steamcompmgr_win_t *w = find_win( xwayland_server->ctx.get(), xid, true ); if ( !w ) return nullptr; return &w->xwayland().surface; } MouseCursor *steamcompmgr_get_current_cursor() { global_focus_t *pFocus = GetCurrentFocus(); if ( !pFocus ) return nullptr; return pFocus->cursor; } MouseCursor *steamcompmgr_get_server_cursor(uint32_t idx) { gamescope_xwayland_server_t *server = wlserver_get_xwayland_server( idx ); if ( server && server->ctx ) return server->ctx->cursor.get(); return nullptr; } ValveSoftware-gamescope-eb620ab/src/steamcompmgr.hpp000066400000000000000000000102311502457270500227300ustar00rootroot00000000000000#include #include "wlr_begin.hpp" #include #include #include #include "wlr_end.hpp" extern uint32_t currentOutputWidth; extern uint32_t currentOutputHeight; unsigned int get_time_in_milliseconds(void); uint64_t get_time_in_nanos(); void sleep_for_nanos(uint64_t nanos); void sleep_until_nanos(uint64_t nanos); timespec nanos_to_timespec( uint64_t ulNanos ); void steamcompmgr_main(int argc, char **argv); #include "rendervulkan.hpp" #include "wlserver.hpp" #include "vblankmanager.hpp" #include #include #include struct _XDisplay; struct steamcompmgr_win_t; struct xwayland_ctx_t; class gamescope_xwayland_server_t; static const uint32_t g_zposBase = 0; static const uint32_t g_zposOverride = 1; static const uint32_t g_zposExternalOverlay = 2; static const uint32_t g_zposOverlay = 3; static const uint32_t g_zposCursor = 4; static const uint32_t g_zposMuraCorrection = 5; extern bool g_bHDRItmEnable; extern bool g_bForceHDRSupportDebug; extern EStreamColorspace g_ForcedNV12ColorSpace; struct CursorBarrierInfo { int x1 = 0; int y1 = 0; int x2 = 0; int y2 = 0; }; struct CursorBarrier { PointerBarrier obj = None; CursorBarrierInfo info = {}; }; class MouseCursor { public: explicit MouseCursor(xwayland_ctx_t *ctx); int x() const; int y() const; void paint(steamcompmgr_win_t *window, steamcompmgr_win_t *fit, FrameInfo_t *frameInfo); void setDirty(); // Will take ownership of data. bool setCursorImage(char *data, int w, int h, int hx, int hy); bool setCursorImageByName(const char *name); void hide() { wlserver_lock(); wlserver_mousehide(); wlserver_unlock( false ); checkSuspension(); } void UpdatePosition(); bool isHidden() { return wlserver.bCursorHidden || m_imageEmpty; } bool imageEmpty() const { return m_imageEmpty; } void undirty() { getTexture(); } xwayland_ctx_t *getCtx() const { return m_ctx; } bool needs_server_flush() const { return m_needs_server_flush; } void inform_flush() { m_needs_server_flush = false; } void GetDesiredSize( int& nWidth, int &nHeight ); void checkSuspension(); bool IsConstrained() const { return m_bConstrained; } private: bool getTexture(); void updateCursorFeedback( bool bForce = false ); int m_x = 0, m_y = 0; bool m_bConstrained = false; int m_hotspotX = 0, m_hotspotY = 0; gamescope::OwningRc m_texture; bool m_dirty; uint64_t m_ulLastConnectorId = 0; bool m_imageEmpty; xwayland_ctx_t *m_ctx; bool m_bCursorVisibleFeedback = false; bool m_needs_server_flush = false; }; extern std::vector< wlr_surface * > wayland_surfaces_deleted; extern bool hasFocusWindow; // These are used for touch scaling, so it's really the window that's focused for touch extern float focusedWindowScaleX; extern float focusedWindowScaleY; extern float focusedWindowOffsetX; extern float focusedWindowOffsetY; extern bool g_bFSRActive; extern uint32_t inputCounter; extern uint64_t g_lastWinSeq; void nudge_steamcompmgr( void ); void force_repaint( void ); extern void mangoapp_update( uint64_t visible_frametime, uint64_t app_frametime_ns, uint64_t latency_ns ); struct wlr_surface *steamcompmgr_get_server_input_surface( size_t idx ); wlserver_vk_swapchain_feedback* steamcompmgr_get_base_layer_swapchain_feedback(); struct wlserver_x11_surface_info *lookup_x11_surface_info_from_xid( gamescope_xwayland_server_t *xwayland_server, uint32_t xid ); extern gamescope::VBlankTime g_SteamCompMgrVBlankTime; extern pid_t focusWindow_pid; extern std::shared_ptr focusWindow_engine; void init_xwayland_ctx(uint32_t serverId, gamescope_xwayland_server_t *xwayland_server); void gamescope_set_selection(std::string contents, GamescopeSelection eSelection); void gamescope_set_reshade_effect(std::string effect_path); void gamescope_clear_reshade_effect(); MouseCursor *steamcompmgr_get_current_cursor(); MouseCursor *steamcompmgr_get_server_cursor(uint32_t serverId); extern gamescope::ConVar cv_tearing_enabled; extern void steamcompmgr_set_app_refresh_cycle_override( gamescope::GamescopeScreenType type, int override_fps, bool change_refresh, bool change_fps_cap ); ValveSoftware-gamescope-eb620ab/src/steamcompmgr_shared.hpp000066400000000000000000000152441502457270500242670ustar00rootroot00000000000000#pragma once #include #include #include #include #include "xwayland_ctx.hpp" #include "gamescope-control-protocol.h" struct commit_t; struct wlserver_vk_swapchain_feedback; struct wlserver_x11_surface_info { std::atomic override_surface; std::atomic main_surface; struct wlr_surface *current_surface() const { if ( override_surface ) return override_surface; return main_surface; } // owned by wlserver uint32_t wl_id, x11_id; struct wl_list pending_link; gamescope_xwayland_server_t *xwayland_server; }; // not always an xdg window anymore // wayland-y window (not xwayland) struct wlserver_xdg_surface_info { std::atomic main_surface; struct wlr_surface *current_surface() { return main_surface; } // owned by wlserver struct wlr_xdg_surface *xdg_surface = nullptr; struct wlr_layer_surface_v1 *layer_surface = nullptr; steamcompmgr_win_t *win = nullptr; bool bDoneConfigure = false; std::atomic mapped = { false }; struct wl_list link; struct wl_listener map; struct wl_listener unmap; struct wl_listener destroy; }; enum class steamcompmgr_win_type_t { XWAYLAND, XDG, // could also be layer shell }; struct steamcompmgr_xwayland_win_t { steamcompmgr_win_t *next; Window id; XWindowAttributes a; Damage damage; unsigned long map_sequence; unsigned long damage_sequence; Window transientFor; struct wlserver_x11_surface_info surface; xwayland_ctx_t *ctx; }; struct steamcompmgr_xdg_win_t { uint32_t id; struct wlserver_xdg_surface_info surface; struct wlr_box geometry; }; struct Rect { int32_t nX; int32_t nY; int32_t nWidth; int32_t nHeight; }; extern focus_t g_steamcompmgr_xdg_focus; struct steamcompmgr_win_t { unsigned int opacity = 0xffffffff; uint64_t seq = 0; std::shared_ptr title; bool utf8_title = false; pid_t pid = -1; bool isSteamLegacyBigPicture = false; bool isSteamStreamingClient = false; bool isSteamStreamingClientVideo = false; uint32_t inputFocusMode = 0; uint32_t appID = 0; bool isOverlay = false; bool isExternalOverlay = false; bool IsAnyOverlay() const { return isOverlay || isExternalOverlay; } bool isFullscreen = false; bool isSysTrayIcon = false; bool sizeHintsSpecified = false; bool skipTaskbar = false; bool skipPager = false; unsigned int requestedWidth = 0; unsigned int requestedHeight = 0; bool is_dialog = false; bool maybe_a_dropdown = false; bool outdatedInteractiveFocus = false; uint64_t last_commit_first_latch_time = 0; bool hasHwndStyle = false; uint32_t hwndStyle = 0; bool hasHwndStyleEx = false; uint32_t hwndStyleEx = 0; bool bHasHadNonSRGBColorSpace = false; bool nudged = false; bool ignoreOverrideRedirect = false; bool unlockedForFrameCallback = false; bool receivedDoneCommit = false; std::shared_ptr engineName; std::vector< gamescope::Rc > commit_queue; std::shared_ptr> icon; steamcompmgr_win_type_t type; steamcompmgr_xwayland_win_t& xwayland() { return std::get(_window_types); } const steamcompmgr_xwayland_win_t& xwayland() const { return std::get(_window_types); } steamcompmgr_xdg_win_t& xdg() { return std::get(_window_types); } const steamcompmgr_xdg_win_t& xdg() const { return std::get(_window_types); } std::variant _window_types; focus_t *GetFocus() const { if (type == steamcompmgr_win_type_t::XWAYLAND) return &xwayland().ctx->focus; else if (type == steamcompmgr_win_type_t::XDG) return &g_steamcompmgr_xdg_focus; else return nullptr; } Rect GetGeometry() const { if (type == steamcompmgr_win_type_t::XWAYLAND) return Rect{ xwayland().a.x, xwayland().a.y, xwayland().a.width, xwayland().a.height }; else if (type == steamcompmgr_win_type_t::XDG) return Rect{ xdg().geometry.x, xdg().geometry.y, xdg().geometry.width, xdg().geometry.height }; else return Rect{}; } uint32_t id() const { if (type == steamcompmgr_win_type_t::XWAYLAND) return uint32_t(xwayland().id); else if (type == steamcompmgr_win_type_t::XDG) return xdg().id; else return ~(0u); } wlr_surface *main_surface() const { if (type == steamcompmgr_win_type_t::XWAYLAND) return xwayland().surface.main_surface; else if (type == steamcompmgr_win_type_t::XDG) return xdg().surface.main_surface; else return nullptr; } wlr_surface *current_surface() const { if (type == steamcompmgr_win_type_t::XWAYLAND) return xwayland().surface.current_surface(); return main_surface(); } wlr_surface *override_surface() const { if (type == steamcompmgr_win_type_t::XWAYLAND) return xwayland().surface.override_surface; else return nullptr; } gamescope::VirtualConnectorKey_t GetVirtualConnectorKey( gamescope::VirtualConnectorStrategy eStrategy ) { switch ( eStrategy ) { default: case gamescope::VirtualConnectorStrategies::SingleApplication: case gamescope::VirtualConnectorStrategies::SteamControlled: return 0; case gamescope::VirtualConnectorStrategies::PerAppId: return static_cast( this->appID ); case gamescope::VirtualConnectorStrategies::PerWindow: return static_cast( this->seq ); } } }; namespace gamescope { struct GamescopeScreenshotInfo { std::string szScreenshotPath; gamescope_control_screenshot_type eScreenshotType = GAMESCOPE_CONTROL_SCREENSHOT_TYPE_BASE_PLANE_ONLY; uint32_t uScreenshotFlags = 0; bool bX11PropertyRequested = false; bool bWaylandRequested = false; }; class CScreenshotManager { public: void TakeScreenshot( GamescopeScreenshotInfo info = GamescopeScreenshotInfo{} ) { std::unique_lock lock{ m_ScreenshotInfoMutex }; m_ScreenshotInfo = std::move( info ); } void TakeScreenshot( bool bAVIF ) { char szTimeBuffer[ 1024 ]; time_t currentTime = time(0); struct tm *pLocalTime = localtime( ¤tTime ); strftime( szTimeBuffer, sizeof( szTimeBuffer ), bAVIF ? "/tmp/gamescope_%Y-%m-%d_%H-%M-%S.avif" : "/tmp/gamescope_%Y-%m-%d_%H-%M-%S.png", pLocalTime ); TakeScreenshot( GamescopeScreenshotInfo { .szScreenshotPath = szTimeBuffer, } ); } std::optional ProcessPendingScreenshot() { std::unique_lock lock{ m_ScreenshotInfoMutex }; return std::exchange( m_ScreenshotInfo, std::nullopt ); } static CScreenshotManager &Get(); private: std::mutex m_ScreenshotInfoMutex; std::optional m_ScreenshotInfo; }; extern CScreenshotManager g_ScreenshotMgr; } ValveSoftware-gamescope-eb620ab/src/vblankmanager.cpp000066400000000000000000000256751502457270500230570ustar00rootroot00000000000000// Try to figure out when vblank is and notify steamcompmgr to render some time before it #include #include #include #include #include #include #include #include #include #include #include "gpuvis_trace_utils.h" #include "vblankmanager.hpp" #include "steamcompmgr.hpp" #include "main.hpp" #include "refresh_rate.h" LogScope g_VBlankLog("vblank"); namespace gamescope { ConVar vblank_debug( "vblank_debug", false, "Enable vblank debug spew to stderr." ); CVBlankTimer::CVBlankTimer() { m_ulTargetVBlank = get_time_in_nanos(); m_ulLastVBlank = m_ulTargetVBlank; if ( !GetBackend()->NeedsFrameSync() ) { // Majority of backends fall down this optimal // timerfd path, vs nudge thread. g_VBlankLog.infof( "Using timerfd." ); } else { g_VBlankLog.infof( "Using nudge thread." ); if ( pipe2( m_nNudgePipe, O_CLOEXEC | O_NONBLOCK ) != 0 ) { g_VBlankLog.errorf_errno( "Failed to create VBlankTimer pipe." ); abort(); } std::thread vblankThread( [this]() { this->NudgeThread(); } ); vblankThread.detach(); } } CVBlankTimer::~CVBlankTimer() { std::unique_lock lock( m_ScheduleMutex ); m_bRunning = false; m_bArmed = true; m_bArmed.notify_all(); for ( int i = 0; i < 2; i++ ) { if ( m_nNudgePipe[ i ] >= 0 ) { close ( m_nNudgePipe[ i ] ); m_nNudgePipe[ i ] = -1; } } } int CVBlankTimer::GetRefresh() const { return g_nNestedRefresh ? g_nNestedRefresh : g_nOutputRefresh; } uint64_t CVBlankTimer::GetLastVBlank() const { return m_ulLastVBlank; } uint64_t CVBlankTimer::GetNextVBlank( uint64_t ulOffset ) const { const uint64_t ulIntervalNSecs = mHzToRefreshCycle( GetRefresh() ); const uint64_t ulNow = get_time_in_nanos(); uint64_t ulTargetPoint = GetLastVBlank() + ulIntervalNSecs - ulOffset; while ( ulTargetPoint < ulNow ) ulTargetPoint += ulIntervalNSecs; return ulTargetPoint; } VBlankScheduleTime CVBlankTimer::CalcNextWakeupTime( bool bPreemptive ) { const GamescopeScreenType eScreenType = GetBackend()->GetScreenType(); const int nRefreshRate = GetRefresh(); const uint64_t ulRefreshInterval = mHzToRefreshCycle( nRefreshRate ); bool bVRR = GetBackend()->GetCurrentConnector() && GetBackend()->GetCurrentConnector()->IsVRRActive(); uint64_t ulOffset = 0; if ( !bVRR ) { // The redzone is relative to 60Hz for external displays. // Scale it by our target refresh so we don't miss submitting for // vblank in DRM. // (This fixes wonky frame-pacing on 4K@30Hz screens) // // TODO(Josh): Is this fudging still needed with our SteamOS kernel patches // to not account for vertical front porch when dealing with the vblank // drm_commit is going to target? // Need to re-test that. const uint64_t ulRedZone = eScreenType == GAMESCOPE_SCREEN_TYPE_INTERNAL ? m_ulVBlankDrawBufferRedZone : std::min( m_ulVBlankDrawBufferRedZone, ( m_ulVBlankDrawBufferRedZone * 60'000 * nRefreshRate ) / 60'000 ); const uint64_t ulDecayAlpha = m_ulVBlankRateOfDecayPercentage; // eg. 980 = 98% uint64_t ulDrawTime = m_ulLastDrawTime; /// See comment of m_ulVBlankDrawTimeMinCompositing. if ( m_bCurrentlyCompositing ) ulDrawTime = std::max( ulDrawTime, m_ulVBlankDrawTimeMinCompositing ); uint64_t ulNewRollingDrawTime; // This is a rolling average when ulDrawTime < m_ulRollingMaxDrawTime, // and a maximum when ulDrawTime > m_ulRollingMaxDrawTime. // // This allows us to deal with spikes in the draw buffer time very easily. // eg. if we suddenly spike up (eg. because of test commits taking a stupid long time), // we will then be able to deal with spikes in the long term, even if several commits after // we get back into a good state and then regress again. // If we go over half of our deadzone, be more defensive about things and // spike up back to our current drawtime (sawtooth). if ( int64_t( ulDrawTime ) - int64_t( ulRedZone / 2 ) > int64_t( m_ulRollingMaxDrawTime ) ) ulNewRollingDrawTime = ulDrawTime; else ulNewRollingDrawTime = ( ( ulDecayAlpha * m_ulRollingMaxDrawTime ) + ( kVBlankRateOfDecayMax - ulDecayAlpha ) * ulDrawTime ) / kVBlankRateOfDecayMax; // If we need to offset for our draw more than half of our vblank, something is very wrong. // Clamp our max time to half of the vblank if we can. ulNewRollingDrawTime = std::min( ulNewRollingDrawTime, ulRefreshInterval - ulRedZone ); // If this is not a pre-emptive re-arming, then update // the rolling internal max draw time for next time. if ( !bPreemptive ) m_ulRollingMaxDrawTime = ulNewRollingDrawTime; ulOffset = ulNewRollingDrawTime + ulRedZone; if ( vblank_debug && !bPreemptive ) VBlankDebugSpew( ulOffset, ulDrawTime, ulRedZone ); } else { // See above. if ( !bPreemptive ) { // Reset the max draw time to default, it is unused for VRR. m_ulRollingMaxDrawTime = kStartingVBlankDrawTime; } uint64_t ulRedZone = kVRRFlushingTime; uint64_t ulDrawTime = 0; /// See comment of m_ulVBlankDrawTimeMinCompositing. if ( m_bCurrentlyCompositing ) ulDrawTime = std::max( ulDrawTime, m_ulVBlankDrawTimeMinCompositing ); ulOffset = ulDrawTime + ulRedZone; if ( vblank_debug && !bPreemptive ) VBlankDebugSpew( ulOffset, ulDrawTime, ulRedZone ); } const uint64_t ulScheduledWakeupPoint = GetNextVBlank( ulOffset ); const uint64_t ulTargetVBlank = ulScheduledWakeupPoint + ulOffset; VBlankScheduleTime schedule = { .ulTargetVBlank = ulTargetVBlank, .ulScheduledWakeupPoint = ulScheduledWakeupPoint, }; return schedule; } std::optional CVBlankTimer::ProcessVBlank() { return std::exchange( m_PendingVBlank, std::nullopt ); } void CVBlankTimer::MarkVBlank( uint64_t ulNanos, bool bReArmTimer ) { m_ulLastVBlank = ulNanos; if ( bReArmTimer ) { // Force timer re-arm with the new vblank timings. ArmNextVBlank( false ); } } bool CVBlankTimer::WasCompositing() const { return m_bCurrentlyCompositing; } void CVBlankTimer::UpdateWasCompositing( bool bCompositing ) { m_bCurrentlyCompositing = bCompositing; } void CVBlankTimer::UpdateLastDrawTime( uint64_t ulNanos ) { m_ulLastDrawTime = ulNanos; } void CVBlankTimer::WaitToBeArmed() { // Wait for m_bArmed to change *from* false. m_bArmed.wait( false ); } void CVBlankTimer::ArmNextVBlank( bool bPreemptive ) { std::unique_lock lock( m_ScheduleMutex ); // If we're pre-emptively re-arming, don't // do anything if we are already armed. if ( bPreemptive && m_bArmed ) return; m_bArmed = true; m_bArmed.notify_all(); if ( UsingTimerFD() ) { m_TimerFDSchedule = CalcNextWakeupTime( bPreemptive ); ITimerWaitable::ArmTimer( m_TimerFDSchedule.ulScheduledWakeupPoint ); } } bool CVBlankTimer::UsingTimerFD() const { return m_nNudgePipe[ 0 ] < 0; } int CVBlankTimer::GetFD() { return UsingTimerFD() ? ITimerWaitable::GetFD() : m_nNudgePipe[ 0 ]; } void CVBlankTimer::OnPollIn() { if ( UsingTimerFD() ) { std::unique_lock lock( m_ScheduleMutex ); // Disarm the timer if it was armed. if ( !m_bArmed.exchange( false ) ) return; m_PendingVBlank = VBlankTime { .schedule = m_TimerFDSchedule, // One might think this should just be 'now', however consider the fact // that the effective draw-time should also include the scheduling quantums // and any work before we reached this poll. // The old path used to be be on its own thread, simply awaking from sleep // then writing to a pipe and going back to sleep, the wakeup time was before we // did the write, so we included the quantum of pipe nudge -> wakeup. // Doing this aims to include that, like we were before, but with timerfd. .ulWakeupTime = m_TimerFDSchedule.ulScheduledWakeupPoint, }; gpuvis_trace_printf( "vblank timerfd wakeup" ); ITimerWaitable::DisarmTimer(); } else { VBlankTime time{}; for ( ;; ) { ssize_t ret = read( m_nNudgePipe[ 0 ], &time, sizeof( time ) ); if ( ret < 0 ) { if ( errno == EAGAIN ) continue; g_VBlankLog.errorf_errno( "Failed to read nudge pipe. Pre-emptively re-arming." ); ArmNextVBlank( true ); return; } else if ( ret != sizeof( VBlankTime ) ) { g_VBlankLog.errorf( "Nudge pipe had less data than sizeof( VBlankTime ). Pre-emptively re-arming." ); ArmNextVBlank( true ); return; } else { break; } } uint64_t ulDiff = get_time_in_nanos() - time.ulWakeupTime; if ( ulDiff > 1'000'000ul ) { gpuvis_trace_printf( "Ignoring stale vblank... Pre-emptively re-arming." ); ArmNextVBlank( true ); return; } gpuvis_trace_printf( "got vblank" ); m_PendingVBlank = time; } } void CVBlankTimer::VBlankDebugSpew( uint64_t ulOffset, uint64_t ulDrawTime, uint64_t ulRedZone ) { static uint64_t s_ulVBlankID = 0; static uint64_t s_ulLastDrawTime = kStartingVBlankDrawTime; static uint64_t s_ulLastOffset = kStartingVBlankDrawTime + ulRedZone; if ( s_ulVBlankID++ % 300 == 0 || ulDrawTime > s_ulLastOffset ) { if ( ulDrawTime > s_ulLastOffset ) g_VBlankLog.infof( " !! missed vblank " ); g_VBlankLog.infof( "redZone: %.2fms decayRate: %lu%% - rollingMaxDrawTime: %.2fms lastDrawTime: %.2fms lastOffset: %.2fms - drawTime: %.2fms offset: %.2fms", ulRedZone / 1'000'000.0, m_ulVBlankRateOfDecayPercentage, m_ulRollingMaxDrawTime / 1'000'000.0, s_ulLastDrawTime / 1'000'000.0, s_ulLastOffset / 1'000'000.0, ulDrawTime / 1'000'000.0, ulOffset / 1'000'000.0 ); } s_ulLastDrawTime = ulDrawTime; s_ulLastOffset = ulOffset; } void CVBlankTimer::NudgeThread() { pthread_setname_np( pthread_self(), "gamescope-vblk" ); for ( ;; ) { WaitToBeArmed(); if ( !m_bRunning ) return; VBlankScheduleTime schedule; if ( GetBackend()->GetCurrentConnector() ) { schedule = GetBackend()->GetCurrentConnector()->FrameSync(); } else { // If we don't currently have a connector, make up some dummy refresh cycle. sleep_for_nanos( mHzToRefreshCycle( g_nNestedRefresh ? g_nNestedRefresh : g_nOutputRefresh ) ); uint64_t ulNow = get_time_in_nanos(); schedule = VBlankScheduleTime { .ulTargetVBlank = ulNow + 3'000'000, .ulScheduledWakeupPoint = ulNow, }; } const uint64_t ulWakeupTime = get_time_in_nanos(); { std::unique_lock lock( m_ScheduleMutex ); // Unarm, we are processing now! m_bArmed = false; VBlankTime timeInfo = { .schedule = schedule, .ulWakeupTime = ulWakeupTime, }; ssize_t ret = write( m_nNudgePipe[ 1 ], &timeInfo, sizeof( timeInfo ) ); if ( ret <= 0 ) { g_VBlankLog.errorf_errno( "Nudge write failed" ); } else { gpuvis_trace_printf( "sent vblank (nudge thread)" ); } } } } } gamescope::CVBlankTimer &GetVBlankTimer() { static gamescope::CVBlankTimer s_VBlankTimer; return s_VBlankTimer; } ValveSoftware-gamescope-eb620ab/src/vblankmanager.hpp000066400000000000000000000125121502457270500230460ustar00rootroot00000000000000#pragma once #include #include "waitable.h" namespace gamescope { struct VBlankScheduleTime { // The expected time for the vblank we want to target. uint64_t ulTargetVBlank = 0; // The vblank offset by the redzone/scheduling calculation. // This is when we want to wake-up by to meet that vblank time above. uint64_t ulScheduledWakeupPoint = 0; }; struct VBlankTime { VBlankScheduleTime schedule; // This is when we woke-up either by the timerfd poll // or on the nudge thread. We use this to feed-back into // the draw time so we automatically account for th // CPU scheduler quantums. uint64_t ulWakeupTime = 0; }; class CVBlankTimer : public ITimerWaitable { public: static constexpr uint64_t kMilliSecInNanoSecs = 1'000'000ul; // VBlank timer defaults and starting values. // Anything time-related is nanoseconds unless otherwise specified. static constexpr uint64_t kStartingVBlankDrawTime = 3'000'000ul; static constexpr uint64_t kDefaultMinVBlankTime = 350'000ul; static constexpr uint64_t kDefaultVBlankRedZone = 1'650'000ul; static constexpr uint64_t kDefaultVBlankDrawTimeMinCompositing = 2'400'000ul; static constexpr uint64_t kDefaultVBlankRateOfDecayPercentage = 980ul; // 98% static constexpr uint64_t kVBlankRateOfDecayMax = 1000ul; // 100% static constexpr uint64_t kVRRFlushingTime = 300'000; CVBlankTimer(); ~CVBlankTimer(); int GetRefresh() const; uint64_t GetLastVBlank() const; uint64_t GetNextVBlank( uint64_t ulOffset ) const; VBlankScheduleTime CalcNextWakeupTime( bool bPreemptive ); void Reschedule(); std::optional ProcessVBlank(); void MarkVBlank( uint64_t ulNanos, bool bReArmTimer ); bool WasCompositing() const; void UpdateWasCompositing( bool bCompositing ); void UpdateLastDrawTime( uint64_t ulNanos ); void WaitToBeArmed(); void ArmNextVBlank( bool bPreemptive ); bool UsingTimerFD() const; int GetFD() final; void OnPollIn() final; private: void VBlankDebugSpew( uint64_t ulOffset, uint64_t ulDrawTime, uint64_t ulRedZone ); uint64_t m_ulTargetVBlank = 0; std::atomic m_ulLastVBlank = { 0 }; std::atomic m_bArmed = { false }; std::atomic m_bRunning = { true }; std::optional m_PendingVBlank; // Should have 0 contest, but just to be safe. // This also covers setting of m_bArmed, etc // so we keep in sequence. // m_bArmed is atomic so can still be .wait()'ed // on/read outside. // Does not cover m_ulLastVBlank, this is just atomic. std::mutex m_ScheduleMutex; VBlankScheduleTime m_TimerFDSchedule{}; std::thread m_NudgeThread; int m_nNudgePipe[2] = { -1, -1 }; ///////////////////////////// // Scheduling bits and bobs. ///////////////////////////// // Are we currently compositing? We may need // to push back to avoid clock feedback loops if so. // This is fed-back from steamcompmgr. std::atomic m_bCurrentlyCompositing = { false }; // This is the last time a 'draw' took from wake-up to page flip. // 3ms by default to get the ball rolling. // This is calculated by steamcompmgr/drm and fed-back to the vblank timer. std::atomic m_ulLastDrawTime = { kStartingVBlankDrawTime }; ////////////////////////////////// // VBlank timing tuneables below! ////////////////////////////////// // Internal rolling peak exponential avg. draw time. // This is updated in CalcNextWakeupTime when not // doing pre-emptive timer re-arms. uint64_t m_ulRollingMaxDrawTime = kStartingVBlankDrawTime; // This accounts for some time we cannot account for (which (I think) is the drm_commit -> triggering the pageflip) // It would be nice to make this lower if we can find a way to track that effectively // Perhaps the missing time is spent elsewhere, but given we track from the pipe write // to after the return from `drm_commit` -- I am very doubtful. // 1.3ms by default. (kDefaultMinVBlankTime) uint64_t m_ulMinVBlankTime = kDefaultMinVBlankTime; // The leeway we always apply to our buffer. // 0.3ms by default. (kDefaultVBlankRedZone) uint64_t m_ulVBlankDrawBufferRedZone = kDefaultVBlankRedZone; // The minimum drawtime to use when we are compositing. // Getting closer and closer to vblank when compositing means that we can get into // a feedback loop with our GPU clocks. Pick a sane minimum draw time. // 2.4ms by default. (kDefaultVBlankDrawTimeMinCompositing) uint64_t m_ulVBlankDrawTimeMinCompositing = kDefaultVBlankDrawTimeMinCompositing; // The rate of decay (as a percentage) of the rolling average -> current draw time // 930 = 93%. // 93% by default. (kDefaultVBlankRateOfDecayPercentage) uint64_t m_ulVBlankRateOfDecayPercentage = kDefaultVBlankRateOfDecayPercentage; void NudgeThread(); }; } gamescope::CVBlankTimer &GetVBlankTimer(); ValveSoftware-gamescope-eb620ab/src/vulkan_include.h000066400000000000000000000001071502457270500226760ustar00rootroot00000000000000#pragma once #define VK_NO_PROTOTYPES #include ValveSoftware-gamescope-eb620ab/src/waitable.h000066400000000000000000000301061502457270500214650ustar00rootroot00000000000000#pragma once #include #include #include #include #include #include #include #include #include "log.hpp" extern LogScope g_WaitableLog; timespec nanos_to_timespec( uint64_t ulNanos ); namespace gamescope { class IWaitable { public: virtual ~IWaitable() {} virtual int GetFD() { return -1; } virtual void OnPollIn() {} virtual void OnPollOut() {} virtual void OnPollHangUp() { g_WaitableLog.errorf( "IWaitable hung up. Aborting." ); abort(); } void HandleEvents( uint32_t nEvents ) { if ( nEvents & EPOLLIN ) this->OnPollIn(); if ( nEvents & EPOLLOUT ) this->OnPollOut(); if ( nEvents & EPOLLHUP ) this->OnPollHangUp(); } static void Drain( int nFD ) { if ( nFD < 0 ) return; char buf[1024]; for (;;) { if ( read( nFD, buf, sizeof( buf ) ) < 0 ) { if ( errno != EAGAIN ) g_WaitableLog.errorf_errno( "Failed to drain CNudgeWaitable" ); break; } } } }; class CNudgeWaitable final : public IWaitable { public: CNudgeWaitable() { if ( pipe2( m_nFDs, O_CLOEXEC | O_NONBLOCK ) != 0 ) Shutdown(); } ~CNudgeWaitable() { Shutdown(); } void Shutdown() { for ( int i = 0; i < 2; i++ ) { if ( m_nFDs[i] >= 0 ) { close( m_nFDs[i] ); m_nFDs[i] = -1; } } } void Drain() { IWaitable::Drain( m_nFDs[0] ); } void OnPollIn() final { Drain(); } bool Nudge() { return write( m_nFDs[1], "\n", 1 ) >= 0; } int GetFD() final { return m_nFDs[0]; } private: int m_nFDs[2] = { -1, -1 }; }; class CFunctionWaitable final : public IWaitable { public: CFunctionWaitable( int nFD, std::function fnPollFunc = nullptr ) : m_nFD{ nFD } , m_fnPollFunc{ fnPollFunc } { } void OnPollIn() final { if ( m_fnPollFunc ) m_fnPollFunc(); } void Drain() { IWaitable::Drain( m_nFD ); } int GetFD() final { return m_nFD; } private: int m_nFD; std::function m_fnPollFunc; }; class ITimerWaitable : public IWaitable { public: ITimerWaitable() { m_nFD = timerfd_create( CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC ); if ( m_nFD < 0 ) { g_WaitableLog.errorf_errno( "Failed to create timerfd." ); abort(); } } ~ITimerWaitable() { Shutdown(); } void Shutdown() { if ( m_nFD >= 0 ) { close( m_nFD ); m_nFD = -1; } } void ArmTimer( uint64_t ulScheduledWakeupTime, bool bRepeatingRelative = false ) { timespec wakeupTimeSpec = nanos_to_timespec( ulScheduledWakeupTime ); itimerspec timerspec = { .it_interval = bRepeatingRelative ? wakeupTimeSpec : timespec{}, .it_value = bRepeatingRelative ? timespec{} : wakeupTimeSpec, }; if ( timerfd_settime( m_nFD, TFD_TIMER_ABSTIME, &timerspec, NULL ) < 0 ) g_WaitableLog.errorf_errno( "timerfd_settime failed!" ); } void DisarmTimer() { ArmTimer( 0ul, false ); } int GetFD() { return m_nFD; } private: int m_nFD = -1; }; class CTimerFunction final : public ITimerWaitable { public: CTimerFunction( std::function fnPollFunc ) : m_fnPollFunc{ fnPollFunc } { } void OnPollIn() final { m_fnPollFunc(); } private: std::function m_fnPollFunc; }; template class CWaiter { public: CWaiter() : m_nEpollFD{ epoll_create1( EPOLL_CLOEXEC ) } { AddWaitable( &m_NudgeWaitable ); } ~CWaiter() { Shutdown(); } void Shutdown() { if ( !m_bRunning ) return; m_bRunning = false; Nudge(); if ( m_nEpollFD >= 0 ) { close( m_nEpollFD ); m_nEpollFD = -1; } } bool AddWaitable( IWaitable *pWaitable, uint32_t nEvents = EPOLLIN | EPOLLHUP ) { epoll_event event = { .events = nEvents, .data = { .ptr = reinterpret_cast( pWaitable ), }, }; if ( epoll_ctl( m_nEpollFD, EPOLL_CTL_ADD, pWaitable->GetFD(), &event ) != 0 ) { g_WaitableLog.errorf_errno( "Failed to add waitable" ); return false; } return true; } void RemoveWaitable( IWaitable *pWaitable ) { epoll_ctl( m_nEpollFD, EPOLL_CTL_DEL, pWaitable->GetFD(), nullptr ); } int PollEvents( int nTimeOut = -1 ) { epoll_event events[MaxEvents]; for ( ;; ) { int nEventCount = epoll_wait( m_nEpollFD, events, MaxEvents, nTimeOut ); if ( !m_bRunning ) return 0; if ( nEventCount < 0 ) { if ( errno == EAGAIN || errno == EINTR ) continue; g_WaitableLog.errorf_errno( "Failed to epoll_wait in CAsyncWaiter" ); return nEventCount; } for ( int i = 0; i < nEventCount; i++ ) { epoll_event &event = events[i]; IWaitable *pWaitable = reinterpret_cast( event.data.ptr ); pWaitable->HandleEvents( event.events ); } return nEventCount; } } bool Nudge() { return m_NudgeWaitable.Nudge(); } bool IsRunning() { return m_bRunning; } private: std::atomic m_bRunning = { true }; CNudgeWaitable m_NudgeWaitable; int m_nEpollFD = -1; }; // A raw pointer class that's compatible with shared/unique_ptr + Rc semantics // eg. .get(), etc. // for compatibility with structures that use other types that assume ownership/lifetime // in some way. template class CRawPointer { public: CRawPointer() {} CRawPointer( std::nullptr_t ) {} CRawPointer( const CRawPointer &other ) : m_pObject{ other.m_pObject } { } CRawPointer( CRawPointer&& other ) : m_pObject{ other.m_pObject } { other.m_pObject = nullptr; } CRawPointer( T* pObject ) : m_pObject{ pObject } { } CRawPointer& operator = ( std::nullptr_t ) { m_pObject = nullptr; return *this; } CRawPointer& operator = ( const CRawPointer& other ) { m_pObject = other.m_pObject; return *this; } CRawPointer& operator = ( CRawPointer&& other ) { this->m_pObject = other.m_pObject; other.m_pObject = nullptr; return *this; } T& operator * () const { return *m_pObject; } T* operator -> () const { return m_pObject; } T* get() const { return m_pObject; } bool operator == ( const CRawPointer& other ) const { return m_pObject == other.m_pObject; } bool operator != ( const CRawPointer& other ) const { return m_pObject != other.m_pObject; } bool operator == ( T *pOther ) const { return m_pObject == pOther; } bool operator != ( T *pOther ) const { return m_pObject == pOther; } bool operator == ( std::nullptr_t ) const { return m_pObject == nullptr; } bool operator != ( std::nullptr_t ) const { return m_pObject != nullptr; } private: T* m_pObject = nullptr; }; template , size_t MaxEvents = 1024> class CAsyncWaiter : private CWaiter { public: CAsyncWaiter( const char *pszThreadName ) : m_Thread{ [cWaiter = this, cName = pszThreadName](){ cWaiter->WaiterThreadFunc(cName); } } { if constexpr ( UseTracking() ) { m_AddedWaitables.reserve( 32 ); m_RemovedWaitables.reserve( 32 ); } } ~CAsyncWaiter() { Shutdown(); } void Shutdown() { CWaiter::Shutdown(); if ( m_Thread.joinable() ) m_Thread.join(); if constexpr ( UseTracking() ) { { std::unique_lock lock( m_AddedWaitablesMutex ); m_AddedWaitables.clear(); } { std::unique_lock lock( m_RemovedWaitablesMutex ); m_RemovedWaitables.clear(); } } } bool AddWaitable( WaitableType pWaitable, uint32_t nEvents = EPOLLIN | EPOLLHUP ) { if constexpr ( UseTracking() ) { if ( !pWaitable->HasLiveReferences() ) return false; std::unique_lock lock( m_AddedWaitablesMutex ); if ( !CWaiter::AddWaitable( pWaitable.get(), nEvents ) ) return false; m_AddedWaitables.emplace_back( pWaitable ); return true; } else { return CWaiter::AddWaitable( pWaitable.get(), nEvents ); } } void RemoveWaitable( WaitableType pWaitable ) { if constexpr ( UseTracking() ) { if ( !pWaitable->HasLiveReferences() ) return; std::unique_lock lock( m_RemovedWaitablesMutex ); m_RemovedWaitables.emplace_back( pWaitable.get() ); } CWaiter::RemoveWaitable( pWaitable.get() ); } void WaiterThreadFunc( const char *pszThreadName ) { pthread_setname_np( pthread_self(), pszThreadName ); while ( this->IsRunning() ) { CWaiter::PollEvents(); if constexpr ( UseTracking() ) { std::scoped_lock lock( m_AddedWaitablesMutex, m_RemovedWaitablesMutex ); for ( auto& pRemoved : m_RemovedWaitables ) std::erase( m_AddedWaitables, pRemoved ); m_RemovedWaitables.clear(); } } } private: static constexpr bool UseTracking() { return !std::is_same>::value; } std::thread m_Thread; // Avoids bubble in the waiter thread func where lifetimes // of objects (eg. shared_ptr) could be too short. // Eg. RemoveWaitable but still processing events, or about // to start processing events. std::mutex m_AddedWaitablesMutex; std::vector m_AddedWaitables; std::mutex m_RemovedWaitablesMutex; std::vector m_RemovedWaitables; }; } ValveSoftware-gamescope-eb620ab/src/win32_styles.h000066400000000000000000000071021502457270500222420ustar00rootroot00000000000000#pragma once static constexpr uint32_t WS_OVERLAPPED = 0x00000000u; static constexpr uint32_t WS_POPUP = 0x80000000u; static constexpr uint32_t WS_CHILD = 0x40000000u; static constexpr uint32_t WS_MINIMIZE = 0x20000000u; static constexpr uint32_t WS_VISIBLE = 0x10000000u; static constexpr uint32_t WS_DISABLED = 0x08000000u; static constexpr uint32_t WS_CLIPSIBLINGS = 0x04000000u; static constexpr uint32_t WS_CLIPCHILDREN = 0x02000000u; static constexpr uint32_t WS_MAXIMIZE = 0x01000000u; static constexpr uint32_t WS_BORDER = 0x00800000u; static constexpr uint32_t WS_DLGFRAME = 0x00400000u; static constexpr uint32_t WS_VSCROLL = 0x00200000u; static constexpr uint32_t WS_HSCROLL = 0x00100000u; static constexpr uint32_t WS_SYSMENU = 0x00080000u; static constexpr uint32_t WS_THICKFRAME = 0x00040000u; static constexpr uint32_t WS_GROUP = 0x00020000u; static constexpr uint32_t WS_TABSTOP = 0x00010000u; static constexpr uint32_t WS_MINIMIZEBOX = 0x00020000u; static constexpr uint32_t WS_MAXIMIZEBOX = 0x00010000u; static constexpr uint32_t WS_CAPTION = WS_BORDER | WS_DLGFRAME; static constexpr uint32_t WS_TILED = WS_OVERLAPPED; static constexpr uint32_t WS_ICONIC = WS_MINIMIZE; static constexpr uint32_t WS_SIZEBOX = WS_THICKFRAME; static constexpr uint32_t WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME| WS_MINIMIZEBOX | WS_MAXIMIZEBOX; static constexpr uint32_t WS_POPUPWINDOW = WS_POPUP | WS_BORDER | WS_SYSMENU; static constexpr uint32_t WS_CHILDWINDOW = WS_CHILD; static constexpr uint32_t WS_TILEDWINDOW = WS_OVERLAPPEDWINDOW; static constexpr uint32_t WS_EX_DLGMODALFRAME = 0x00000001u; static constexpr uint32_t WS_EX_DRAGDETECT = 0x00000002u; // Undocumented static constexpr uint32_t WS_EX_NOPARENTNOTIFY = 0x00000004u; static constexpr uint32_t WS_EX_TOPMOST = 0x00000008u; static constexpr uint32_t WS_EX_ACCEPTFILES = 0x00000010u; static constexpr uint32_t WS_EX_TRANSPARENT = 0x00000020u; static constexpr uint32_t WS_EX_MDICHILD = 0x00000040u; static constexpr uint32_t WS_EX_TOOLWINDOW = 0x00000080u; static constexpr uint32_t WS_EX_WINDOWEDGE = 0x00000100u; static constexpr uint32_t WS_EX_CLIENTEDGE = 0x00000200u; static constexpr uint32_t WS_EX_CONTEXTHELP = 0x00000400u; static constexpr uint32_t WS_EX_RIGHT = 0x00001000u; static constexpr uint32_t WS_EX_LEFT = 0x00000000u; static constexpr uint32_t WS_EX_RTLREADING = 0x00002000u; static constexpr uint32_t WS_EX_LTRREADING = 0x00000000u; static constexpr uint32_t WS_EX_LEFTSCROLLBAR = 0x00004000u; static constexpr uint32_t WS_EX_RIGHTSCROLLBAR = 0x00000000u; static constexpr uint32_t WS_EX_CONTROLPARENT = 0x00010000u; static constexpr uint32_t WS_EX_STATICEDGE = 0x00020000u; static constexpr uint32_t WS_EX_APPWINDOW = 0x00040000u; static constexpr uint32_t WS_EX_LAYERED = 0x00080000u; static constexpr uint32_t WS_EX_NOINHERITLAYOUT = 0x00100000u; static constexpr uint32_t WS_EX_NOREDIRECTIONBITMAP = 0x00200000u; static constexpr uint32_t WS_EX_LAYOUTRTL = 0x00400000u; static constexpr uint32_t WS_EX_COMPOSITED = 0x02000000u; static constexpr uint32_t WS_EX_NOACTIVATE = 0x08000000u; ValveSoftware-gamescope-eb620ab/src/wlr_begin.hpp000066400000000000000000000002071502457270500222040ustar00rootroot00000000000000#include extern "C" { #define static #define class class_ #define namespace _namespace #define delete delete_ ValveSoftware-gamescope-eb620ab/src/wlr_end.hpp000066400000000000000000000000741502457270500216700ustar00rootroot00000000000000#undef static #undef class #undef namespace #undef delete } ValveSoftware-gamescope-eb620ab/src/wlserver.cpp000066400000000000000000002737621502457270500221220ustar00rootroot00000000000000#include #define _GNU_SOURCE 1 #include #include #include #include #include #include #include #include #include #include #include #include #include "WaylandServer/WaylandResource.h" #include "WaylandServer/WaylandProtocol.h" #include "WaylandServer/LinuxDrmSyncobj.h" #include "WaylandServer/Reshade.h" #include "WaylandServer/GamescopeActionBinding.h" #include "wlr_begin.hpp" #include #include #if HAVE_DRM #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "wlr_end.hpp" #include "gamescope-xwayland-protocol.h" #include "gamescope-pipewire-protocol.h" #include "gamescope-control-protocol.h" #include "gamescope-private-protocol.h" #include "gamescope-swapchain-protocol.h" #include "presentation-time-protocol.h" #include "wlserver.hpp" #include "hdmi.h" #include "main.hpp" #include "steamcompmgr.hpp" #include "color_helpers.h" #include "log.hpp" #include "ime.hpp" #include "xwayland_ctx.hpp" #include "refresh_rate.h" #include "InputEmulation.h" #include "commit.h" #include "Timeline.h" #include "Utils/NonCopyable.h" #if HAVE_PIPEWIRE #include "pipewire.hpp" #endif #include "gpuvis_trace_utils.h" #include #include #include static LogScope wl_log("wlserver"); using namespace std::literals; extern gamescope::ConVar cv_drm_debug_disable_explicit_sync; //#define GAMESCOPE_SWAPCHAIN_DEBUG struct wlserver_t wlserver = { .touch_down_ids = {} }; struct wlserver_content_override { gamescope_xwayland_server_t *server; struct wlr_surface *surface; uint32_t x11_window; struct wl_listener surface_destroy_listener; struct wl_resource *gamescope_swapchain; }; std::mutex g_wlserver_xdg_shell_windows_lock; static struct wl_list pending_surfaces = {0}; static std::atomic g_bShutdownWLServer{ false }; static void wlserver_x11_surface_info_set_wlr( struct wlserver_x11_surface_info *surf, struct wlr_surface *wlr_surf, bool override ); wlserver_wl_surface_info *get_wl_surface_info(struct wlr_surface *wlr_surf); static void wlserver_update_cursor_constraint(); static void handle_pointer_constraint(struct wl_listener *listener, void *data); static void wlserver_constrain_cursor( struct wlr_pointer_constraint_v1 *pNewConstraint ); struct wlr_surface *wlserver_surface_to_main_surface( struct wlr_surface *pSurface ); bool wlserver_process_hotkeys( wlr_keyboard *keyboard, uint32_t key, bool press ); extern std::atomic hasRepaint; std::vector& gamescope_xwayland_server_t::retrieve_commits() { static std::vector commits; commits.clear(); commits.reserve(16); { std::lock_guard lock( wayland_commit_lock ); commits.swap(wayland_commit_queue); } return commits; } gamescope::ConVar cv_drm_debug_syncobj_force_wait_on_commit( "drm_debug_syncobj_force_wait_on_commit", false, "Force a wait on DRM sync objects before committing buffers" ); std::optional PrepareCommit( struct wlr_surface *surf, struct wlr_buffer *buf ) { auto wl_surf = get_wl_surface_info( surf ); const auto& pFeedback = wlserver_surface_swapchain_feedback(surf); bool bExplicitSync = wl_surf->pSyncobjSurface && wl_surf->pSyncobjSurface->HasExplicitSync(); std::shared_ptr pAcquirePoint; std::shared_ptr pReleasePoint; if ( bExplicitSync ) { pAcquirePoint = wl_surf->pSyncobjSurface->ExtractAcquireTimelinePoint(); pReleasePoint = wl_surf->pSyncobjSurface->ExtractReleaseTimelinePoint(); if ( cv_drm_debug_syncobj_force_wait_on_commit ) { if ( !pAcquirePoint->Wait() ) wl_log.errorf( "drmSyncobjWait failed!" ); } } auto oNewEntry = std::optional { std::in_place_t{}, surf, buf, wlserver_surface_is_async(surf), wlserver_surface_is_fifo(surf), pFeedback, std::move(wl_surf->pending_presentation_feedbacks), wl_surf->present_id, wl_surf->desired_present_time, std::move( pAcquirePoint ), std::move( pReleasePoint ) }; wl_surf->present_id = std::nullopt; wl_surf->desired_present_time = 0; wl_surf->pending_presentation_feedbacks.clear(); wl_surf->oCurrentPresentMode = std::nullopt; struct wlr_surface *pConstraintSurface = wlserver_surface_to_main_surface( surf ); struct wlr_pointer_constraint_v1 *pConstraint = wlserver.GetCursorConstraint(); if ( pConstraint && pConstraint->surface == pConstraintSurface ) wlserver_update_cursor_constraint(); return oNewEntry; } void gamescope_xwayland_server_t::wayland_commit(struct wlr_surface *surf, struct wlr_buffer *buf) { std::optional oEntry = PrepareCommit( surf, buf ); if ( !oEntry ) return; { std::lock_guard lock( wayland_commit_lock ); wayland_commit_queue.emplace_back( std::move( *oEntry ) ); } nudge_steamcompmgr(); } struct PendingCommit_t { struct wlr_surface *surf; struct wlr_buffer *buf; }; std::list g_PendingCommits; void wlserver_xdg_commit(struct wlr_surface *surf, struct wlr_buffer *buf) { std::optional oEntry = PrepareCommit( surf, buf ); if ( !oEntry ) return; { std::lock_guard lock( wlserver.xdg_commit_lock ); wlserver.xdg_commit_queue.push_back( std::move( *oEntry ) ); } nudge_steamcompmgr(); } void xwayland_surface_commit(struct wlr_surface *wlr_surface) { wlr_surface->current.committed = 0; wlserver_x11_surface_info *wlserver_x11_surface_info = get_wl_surface_info(wlr_surface)->x11_surface; wlserver_xdg_surface_info *wlserver_xdg_surface_info = get_wl_surface_info(wlr_surface)->xdg_surface; if ( wlserver_xdg_surface_info ) { if ( !wlserver_xdg_surface_info->bDoneConfigure ) { if ( wlserver_xdg_surface_info->xdg_surface ) wlr_xdg_surface_schedule_configure( wlserver_xdg_surface_info->xdg_surface ); if ( wlserver_xdg_surface_info->layer_surface ) wlr_layer_surface_v1_configure( wlserver_xdg_surface_info->layer_surface, g_nNestedWidth, g_nNestedHeight ); wlserver_xdg_surface_info->bDoneConfigure = true; } } // Committing without buffer state is valid and commits the same buffer again. // Mutter and Weston have forward progress on the frame callback in this situation, // so let the commit go through. It will be duplication-eliminated later. VulkanWlrTexture_t *tex = (VulkanWlrTexture_t *) wlr_surface_get_texture( wlr_surface ); if ( tex == NULL ) { return; } struct wlr_buffer *buf = wlr_buffer_lock( tex->buf ); gpuvis_trace_printf( "xwayland_surface_commit wlr_surface %p", wlr_surface ); if (wlserver_x11_surface_info) { assert(wlserver_x11_surface_info->xwayland_server); wlserver_x11_surface_info->xwayland_server->wayland_commit( wlr_surface, buf ); } else if (wlserver_xdg_surface_info) { wlserver_xdg_commit(wlr_surface, buf); } else { g_PendingCommits.push_back(PendingCommit_t{ wlr_surface, buf }); } } void gamescope_xwayland_server_t::on_xwayland_ready(void *data) { xwayland_ready = true; if (!xwayland_server->options.no_touch_pointer_emulation) wl_log.infof("Xwayland doesn't support -noTouchPointerEmulation, touch events might get duplicated"); dpy = XOpenDisplay( get_nested_display_name() ); } void gamescope_xwayland_server_t::xwayland_ready_callback(struct wl_listener *listener, void *data) { gamescope_xwayland_server_t *server = wl_container_of( listener, server, xwayland_ready_listener ); server->on_xwayland_ready(data); } static void bump_input_counter() { inputCounter++; nudge_steamcompmgr(); } static void wlserver_handle_modifiers(struct wl_listener *listener, void *data) { struct wlr_keyboard *keyboard = &wlserver.keyboard_group->keyboard; wlr_seat_set_keyboard( wlserver.wlr.seat, keyboard ); wlr_seat_keyboard_notify_modifiers( wlserver.wlr.seat, &keyboard->modifiers ); bump_input_counter(); } static void wlserver_handle_key(struct wl_listener *listener, void *data) { struct wlr_keyboard *keyboard = &wlserver.keyboard_group->keyboard; struct wlr_keyboard_key_event *event = (struct wlr_keyboard_key_event *) data; xkb_keycode_t keycode = event->keycode + 8; xkb_keysym_t keysym = xkb_state_key_get_one_sym(keyboard->xkb_state, keycode); #if HAVE_SESSION if (wlserver.wlr.session && event->state == WL_KEYBOARD_KEY_STATE_PRESSED && keysym >= XKB_KEY_XF86Switch_VT_1 && keysym <= XKB_KEY_XF86Switch_VT_12) { unsigned vt = keysym - XKB_KEY_XF86Switch_VT_1 + 1; wlr_session_change_vt(wlserver.wlr.session, vt); return; } #endif // TODO: Remove the below hack when Steam is shipping // `gamescope_action_binding_manager` in Steam Stable // as it can just use a keybind to grab these always. bool forbidden_key = keysym == XKB_KEY_XF86AudioLowerVolume || keysym == XKB_KEY_XF86AudioRaiseVolume || keysym == XKB_KEY_XF86PowerOff; if ( ( event->state == WL_KEYBOARD_KEY_STATE_PRESSED || event->state == WL_KEYBOARD_KEY_STATE_RELEASED ) && forbidden_key ) { // Always send volume+/- to root server only, to avoid it reaching the game. struct wlr_surface *old_kb_surf = wlserver.kb_focus_surface; struct wlr_surface *new_kb_surf = steamcompmgr_get_server_input_surface( 0 ); if ( new_kb_surf ) { wlserver_keyboardfocus( new_kb_surf, false ); wlr_seat_set_keyboard( wlserver.wlr.seat, keyboard ); wlr_seat_keyboard_notify_key( wlserver.wlr.seat, event->time_msec, event->keycode, event->state ); wlserver_keyboardfocus( old_kb_surf, false ); return; } } if ( !wlserver_process_hotkeys( keyboard, event->keycode, event->state == WL_KEYBOARD_KEY_STATE_PRESSED ) ) { wlr_seat_set_keyboard( wlserver.wlr.seat, keyboard ); wlr_seat_keyboard_notify_key( wlserver.wlr.seat, event->time_msec, event->keycode, event->state ); } bump_input_counter(); } static void wlserver_perform_rel_pointer_motion(double unaccel_dx, double unaccel_dy) { assert( wlserver_is_lock_held() ); wlr_relative_pointer_manager_v1_send_relative_motion( wlserver.relative_pointer_manager, wlserver.wlr.seat, 0, unaccel_dx, unaccel_dy, unaccel_dx, unaccel_dy ); } static void wlserver_handle_pointer_motion(struct wl_listener *listener, void *data) { struct wlr_pointer_motion_event *event = (struct wlr_pointer_motion_event *) data; wlserver_mousemotion(event->unaccel_dx, event->unaccel_dy, event->time_msec); } void wlserver_open_steam_menu( bool qam ) { gamescope_xwayland_server_t *server = wlserver_get_xwayland_server( 0 ); if (!server) return; uint32_t keycode = qam ? XK_2 : XK_1; XTestFakeKeyEvent(server->get_xdisplay(), XKeysymToKeycode( server->get_xdisplay(), XK_Control_L ), True, CurrentTime); XTestFakeKeyEvent(server->get_xdisplay(), XKeysymToKeycode( server->get_xdisplay(), keycode ), True, CurrentTime); XTestFakeKeyEvent(server->get_xdisplay(), XKeysymToKeycode( server->get_xdisplay(), keycode ), False, CurrentTime); XTestFakeKeyEvent(server->get_xdisplay(), XKeysymToKeycode( server->get_xdisplay(), XK_Control_L ), False, CurrentTime); } static void wlserver_handle_pointer_button(struct wl_listener *listener, void *data) { struct wlserver_pointer *pointer = wl_container_of( listener, pointer, button ); struct wlr_pointer_button_event *event = (struct wlr_pointer_button_event *) data; wlr_seat_pointer_notify_button( wlserver.wlr.seat, event->time_msec, event->button, event->state ); } static void wlserver_handle_pointer_axis(struct wl_listener *listener, void *data) { struct wlserver_pointer *pointer = wl_container_of( listener, pointer, axis ); struct wlr_pointer_axis_event *event = (struct wlr_pointer_axis_event *) data; wlr_seat_pointer_notify_axis( wlserver.wlr.seat, event->time_msec, event->orientation, event->delta, event->delta_discrete, event->source, event->relative_direction ); } static void wlserver_handle_pointer_frame(struct wl_listener *listener, void *data) { wlr_seat_pointer_notify_frame( wlserver.wlr.seat ); bump_input_counter(); } static inline uint32_t TouchClickModeToLinuxButton( gamescope::TouchClickMode eTouchClickMode ) { switch ( eTouchClickMode ) { default: case gamescope::TouchClickModes::Hover: return 0; case gamescope::TouchClickModes::Trackpad: case gamescope::TouchClickModes::Left: return BTN_LEFT; case gamescope::TouchClickModes::Right: return BTN_RIGHT; case gamescope::TouchClickModes::Middle: return BTN_MIDDLE; } } std::atomic g_bPendingTouchMovement = { false }; static void wlserver_handle_touch_down(struct wl_listener *listener, void *data) { struct wlserver_touch *touch = wl_container_of( listener, touch, down ); struct wlr_touch_down_event *event = (struct wlr_touch_down_event *) data; wlserver_touchdown( event->x, event->y, event->touch_id, event->time_msec ); } static void wlserver_handle_touch_up(struct wl_listener *listener, void *data) { struct wlserver_touch *touch = wl_container_of( listener, touch, up ); struct wlr_touch_up_event *event = (struct wlr_touch_up_event *) data; wlserver_touchup( event->touch_id, event->time_msec ); } static void wlserver_handle_touch_motion(struct wl_listener *listener, void *data) { struct wlserver_touch *touch = wl_container_of( listener, touch, motion ); struct wlr_touch_motion_event *event = (struct wlr_touch_motion_event *) data; wlserver_touchmotion( event->x, event->y, event->touch_id, event->time_msec ); } static void wlserver_new_input(struct wl_listener *listener, void *data) { struct wlr_input_device *device = (struct wlr_input_device *) data; switch ( device->type ) { case WLR_INPUT_DEVICE_KEYBOARD: { struct wlr_keyboard *keyboard = wlr_keyboard_from_input_device(device); wlr_keyboard_set_keymap(keyboard, wlserver.keyboard_group->keyboard.keymap); if (!wlr_keyboard_group_add_keyboard(wlserver.keyboard_group, keyboard)) { wl_log.errorf("failed to add physical keyboard %s", device->name); break; } // Sync the state of the modifiers and the state of the LEDs struct wlr_keyboard_modifiers mods = wlserver.keyboard_group->keyboard.modifiers; if (mods.depressed != keyboard->modifiers.depressed || mods.latched != keyboard->modifiers.latched || mods.locked != keyboard->modifiers.locked || mods.group != keyboard->modifiers.group) { wlr_keyboard_notify_modifiers(keyboard, mods.depressed, mods.latched, mods.locked, mods.group); } } break; case WLR_INPUT_DEVICE_POINTER: { struct wlserver_pointer *pointer = (struct wlserver_pointer *) calloc( 1, sizeof( struct wlserver_pointer ) ); pointer->wlr = (struct wlr_pointer *)device; pointer->motion.notify = wlserver_handle_pointer_motion; wl_signal_add( &pointer->wlr->events.motion, &pointer->motion ); pointer->button.notify = wlserver_handle_pointer_button; wl_signal_add( &pointer->wlr->events.button, &pointer->button ); pointer->axis.notify = wlserver_handle_pointer_axis; wl_signal_add( &pointer->wlr->events.axis, &pointer->axis); pointer->frame.notify = wlserver_handle_pointer_frame; wl_signal_add( &pointer->wlr->events.frame, &pointer->frame); } break; case WLR_INPUT_DEVICE_TOUCH: { struct wlserver_touch *touch = (struct wlserver_touch *) calloc( 1, sizeof( struct wlserver_touch ) ); touch->wlr = (struct wlr_touch *)device; touch->down.notify = wlserver_handle_touch_down; wl_signal_add( &touch->wlr->events.down, &touch->down ); touch->up.notify = wlserver_handle_touch_up; wl_signal_add( &touch->wlr->events.up, &touch->up ); touch->motion.notify = wlserver_handle_touch_motion; wl_signal_add( &touch->wlr->events.motion, &touch->motion ); } break; default: break; } } static struct wl_listener new_input_listener = { .notify = wlserver_new_input }; wlserver_wl_surface_info *get_wl_surface_info(struct wlr_surface *wlr_surf) { if (!wlr_surf) return NULL; return reinterpret_cast(wlr_surf->data); } static void handle_wl_surface_commit( struct wl_listener *l, void *data ) { wlserver_wl_surface_info *surf = wl_container_of( l, surf, commit ); xwayland_surface_commit(surf->wlr); } static void handle_wl_surface_destroy( struct wl_listener *l, void *data ) { wlserver_wl_surface_info *surf = wl_container_of( l, surf, destroy ); if (surf->x11_surface) { wlserver_x11_surface_info *x11_surface = surf->x11_surface; wlserver_x11_surface_info_finish(x11_surface); // Re-init it so it can be destroyed for good on the x11 side. // This just clears it out from the main wl surface mainly. // // wl_list_remove leaves stuff in a weird state, so we need to call // this to re-init the list to avoid a crash. wlserver_x11_surface_info_init(x11_surface, x11_surface->xwayland_server, x11_surface->x11_id); } if ( surf->wlr == wlserver.mouse_focus_surface ) wlserver.mouse_focus_surface = nullptr; if ( surf->wlr == wlserver.kb_focus_surface ) wlserver.kb_focus_surface = nullptr; wlserver.current_dropdown_surfaces.erase( surf->wlr ); for (auto it = g_PendingCommits.begin(); it != g_PendingCommits.end();) { if (it->surf == surf->wlr) { // We owned the buffer lock, so unlock it here. wlr_buffer_unlock(it->buf); it = g_PendingCommits.erase(it); } else { it++; } } for (auto& feedback : surf->pending_presentation_feedbacks) { wp_presentation_feedback_send_discarded(feedback); wl_resource_destroy(feedback); } surf->pending_presentation_feedbacks.clear(); surf->wlr->data = nullptr; for ( wl_resource *pSwapchain : surf->gamescope_swapchains ) { wl_resource_set_user_data( pSwapchain, nullptr ); } delete surf; } static void wlserver_new_surface(struct wl_listener *l, void *data) { struct wlr_surface *wlr_surf = (struct wlr_surface *)data; uint32_t id = wl_resource_get_id(wlr_surf->resource); wlserver_wl_surface_info *wl_surface_info = new wlserver_wl_surface_info; wl_surface_info->wlr = wlr_surf; wl_surface_info->destroy.notify = handle_wl_surface_destroy; wl_signal_add( &wlr_surf->events.destroy, &wl_surface_info->destroy ); wl_surface_info->commit.notify = handle_wl_surface_commit; wl_signal_add( &wlr_surf->events.commit, &wl_surface_info->commit ); wlr_surf->data = wl_surface_info; struct wlserver_x11_surface_info *s, *tmp; wl_list_for_each_safe(s, tmp, &pending_surfaces, pending_link) { if (s->wl_id == id && s->main_surface == nullptr) { wlserver_x11_surface_info_set_wlr( s, wlr_surf, false ); } } } static struct wl_listener new_surface_listener = { .notify = wlserver_new_surface }; void gamescope_xwayland_server_t::destroy_content_override( struct wlserver_content_override *co ) { #ifdef GAMESCOPE_SWAPCHAIN_DEBUG wl_log.infof( "destroy_content_override REAL: co: %p co->surface: %p co->x11_window: 0x%x co->gamescope_swapchain: %p", co, co->surface, co->x11_window, co->gamescope_swapchain ); #endif if ( co->surface ) { wlserver_wl_surface_info *wl_surface_info = get_wl_surface_info( co->surface ); if ( wl_surface_info ) wl_surface_info->x11_surface = nullptr; } if ( co->gamescope_swapchain ) { gamescope_swapchain_send_retired(co->gamescope_swapchain); } wl_list_remove( &co->surface_destroy_listener.link ); content_overrides.erase( co->x11_window ); free( co ); } void gamescope_xwayland_server_t::destroy_content_override( struct wlserver_x11_surface_info *x11_surface, struct wlr_surface *surf ) { #ifdef GAMESCOPE_SWAPCHAIN_DEBUG wl_log.infof( "destroy_content_override LOOKUP: x11_surface: %p x11_window: 0x%x surf: %p", x11_surface, x11_surface->x11_id, surf ); #endif auto iter = content_overrides.find( x11_surface->x11_id ); if (iter == content_overrides.end()) return; if ( x11_surface->override_surface == surf ) x11_surface->override_surface = nullptr; struct wlserver_content_override *co = iter->second; #ifdef GAMESCOPE_SWAPCHAIN_DEBUG wl_log.infof( "destroy_content_override LOOKUP FOUND: x11_surface: %p x11_window: 0x%x surf: %p co: %p co->surface: %p", x11_surface, x11_surface->x11_id, surf, co, co->surface ); #endif co->gamescope_swapchain = nullptr; if (co->surface == surf) destroy_content_override(iter->second); } static void content_override_handle_surface_destroy( struct wl_listener *listener, void *data ) { struct wlserver_content_override *co = wl_container_of( listener, co, surface_destroy_listener ); gamescope_xwayland_server_t *server = co->server; assert( server ); server->destroy_content_override( co ); } static void gamescope_swapchain_destroy_co( struct wl_resource *resource ); void gamescope_xwayland_server_t::handle_override_window_content( struct wl_client *client, struct wl_resource *gamescope_swapchain_resource, struct wlr_surface *surface, uint32_t x11_window ) { wlserver_x11_surface_info *x11_surface = lookup_x11_surface_info_from_xid( this, x11_window ); // If we found an x11_surface, go back up to our parent. if ( x11_surface ) x11_window = x11_surface->x11_id; #ifdef GAMESCOPE_SWAPCHAIN_DEBUG wl_log.infof( "handle_override_window_content: (1) x11_window: 0x%x swapchain_resource: %p surface: %p", x11_window, gamescope_swapchain_resource, surface ); #endif if ( content_overrides.count( x11_window ) ) { if ( content_overrides[x11_window]->gamescope_swapchain == gamescope_swapchain_resource ) return; #ifdef GAMESCOPE_SWAPCHAIN_DEBUG wl_log.infof( "handle_override_window_content: (2) DESTROYING x11_window: 0x%x old_swapchain: %p new_swapchain: %p", x11_window, content_overrides[x11_window]->gamescope_swapchain, gamescope_swapchain_resource ); #endif destroy_content_override( content_overrides[ x11_window ] ); } if ( gamescope_swapchain_resource ) { gamescope_swapchain_destroy_co( gamescope_swapchain_resource ); } struct wlserver_content_override *co = (struct wlserver_content_override *)calloc(1, sizeof(*co)); co->server = this; co->surface = surface; co->x11_window = x11_window; co->gamescope_swapchain = gamescope_swapchain_resource; co->surface_destroy_listener.notify = content_override_handle_surface_destroy; wl_signal_add( &surface->events.destroy, &co->surface_destroy_listener ); content_overrides[ x11_window ] = co; #ifdef GAMESCOPE_SWAPCHAIN_DEBUG wl_log.infof( "handle_override_window_content: (3) x11_window: 0x%x swapchain_resource: %p surface: %p co: %p", x11_window, gamescope_swapchain_resource, surface, co ); #endif if ( x11_surface ) wlserver_x11_surface_info_set_wlr( x11_surface, surface, true ); if ( x11_surface ) { for (auto it = g_PendingCommits.begin(); it != g_PendingCommits.end();) { if (it->surf == surface) { PendingCommit_t pending = *it; // Still have the buffer lock from before... assert(x11_surface); assert(x11_surface->xwayland_server); x11_surface->xwayland_server->wayland_commit( pending.surf, pending.buf ); it = g_PendingCommits.erase(it); } else { it++; } } } } struct wl_client *gamescope_xwayland_server_t::get_client() { if (!xwayland_server) return nullptr; return xwayland_server->client; } struct wlr_output *gamescope_xwayland_server_t::get_output() { return output; } struct wlr_output_state *gamescope_xwayland_server_t::get_output_state() { return output_state; } static void gamescope_xwayland_handle_override_window_content( struct wl_client *client, struct wl_resource *resource, struct wl_resource *surface_resource, uint32_t x11_window ) { // This should ideally use the surface's xwayland, but we don't know it. // We probably need to change our override_window_content protocol to add a // xwayland socket name. // // Right now, the surface -> xwayland association comes from the // handle_wl_id stuff from steamcompmgr. // However, this surface has no associated X window, and won't recieve // wl_id stuff as it's meant to replace another window's surface // which we can't do without knowing the x11_window's xwayland server // here for it to do that override logic in the first place. // // So... Just assume it comes from server 0 for now. gamescope_xwayland_server_t *server = wlserver_get_xwayland_server( 0 ); assert( server ); struct wlr_surface *surface = wlr_surface_from_resource( surface_resource ); server->handle_override_window_content(client, nullptr, surface, x11_window); } static void gamescope_xwayland_handle_destroy( struct wl_client *client, struct wl_resource *resource ) { wl_resource_destroy( resource ); } static const struct gamescope_xwayland_interface gamescope_xwayland_impl = { .destroy = gamescope_xwayland_handle_destroy, .override_window_content = gamescope_xwayland_handle_override_window_content, }; static void gamescope_xwayland_bind( struct wl_client *client, void *data, uint32_t version, uint32_t id ) { struct wl_resource *resource = wl_resource_create( client, &gamescope_xwayland_interface, version, id ); wl_resource_set_implementation( resource, &gamescope_xwayland_impl, NULL, NULL ); } static void create_gamescope_xwayland( void ) { uint32_t version = 1; wl_global_create( wlserver.display, &gamescope_xwayland_interface, version, NULL, gamescope_xwayland_bind ); } static void gamescope_swapchain_destroy_co( struct wl_resource *resource ) { #ifdef GAMESCOPE_SWAPCHAIN_DEBUG wl_log.infof( "gamescope_swapchain_destroy_co swapchain: %p", resource ); #endif wlserver_wl_surface_info *wl_surface_info = (wlserver_wl_surface_info *)wl_resource_get_user_data( resource ); if ( wl_surface_info ) { #ifdef GAMESCOPE_SWAPCHAIN_DEBUG wl_log.infof( "gamescope_swapchain_destroy_co swapchain: %p GOT WAYLAND SURFACE", resource ); #endif wlserver_x11_surface_info *x11_surface = wl_surface_info->x11_surface; if (x11_surface) { #ifdef GAMESCOPE_SWAPCHAIN_DEBUG wl_log.infof( "gamescope_swapchain_destroy_co swapchain: %p GOT X11 SURFACE", resource ); #endif x11_surface->xwayland_server->destroy_content_override( x11_surface, wl_surface_info->wlr ); } } } static void gamescope_swapchain_handle_resource_destroy( struct wl_resource *resource ) { #ifdef GAMESCOPE_SWAPCHAIN_DEBUG wl_log.infof( "gamescope_swapchain_handle_resource_destroy swapchain: %p", resource ); #endif wlserver_wl_surface_info *wl_surface_info = (wlserver_wl_surface_info *)wl_resource_get_user_data( resource ); if ( wl_surface_info ) { gamescope_swapchain_destroy_co( resource ); std::erase(wl_surface_info->gamescope_swapchains, resource); } } static void gamescope_swapchain_destroy( struct wl_client *client, struct wl_resource *resource ) { wl_resource_destroy( resource ); } static void gamescope_swapchain_override_window_content( struct wl_client *client, struct wl_resource *resource, uint32_t server_id, uint32_t x11_window ) { wlserver_wl_surface_info *wl_surface_info = (wlserver_wl_surface_info *)wl_resource_get_user_data( resource ); gamescope_xwayland_server_t *server = wlserver_get_xwayland_server( server_id ); assert( server ); server->handle_override_window_content(client, resource, wl_surface_info->wlr, x11_window); } static void gamescope_swapchain_swapchain_feedback( struct wl_client *client, struct wl_resource *resource, uint32_t image_count, uint32_t vk_format, uint32_t vk_colorspace, uint32_t vk_composite_alpha, uint32_t vk_pre_transform, uint32_t vk_clipped, const char *vk_engine_name) { wlserver_wl_surface_info *wl_info = (wlserver_wl_surface_info *)wl_resource_get_user_data( resource ); if ( wl_info ) { wl_info->swapchain_feedback = std::make_unique(wlserver_vk_swapchain_feedback{ .image_count = image_count, .vk_format = VkFormat(vk_format), .vk_colorspace = VkColorSpaceKHR(vk_colorspace), .vk_composite_alpha = VkCompositeAlphaFlagBitsKHR(vk_composite_alpha), .vk_pre_transform = VkSurfaceTransformFlagBitsKHR(vk_pre_transform), .vk_clipped = VkBool32(vk_clipped), .vk_engine_name = std::make_shared(vk_engine_name), .hdr_metadata_blob = nullptr, }); } } static void gamescope_swapchain_set_hdr_metadata( struct wl_client *client, struct wl_resource *resource, uint32_t display_primary_red_x, uint32_t display_primary_red_y, uint32_t display_primary_green_x, uint32_t display_primary_green_y, uint32_t display_primary_blue_x, uint32_t display_primary_blue_y, uint32_t white_point_x, uint32_t white_point_y, uint32_t max_display_mastering_luminance, uint32_t min_display_mastering_luminance, uint32_t max_cll, uint32_t max_fall) { wlserver_wl_surface_info *wl_info = (wlserver_wl_surface_info *)wl_resource_get_user_data( resource ); if ( wl_info ) { if ( !wl_info->swapchain_feedback ) { wl_log.errorf("set_hdr_metadata with no swapchain_feedback."); return; } // Check validity of this metadata, // if it's garbage, just toss it... if (!max_cll || !max_fall || (!white_point_x && !white_point_y)) return; hdr_output_metadata metadata = {}; metadata.metadata_type = 0; hdr_metadata_infoframe& infoframe = metadata.hdmi_metadata_type1; infoframe.eotf = HDMI_EOTF_ST2084; infoframe.metadata_type = 0; infoframe.display_primaries[0].x = display_primary_red_x; infoframe.display_primaries[0].y = display_primary_red_y; infoframe.display_primaries[1].x = display_primary_green_x; infoframe.display_primaries[1].y = display_primary_green_y; infoframe.display_primaries[2].x = display_primary_blue_x; infoframe.display_primaries[2].y = display_primary_blue_y; infoframe.white_point.x = white_point_x; infoframe.white_point.y = white_point_y; infoframe.max_display_mastering_luminance = max_display_mastering_luminance; infoframe.min_display_mastering_luminance = min_display_mastering_luminance; infoframe.max_cll = max_cll; infoframe.max_fall = max_fall; wl_info->swapchain_feedback->hdr_metadata_blob = GetBackend()->CreateBackendBlob( metadata ); } } static void gamescope_swapchain_set_present_time( struct wl_client *client, struct wl_resource *resource, uint32_t present_id, uint32_t desired_present_time_hi, uint32_t desired_present_time_lo) { wlserver_wl_surface_info *wl_info = (wlserver_wl_surface_info *)wl_resource_get_user_data( resource ); if ( wl_info ) { wl_info->present_id = present_id; wl_info->desired_present_time = (uint64_t(desired_present_time_hi) << 32) | desired_present_time_lo; } } static void gamescope_swapchain_set_present_mode( struct wl_client *client, struct wl_resource *resource, uint32_t present_mode ) { wlserver_wl_surface_info *wl_info = (wlserver_wl_surface_info *)wl_resource_get_user_data( resource ); if ( wl_info ) { wl_info->oCurrentPresentMode = VkPresentModeKHR( present_mode ); } } static const struct gamescope_swapchain_interface gamescope_swapchain_impl = { .destroy = gamescope_swapchain_destroy, .override_window_content = gamescope_swapchain_override_window_content, .swapchain_feedback = gamescope_swapchain_swapchain_feedback, .set_present_mode = gamescope_swapchain_set_present_mode, .set_hdr_metadata = gamescope_swapchain_set_hdr_metadata, .set_present_time = gamescope_swapchain_set_present_time, }; static void gamescope_swapchain_factory_v2_destroy( struct wl_client *client, struct wl_resource *resource ) { wl_resource_destroy( resource ); } static void gamescope_swapchain_factory_v2_create_swapchain( struct wl_client *client, struct wl_resource *resource, struct wl_resource *surface_resource, uint32_t id ) { struct wlr_surface *surface = wlr_surface_from_resource( surface_resource ); wlserver_wl_surface_info *wl_surface_info = get_wl_surface_info(surface); struct wl_resource *gamescope_swapchain_resource = wl_resource_create( client, &gamescope_swapchain_interface, wl_resource_get_version( resource ), id ); wl_resource_set_implementation( gamescope_swapchain_resource, &gamescope_swapchain_impl, wl_surface_info, gamescope_swapchain_handle_resource_destroy ); if (wl_surface_info->gamescope_swapchains.size()) wl_log.errorf("create_swapchain: Surface already had a gamescope_swapchain! Warning!"); wl_surface_info->gamescope_swapchains.emplace_back( gamescope_swapchain_resource ); } static const struct gamescope_swapchain_factory_v2_interface gamescope_swapchain_factory_v2_impl = { .destroy = gamescope_swapchain_factory_v2_destroy, .create_swapchain = gamescope_swapchain_factory_v2_create_swapchain, }; static void gamescope_swapchain_factory_v2_bind( struct wl_client *client, void *data, uint32_t version, uint32_t id ) { struct wl_resource *resource = wl_resource_create( client, &gamescope_swapchain_factory_v2_interface, version, id ); wl_resource_set_implementation( resource, &gamescope_swapchain_factory_v2_impl, NULL, NULL ); } static void create_gamescope_swapchain_factory_v2( void ) { uint32_t version = 1; wl_global_create( wlserver.display, &gamescope_swapchain_factory_v2_interface, version, NULL, gamescope_swapchain_factory_v2_bind ); } #if HAVE_PIPEWIRE static void gamescope_pipewire_handle_destroy( struct wl_client *client, struct wl_resource *resource ) { wl_resource_destroy( resource ); } static const struct gamescope_pipewire_interface gamescope_pipewire_impl = { .destroy = gamescope_pipewire_handle_destroy, }; static void gamescope_pipewire_bind( struct wl_client *client, void *data, uint32_t version, uint32_t id ) { struct wl_resource *resource = wl_resource_create( client, &gamescope_pipewire_interface, version, id ); wl_resource_set_implementation( resource, &gamescope_pipewire_impl, NULL, NULL ); gamescope_pipewire_send_stream_node( resource, get_pipewire_stream_node_id() ); } static void create_gamescope_pipewire( void ) { uint32_t version = 1; wl_global_create( wlserver.display, &gamescope_pipewire_interface, version, NULL, gamescope_pipewire_bind ); } #endif // static void gamescope_control_set_app_target_refresh_cycle( struct wl_client *client, struct wl_resource *resource, uint32_t fps, uint32_t flags ) { auto display_type = gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL; if ( flags & GAMESCOPE_CONTROL_TARGET_REFRESH_CYCLE_FLAG_INTERNAL_DISPLAY ) display_type = gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL; steamcompmgr_set_app_refresh_cycle_override( display_type, fps, !!( flags & GAMESCOPE_CONTROL_TARGET_REFRESH_CYCLE_FLAG_ALLOW_REFRESH_SWITCHING ), !( flags & GAMESCOPE_CONTROL_TARGET_REFRESH_CYCLE_FLAG_ONLY_CHANGE_REFRESH_RATE ) ); } static void gamescope_control_take_screenshot( struct wl_client *client, struct wl_resource *resource, const char *path, uint32_t type, uint32_t flags ) { gamescope::CScreenshotManager::Get().TakeScreenshot( gamescope::GamescopeScreenshotInfo { .szScreenshotPath = path, .eScreenshotType = (gamescope_control_screenshot_type)type, .uScreenshotFlags = flags, .bWaylandRequested = true, } ); } void drm_sleep_screen( gamescope::GamescopeScreenType eType, bool bSleep ); static void gamescope_control_display_sleep( struct wl_client *client, struct wl_resource *resource, uint32_t display_type_flags, uint32_t flags ) { if ( flags & ( GAMESCOPE_CONTROL_DISPLAY_SLEEP_FLAGS_SLEEP | GAMESCOPE_CONTROL_DISPLAY_SLEEP_FLAGS_WAKE ) ) { const bool sleep = !!( flags & GAMESCOPE_CONTROL_DISPLAY_SLEEP_FLAGS_SLEEP ); if ( display_type_flags & GAMESCOPE_CONTROL_DISPLAY_TYPE_FLAGS_EXTERNAL_DISPLAY ) drm_sleep_screen( gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL, sleep ); if ( display_type_flags & GAMESCOPE_CONTROL_DISPLAY_TYPE_FLAGS_INTERNAL_DISPLAY ) drm_sleep_screen( gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL, sleep ); } } extern gamescope::ConVar cv_overlay_unmultiplied_alpha; extern std::atomic> g_ColorMgmtLooks[EOTF_Count]; static gamescope::ConCommand cc_set_look("set_look", "Set a look for a specific EOTF. Eg. set_look mylook.cube (g22 only), set_look pq mylook.cube, set_look mylook_g22.cube mylook_pq.cube", []( std::span args ) { if ( args.size() == 2 ) { std::string arg1 = std::string{ args[1] }; bool bRaisesBlackLevelFloor = false; g_ColorMgmtLooks[ EOTF_Gamma22 ] = LoadCubeLut( arg1.c_str(), bRaisesBlackLevelFloor ); cv_overlay_unmultiplied_alpha = bRaisesBlackLevelFloor; g_ColorMgmt.pending.externalDirtyCtr++; hasRepaint = true; } else if ( args.size() == 3 ) { std::string arg2 = std::string{ args[2] }; bool bRaisesBlackLevelFloor = false; if ( args[1] == "g22" || args[1] == "G22") { g_ColorMgmtLooks[ EOTF_Gamma22 ] = LoadCubeLut( arg2.c_str(), bRaisesBlackLevelFloor ); } else if ( args[1] == "pq" || args[1] == "PQ" ) { g_ColorMgmtLooks[ EOTF_PQ ] = LoadCubeLut( arg2.c_str(), bRaisesBlackLevelFloor ); } else { std::string arg1 = std::string{ args[1] }; std::shared_ptr pG22LUT; std::shared_ptr pPQLUT; bool bDummy = false; pG22LUT = LoadCubeLut( arg1.c_str(), bRaisesBlackLevelFloor ); pPQLUT = LoadCubeLut( arg2.c_str(), bDummy ); g_ColorMgmtLooks[ EOTF_Gamma22 ] = pG22LUT; g_ColorMgmtLooks[ EOTF_PQ ] = pPQLUT; } cv_overlay_unmultiplied_alpha = bRaisesBlackLevelFloor; g_ColorMgmt.pending.externalDirtyCtr++; hasRepaint = true; } else { cv_overlay_unmultiplied_alpha = false; g_ColorMgmtLooks[ EOTF_Gamma22 ] = nullptr; g_ColorMgmtLooks[ EOTF_PQ ] = nullptr; g_ColorMgmt.pending.externalDirtyCtr++; hasRepaint = true; } }); static void gamescope_control_set_look( struct wl_client *client, struct wl_resource *resource, int g22_fd, int pq_fd, uint32_t flags ) { std::shared_ptr pG22LUT; std::shared_ptr pPQLUT; bool bRaisesBlackLevelFloor = false; if ( g22_fd >= 0 ) { // takes ownership of FD. FILE *pG22 = fdopen( g22_fd, "r" ); if ( pG22 ) { pG22LUT = LoadCubeLut( pG22, bRaisesBlackLevelFloor ); fclose( pG22 ); } else { wl_log.errorf_errno( "gamescope_control_set_look error opening g22 lut" ); close( g22_fd ); } g22_fd = -1; } if ( pq_fd >= 0 ) { // takes ownership of FD. FILE *pPQ = fdopen( pq_fd, "r" ); if ( pPQ ) { bool bDummy = false; pPQLUT = LoadCubeLut( pPQ, bDummy ); fclose( pPQ ); } else { wl_log.errorf_errno( "gamescope_control_set_look error opening pq lut" ); close( pq_fd ); } pq_fd = -1; } cv_overlay_unmultiplied_alpha = bRaisesBlackLevelFloor; g_ColorMgmtLooks[ EOTF_Gamma22 ] = pG22LUT; g_ColorMgmtLooks[ EOTF_PQ ] = pPQLUT; g_ColorMgmt.pending.externalDirtyCtr++; hasRepaint = true; } static void gamescope_control_unset_look( struct wl_client *client, struct wl_resource *resource ) { cv_overlay_unmultiplied_alpha = false; g_ColorMgmtLooks[ EOTF_Gamma22 ] = nullptr; g_ColorMgmtLooks[ EOTF_PQ ] = nullptr; g_ColorMgmt.pending.externalDirtyCtr++; hasRepaint = true; } static void gamescope_control_handle_destroy( struct wl_client *client, struct wl_resource *resource ) { wl_resource_destroy( resource ); } static const struct gamescope_control_interface gamescope_control_impl = { .destroy = gamescope_control_handle_destroy, .set_app_target_refresh_cycle = gamescope_control_set_app_target_refresh_cycle, .take_screenshot = gamescope_control_take_screenshot, .display_sleep = gamescope_control_display_sleep, .set_look = gamescope_control_set_look, .unset_look = gamescope_control_unset_look, }; static uint32_t get_conn_display_info_flags() { gamescope::IBackendConnector *pConn = GetBackend()->GetCurrentConnector(); if ( !pConn ) return 0; uint32_t flags = 0; if ( pConn->GetScreenType() == gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL ) flags |= GAMESCOPE_CONTROL_DISPLAY_FLAG_INTERNAL_DISPLAY; if ( pConn->SupportsVRR() ) flags |= GAMESCOPE_CONTROL_DISPLAY_FLAG_SUPPORTS_VRR; if ( pConn->GetHDRInfo().bExposeHDRSupport ) flags |= GAMESCOPE_CONTROL_DISPLAY_FLAG_SUPPORTS_HDR; return flags; } void wlserver_send_gamescope_control( wl_resource *control ) { assert( wlserver_is_lock_held() ); gamescope::IBackendConnector *pConn = GetBackend()->GetCurrentConnector(); if ( !pConn ) return; uint32_t flags = get_conn_display_info_flags(); struct wl_array display_rates; wl_array_init(&display_rates); if ( pConn->GetValidDynamicRefreshRates().size() ) { for ( uint32_t uRateHz : pConn->GetValidDynamicRefreshRates() ) { uint32_t *ptr = (uint32_t *)wl_array_add( &display_rates, sizeof( uint32_t ) ); *ptr = uRateHz; } } else if ( g_nOutputRefresh > 0 ) { uint32_t *ptr = (uint32_t *)wl_array_add( &display_rates, sizeof(uint32_t) ); *ptr = (uint32_t)gamescope::ConvertmHzToHz( g_nOutputRefresh ); } gamescope_control_send_active_display_info( control, pConn->GetName(), pConn->GetMake(), pConn->GetModel(), flags, &display_rates ); wl_array_release(&display_rates); } static void gamescope_control_bind( struct wl_client *client, void *data, uint32_t version, uint32_t id ) { struct wl_resource *resource = wl_resource_create( client, &gamescope_control_interface, version, id ); wl_resource_set_implementation( resource, &gamescope_control_impl, NULL, [](struct wl_resource *resource) { std::erase_if(wlserver.gamescope_controls, [=](struct wl_resource *control) { return control == resource; }); }); // Send feature support gamescope_control_send_feature_support( resource, GAMESCOPE_CONTROL_FEATURE_RESHADE_SHADERS, 1, 0 ); gamescope_control_send_feature_support( resource, GAMESCOPE_CONTROL_FEATURE_DISPLAY_INFO, 1, 0 ); gamescope_control_send_feature_support( resource, GAMESCOPE_CONTROL_FEATURE_PIXEL_FILTER, 1, 0 ); gamescope_control_send_feature_support( resource, GAMESCOPE_CONTROL_FEATURE_REFRESH_CYCLE_ONLY_CHANGE_REFRESH_RATE, 1, 0 ); gamescope_control_send_feature_support( resource, GAMESCOPE_CONTROL_FEATURE_MURA_CORRECTION, 1, 0 ); gamescope_control_send_feature_support( resource, GAMESCOPE_CONTROL_FEATURE_LOOK, 1, 0 ); gamescope_control_send_feature_support( resource, GAMESCOPE_CONTROL_FEATURE_DONE, 0, 0 ); wlserver_send_gamescope_control( resource ); wlserver.gamescope_controls.push_back(resource); } static void create_gamescope_control( void ) { uint32_t version = 5; wl_global_create( wlserver.display, &gamescope_control_interface, version, NULL, gamescope_control_bind ); } //////////////////////// // gamescope_private //////////////////////// static void gamescope_private_execute( struct wl_client *client, struct wl_resource *resource, const char *cvar_name, const char *value ) { std::vector args; args.emplace_back( cvar_name ); args.emplace_back( value ); if ( gamescope::ConCommand::Exec( std::span{ args } ) ) gamescope_private_send_command_executed( resource ); } static void gamescope_private_handle_destroy( struct wl_client *client, struct wl_resource *resource ) { wl_resource_destroy( resource ); } static const struct gamescope_private_interface gamescope_private_impl = { .destroy = gamescope_private_handle_destroy, .execute = gamescope_private_execute, }; static void gamescope_private_bind( struct wl_client *client, void *data, uint32_t version, uint32_t id ) { struct wl_resource *resource = wl_resource_create( client, &gamescope_private_interface, version, id ); console_log.m_LoggingListeners[(uintptr_t)resource] = [ resource ]( LogPriority ePriority, std::string_view psvScope, std::string_view psvText ) { if ( !wlserver_is_lock_held() ) return; // Can't send a length with string on Wayland api currently... :( std::string sText = std::string{ psvText }; gamescope_private_send_log( resource, sText.c_str() ); }; wl_resource_set_implementation( resource, &gamescope_private_impl, NULL, [](struct wl_resource *resource) { console_log.m_LoggingListeners.erase( (uintptr_t)resource ); }); } static void create_gamescope_private( void ) { uint32_t version = 1; wl_global_create( wlserver.display, &gamescope_private_interface, version, NULL, gamescope_private_bind ); } static void create_explicit_sync() { new gamescope::WaylandServer::CLinuxDrmSyncobj( wlserver.display ); } static void create_reshade() { new gamescope::WaylandServer::CReshade( wlserver.display ); } //////////////////////// // presentation-time //////////////////////// static void presentation_time_destroy( struct wl_client *client, struct wl_resource *resource ) { wl_resource_destroy( resource ); } static void presentation_time_feedback( struct wl_client *client, struct wl_resource *resource, struct wl_resource *surface_resource, uint32_t id ) { struct wlr_surface *surface = wlr_surface_from_resource( surface_resource ); wlserver_wl_surface_info *wl_surface_info = get_wl_surface_info(surface); struct wl_resource *presentation_feedback_resource = wl_resource_create( client, &wp_presentation_feedback_interface, wl_resource_get_version( resource ), id ); wl_resource_set_implementation( presentation_feedback_resource, NULL, wl_surface_info, NULL ); wl_surface_info->pending_presentation_feedbacks.emplace_back(presentation_feedback_resource); } static const struct wp_presentation_interface presentation_time_impl = { .destroy = presentation_time_destroy, .feedback = presentation_time_feedback, }; static void presentation_time_bind( struct wl_client *client, void *data, uint32_t version, uint32_t id ) { struct wl_resource *resource = wl_resource_create( client, &wp_presentation_interface, version, id ); wl_resource_set_implementation( resource, &presentation_time_impl, NULL, NULL ); wp_presentation_send_clock_id( resource, CLOCK_MONOTONIC ); } static void create_presentation_time( void ) { uint32_t version = 1; wl_global_create( wlserver.display, &wp_presentation_interface, version, NULL, presentation_time_bind ); } void wlserver_presentation_feedback_presented( struct wlr_surface *surface, std::vector& presentation_feedbacks, uint64_t last_refresh_nsec, uint64_t refresh_cycle ) { wlserver_wl_surface_info *wl_surface_info = get_wl_surface_info(surface); if ( !wl_surface_info ) return; uint32_t flags = 0; // Don't know when we want to send this. // Not useful for an app to know. flags |= WP_PRESENTATION_FEEDBACK_KIND_VSYNC; // We set HW clock because it's based on the HW clock of the last page flip. flags |= WP_PRESENTATION_FEEDBACK_KIND_HW_CLOCK; // We don't set HW_COMPLETION because we actually kinda are using a timer here. // We want to signal this event at latest latch time, but with the info of the refresh // cadence we want to track. // Probably. Not delaying sending this to find out. // Not useful for an app to know. flags |= WP_PRESENTATION_FEEDBACK_KIND_ZERO_COPY; wl_surface_info->sequence++; for (auto& feedback : presentation_feedbacks) { timespec last_refresh_ts; last_refresh_ts.tv_sec = time_t(last_refresh_nsec / 1'000'000'000ul); last_refresh_ts.tv_nsec = long(last_refresh_nsec % 1'000'000'000ul); wp_presentation_feedback_send_presented( feedback, last_refresh_ts.tv_sec >> 32, last_refresh_ts.tv_sec & 0xffffffff, last_refresh_ts.tv_nsec, uint32_t(refresh_cycle), wl_surface_info->sequence >> 32, wl_surface_info->sequence & 0xffffffff, flags); wl_resource_destroy(feedback); } presentation_feedbacks.clear(); } void wlserver_presentation_feedback_discard( struct wlr_surface *surface, std::vector& presentation_feedbacks ) { wlserver_wl_surface_info *wl_surface_info = get_wl_surface_info(surface); if ( !wl_surface_info ) return; wl_surface_info->sequence++; for (auto& feedback : presentation_feedbacks) { wp_presentation_feedback_send_discarded(feedback); } presentation_feedbacks.clear(); } /////////////////////// void wlserver_past_present_timing( struct wlr_surface *surface, uint32_t present_id, uint64_t desired_present_time, uint64_t actual_present_time, uint64_t earliest_present_time, uint64_t present_margin ) { wlserver_wl_surface_info *wl_info = get_wl_surface_info( surface ); if ( !wl_info ) return; for (auto& swapchain : wl_info->gamescope_swapchains) { gamescope_swapchain_send_past_present_timing( swapchain, present_id, desired_present_time >> 32, desired_present_time & 0xffffffff, actual_present_time >> 32, actual_present_time & 0xffffffff, earliest_present_time >> 32, earliest_present_time & 0xffffffff, present_margin >> 32, present_margin & 0xffffffff); } } void wlserver_refresh_cycle( struct wlr_surface *surface, uint64_t refresh_cycle ) { wlserver_wl_surface_info *wl_info = get_wl_surface_info( surface ); if ( !wl_info ) return; for (auto& swapchain : wl_info->gamescope_swapchains) { gamescope_swapchain_send_refresh_cycle( swapchain, refresh_cycle >> 32, refresh_cycle & 0xffffffff); } } /////////////////////// #if HAVE_SESSION bool wlsession_active() { return wlserver.wlr.session->active; } static void handle_session_active( struct wl_listener *listener, void *data ) { GetBackend()->DirtyState( wlserver.wlr.session->active, wlserver.wlr.session->active ); wl_log.infof( "Session %s", wlserver.wlr.session->active ? "resumed" : "paused" ); } #endif static void handle_wlr_log(enum wlr_log_importance importance, const char *fmt, va_list args) { enum LogPriority prio; switch (importance) { case WLR_ERROR: prio = LOG_ERROR; break; case WLR_INFO: prio = LOG_INFO; break; default: prio = LOG_DEBUG; break; } wl_log.vlogf(prio, fmt, args); } void wlserver_set_output_info( const wlserver_output_info *info ) { free(wlserver.output_info.description); wlserver.output_info.description = strdup(info->description); wlserver.output_info.phys_width = info->phys_width; wlserver.output_info.phys_height = info->phys_height; if (wlserver.wlr.xwayland_servers.empty()) return; gamescope_xwayland_server_t *server = wlserver.wlr.xwayland_servers[0].get(); server->update_output_info(); } static bool filter_global(const struct wl_client *client, const struct wl_global *global, void *data) { const struct wl_interface *iface = wl_global_get_interface(global); if ( cv_drm_debug_disable_explicit_sync && iface->name == "wp_linux_drm_syncobj_manager_v1"sv ) return false; if (strcmp(iface->name, wl_output_interface.name) != 0) return true; struct wlr_output *output = (struct wlr_output *) wl_global_get_user_data(global); if (!output) { /* Can happen if the output has been destroyed, but the client hasn't * received the wl_registry.global_remove event yet. */ return false; } if (g_bShutdownWLServer) return false; /* We create one wl_output global per Xwayland server, to easily have * per-server output configuration. Only expose the wl_output belonging to * the server. */ for (size_t i = 0; i < wlserver.wlr.xwayland_servers.size(); i++) { gamescope_xwayland_server_t *server = wlserver.wlr.xwayland_servers[i].get(); if (server && server->get_client() == client) return server->get_output() == output; } if (wlserver.wlr.xwayland_servers.empty()) return false; gamescope_xwayland_server_t *server = wlserver.wlr.xwayland_servers[0].get(); /* If we aren't an xwayland server, then only expose the first wl_output * that's associated with from server 0. */ return server && server->get_output() == output; } bool wlsession_init( void ) { wlr_log_init(WLR_DEBUG, handle_wlr_log); wlserver.display = wl_display_create(); wlserver.event_loop = wl_display_get_event_loop( wlserver.display ); wlserver.wlr.headless_backend = wlr_headless_backend_create( wlserver.event_loop ); wl_display_set_global_filter(wlserver.display, filter_global, nullptr); const struct wlserver_output_info output_info = { .description = "Virtual gamescope output", }; wlserver_set_output_info( &output_info ); #if HAVE_SESSION if ( !GetBackend()->IsSessionBased() ) return true; wlserver.wlr.session = wlr_session_create( wlserver.event_loop ); if ( wlserver.wlr.session == nullptr ) { wl_log.errorf( "Failed to create session" ); return false; } wlserver.session_active.notify = handle_session_active; wl_signal_add( &wlserver.wlr.session->events.active, &wlserver.session_active ); #endif return true; } #if HAVE_SESSION static void kms_device_handle_change( struct wl_listener *listener, void *data ) { GetBackend()->DirtyState(); wl_log.infof( "Got change event for KMS device" ); nudge_steamcompmgr(); } int wlsession_open_kms( const char *device_name ) { if ( device_name != nullptr ) { wlserver.wlr.device = wlr_session_open_file( wlserver.wlr.session, device_name ); if ( wlserver.wlr.device == nullptr ) return -1; } else { ssize_t n = wlr_session_find_gpus( wlserver.wlr.session, 1, &wlserver.wlr.device ); if ( n < 0 ) { wl_log.errorf( "Failed to list GPUs" ); return -1; } if ( n == 0 ) { wl_log.errorf( "No GPU detected" ); return -1; } } struct wl_listener *listener = new wl_listener(); listener->notify = kms_device_handle_change; wl_signal_add( &wlserver.wlr.device->events.change, listener ); return wlserver.wlr.device->fd; } void wlsession_close_kms() { wlr_session_close_file( wlserver.wlr.session, wlserver.wlr.device ); } #endif gamescope_xwayland_server_t::gamescope_xwayland_server_t(wl_display *display) { struct wlr_xwayland_server_options xwayland_options = { .lazy = false, .enable_wm = false, .no_touch_pointer_emulation = true, .force_xrandr_emulation = true, }; xwayland_server = wlr_xwayland_server_create(display, &xwayland_options); wl_signal_add(&xwayland_server->events.ready, &xwayland_ready_listener); output = wlr_headless_add_output(wlserver.wlr.headless_backend, 1280, 720); output_state = new wlr_output_state; wlr_output_state_init(output_state); output->make = strdup("gamescope"); // freed by wlroots output->model = strdup("gamescope"); // freed by wlroots wlr_output_set_name(output, "gamescope"); int refresh = g_nNestedRefresh; if (refresh == 0) { refresh = g_nOutputRefresh; } wlr_output_state_set_enabled(output_state, true); wlr_output_state_set_custom_mode(output_state, g_nNestedWidth, g_nNestedHeight, refresh); if (!wlr_output_commit_state(output, output_state)) { wl_log.errorf("Failed to commit headless output"); abort(); } update_output_info(); wlr_output_create_global(output, wlserver.display); } gamescope_xwayland_server_t::~gamescope_xwayland_server_t() { /* Clear content overrides */ for (auto& co : content_overrides) { wl_list_remove( &co.second->surface_destroy_listener.link ); free( co.second ); } content_overrides.clear(); wlr_xwayland_server_destroy(xwayland_server); xwayland_server = nullptr; wlr_output_destroy(output); delete output_state; } void gamescope_xwayland_server_t::update_output_info() { const auto *info = &wlserver.output_info; output->phys_width = info->phys_width; output->phys_height = info->phys_height; struct wl_resource *resource; wl_resource_for_each(resource, &output->resources) { wl_output_send_geometry(resource, 0, 0, output->phys_width, output->phys_height, output->subpixel, output->make, output->model, output->transform); } wlr_output_schedule_done(output); wlr_output_set_description(output, info->description); } static void xdg_surface_map(struct wl_listener *listener, void *data) { struct wlserver_xdg_surface_info* info = wl_container_of(listener, info, map); info->mapped = true; wlserver.xdg_dirty = true; } static void xdg_surface_unmap(struct wl_listener *listener, void *data) { struct wlserver_xdg_surface_info* info = wl_container_of(listener, info, unmap); info->mapped = false; wlserver.xdg_dirty = true; } static void waylandy_surface_destroy(struct wl_listener *listener, void *data) { struct wlserver_xdg_surface_info* info = wl_container_of(listener, info, destroy); wlserver_wl_surface_info *wlserver_surface = get_wl_surface_info(info->main_surface); if (!wlserver_surface) { wl_log.infof("No base surface info. (destroy)"); return; } { std::unique_lock lock(g_wlserver_xdg_shell_windows_lock); std::erase_if(wlserver.xdg_wins, [=](auto win) { return win.get() == info->win; }); } info->main_surface = nullptr; info->win = nullptr; info->xdg_surface = nullptr; info->layer_surface = nullptr; info->mapped = false; wl_list_remove(&info->map.link); wl_list_remove(&info->unmap.link); wl_list_remove(&info->destroy.link); wlserver_surface->xdg_surface = nullptr; } void xdg_toplevel_new(struct wl_listener *listener, void *data) { } uint32_t get_appid_from_pid( pid_t pid ); wlserver_xdg_surface_info* waylandy_type_surface_new(struct wl_client *client, struct wlr_surface *surface) { wlserver_wl_surface_info *wlserver_surface = get_wl_surface_info(surface); if (!wlserver_surface) { wl_log.infof("No base surface info. (new)"); return nullptr; } auto window = std::make_shared(); { std::unique_lock lock(g_wlserver_xdg_shell_windows_lock); wlserver.xdg_wins.emplace_back(window); } window->seq = ++g_lastWinSeq; window->type = steamcompmgr_win_type_t::XDG; if ( client ) { pid_t nPid = 0; wl_client_get_credentials( client, &nPid, nullptr, nullptr ); window->appID = get_appid_from_pid( nPid ); } window->_window_types.emplace(); static uint32_t s_window_serial = 0; window->xdg().id = ++s_window_serial; wlserver_xdg_surface_info* xdg_surface_info = &window->xdg().surface; xdg_surface_info->main_surface = surface; xdg_surface_info->win = window.get(); wlserver_surface->xdg_surface = xdg_surface_info; xdg_surface_info->map.notify = xdg_surface_map; wl_signal_add(&surface->events.map, &xdg_surface_info->map); xdg_surface_info->unmap.notify = xdg_surface_unmap; wl_signal_add(&surface->events.unmap, &xdg_surface_info->unmap); for (auto it = g_PendingCommits.begin(); it != g_PendingCommits.end();) { if (it->surf == surface) { PendingCommit_t pending = *it; wlserver_xdg_commit(pending.surf, pending.buf); it = g_PendingCommits.erase(it); } else { it++; } } return xdg_surface_info; } void xdg_surface_new(struct wl_listener *listener, void *data) { struct wlr_xdg_surface *xdg_surface = (struct wlr_xdg_surface *)data; wlserver_xdg_surface_info *surface_info = waylandy_type_surface_new(xdg_surface->client->client, xdg_surface->surface); surface_info->destroy.notify = waylandy_surface_destroy; wl_signal_add(&xdg_surface->events.destroy, &surface_info->destroy); surface_info->xdg_surface = xdg_surface; } void layer_shell_surface_new(struct wl_listener *listener, void *data) { struct wlr_layer_surface_v1 *layer_surface = (struct wlr_layer_surface_v1 *)data; wlserver_xdg_surface_info *surface_info = waylandy_type_surface_new(nullptr, layer_surface->surface); surface_info->destroy.notify = waylandy_surface_destroy; wl_signal_add(&layer_surface->events.destroy, &surface_info->destroy); surface_info->layer_surface = layer_surface; surface_info->win->isExternalOverlay = true; } #if HAVE_LIBEIS static gamescope::CAsyncWaiter g_LibEisWaiter( "gamescope-eis" ); // TODO: Move me into some ownership of eg. wlserver. static std::unique_ptr g_InputServer; #endif bool wlserver_init( void ) { assert( wlserver.display != nullptr ); wl_list_init(&pending_surfaces); wlserver.wlr.multi_backend = wlr_multi_backend_create( wlserver.event_loop ); wlr_multi_backend_add( wlserver.wlr.multi_backend, wlserver.wlr.headless_backend ); assert( wlserver.event_loop && wlserver.wlr.multi_backend ); wl_signal_add( &wlserver.wlr.multi_backend->events.new_input, &new_input_listener ); if ( GetBackend()->IsSessionBased() ) { #if HAVE_DRM wlserver.wlr.libinput_backend = wlr_libinput_backend_create( wlserver.wlr.session ); if ( wlserver.wlr.libinput_backend == NULL) { return false; } wlr_multi_backend_add( wlserver.wlr.multi_backend, wlserver.wlr.libinput_backend ); #endif } // Create a stub wlr_keyboard only used to set the keymap // We need to wait for the backend to be started before adding the device struct wlr_keyboard *kbd = (struct wlr_keyboard *) calloc(1, sizeof(*kbd)); wlr_keyboard_init(kbd, nullptr, "virtual"); wlserver.wlr.virtual_keyboard_device = kbd; // Create a keyboard group to keep all externally connected keyboards // in sync (one single layout and a shared state) struct xkb_context *context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); struct xkb_rule_names rules = { 0 }; rules.rules = getenv("XKB_DEFAULT_RULES"); rules.model = getenv("XKB_DEFAULT_MODEL"); rules.layout = getenv("XKB_DEFAULT_LAYOUT"); rules.variant = getenv("XKB_DEFAULT_VARIANT"); rules.options = getenv("XKB_DEFAULT_OPTIONS"); struct xkb_keymap *keymap = xkb_keymap_new_from_names(context, &rules, XKB_KEYMAP_COMPILE_NO_FLAGS); wlserver.keyboard_group = wlr_keyboard_group_create(); struct wlr_keyboard *keyboard = &wlserver.keyboard_group->keyboard; wlr_keyboard_set_repeat_info(keyboard, 25, 600); wlr_keyboard_set_keymap(keyboard, keymap); wlserver.keyboard_group_modifiers.notify = wlserver_handle_modifiers; wl_signal_add(&keyboard->events.modifiers, &wlserver.keyboard_group_modifiers); wlserver.keyboard_group_key.notify = wlserver_handle_key; wl_signal_add(&keyboard->events.key, &wlserver.keyboard_group_key); wlserver.wlr.renderer = vulkan_renderer_create(); wlr_renderer_init_wl_display(wlserver.wlr.renderer, wlserver.display); wlserver.wlr.compositor = wlr_compositor_create(wlserver.display, 5, wlserver.wlr.renderer); wl_signal_add( &wlserver.wlr.compositor->events.new_surface, &new_surface_listener ); create_ime_manager( &wlserver ); create_reshade(); new gamescope::WaylandServer::CGamescopeActionBindingProtocol( wlserver.display ); create_gamescope_xwayland(); create_gamescope_swapchain_factory_v2(); #if HAVE_PIPEWIRE create_gamescope_pipewire(); #endif create_gamescope_control(); create_gamescope_private(); create_presentation_time(); // Have to make this old ancient thing for compat with older XWayland. // Someday, he will be purged. wlr_drm_create(wlserver.display, wlserver.wlr.renderer); if ( GetBackend()->SupportsExplicitSync() ) { create_explicit_sync(); wl_log.infof( "Using explicit sync when available" ); } wlserver.relative_pointer_manager = wlr_relative_pointer_manager_v1_create(wlserver.display); if ( !wlserver.relative_pointer_manager ) { wl_log.errorf( "Failed to create relative pointer manager" ); return false; } wlserver.constraints = wlr_pointer_constraints_v1_create(wlserver.display); if ( !wlserver.constraints ) { wl_log.errorf( "Failed to create pointer constraints" ); return false; } wlserver.new_pointer_constraint.notify = handle_pointer_constraint; wl_signal_add(&wlserver.constraints->events.new_constraint, &wlserver.new_pointer_constraint); wlserver.xdg_shell = wlr_xdg_shell_create(wlserver.display, 3); if (!wlserver.xdg_shell) { wl_log.infof("Unable to create XDG shell interface"); return false; } wlserver.new_xdg_surface.notify = xdg_surface_new; wlserver.new_xdg_toplevel.notify = xdg_toplevel_new; wl_signal_add(&wlserver.xdg_shell->events.new_surface, &wlserver.new_xdg_surface); wl_signal_add(&wlserver.xdg_shell->events.new_toplevel, &wlserver.new_xdg_toplevel); wlserver.layer_shell_v1 = wlr_layer_shell_v1_create(wlserver.display, 4); if (!wlserver.layer_shell_v1) { wl_log.infof("Unable to create layer shell interface"); return false; } wlserver.new_layer_shell_surface.notify = layer_shell_surface_new; wl_signal_add(&wlserver.layer_shell_v1->events.new_surface, &wlserver.new_layer_shell_surface); int result = -1; int display_slot = 0; while ( result != 0 && display_slot < 128 ) { sprintf( wlserver.wl_display_name, "gamescope-%d", display_slot ); result = wl_display_add_socket( wlserver.display, wlserver.wl_display_name ); display_slot++; } if ( result != 0 ) { wl_log.errorf_errno("Unable to open wayland socket"); wlr_backend_destroy( wlserver.wlr.multi_backend ); return false; } wlserver.wlr.seat = wlr_seat_create(wlserver.display, "seat0"); wlr_seat_set_capabilities( wlserver.wlr.seat, WL_SEAT_CAPABILITY_POINTER | WL_SEAT_CAPABILITY_KEYBOARD | WL_SEAT_CAPABILITY_TOUCH ); wl_log.infof("Running compositor on wayland display '%s'", wlserver.wl_display_name); if (!wlr_backend_start( wlserver.wlr.multi_backend )) { wl_log.errorf("Failed to start backend"); wlr_backend_destroy( wlserver.wlr.multi_backend ); wl_display_destroy(wlserver.display); return false; } wl_signal_emit( &wlserver.wlr.multi_backend->events.new_input, kbd ); #if HAVE_LIBEIS { char szEISocket[ 64 ]; snprintf( szEISocket, sizeof( szEISocket ), "%s-ei", wlserver.wl_display_name ); std::unique_ptr pInputServer = std::make_unique(); if ( pInputServer->Init( szEISocket ) ) { g_InputServer = std::move( pInputServer ); setenv( "LIBEI_SOCKET", szEISocket, 1 ); g_LibEisWaiter.AddWaitable( g_InputServer.get() ); wl_log.infof( "Successfully initialized libei for input emulation!" ); } else { wl_log.errorf( "Initializing libei failed, XTEST will not be available!" ); } } #else wl_log.errorf( "Gamescope built without libei, XTEST will not be available!" ); #endif for (int i = 0; i < g_nXWaylandCount; i++) { auto server = std::make_unique(wlserver.display); wlserver.wlr.xwayland_servers.emplace_back(std::move(server)); } for (size_t i = 0; i < wlserver.wlr.xwayland_servers.size(); i++) { while (!wlserver.wlr.xwayland_servers[i]->is_xwayland_ready()) { wl_display_flush_clients(wlserver.display); if (wl_event_loop_dispatch(wlserver.event_loop, -1) < 0) { wl_log.errorf("wl_event_loop_dispatch failed\n"); return false; } } } return true; } pthread_mutex_t waylock = PTHREAD_MUTEX_INITIALIZER; bool wlserver_is_lock_held(void) { int err = pthread_mutex_trylock(&waylock); if (err == 0) { pthread_mutex_unlock(&waylock); return false; } return true; } void wlserver_lock(void) { pthread_mutex_lock(&waylock); } void wlserver_unlock(bool flush) { if (flush) wl_display_flush_clients(wlserver.display); pthread_mutex_unlock(&waylock); } extern std::mutex g_SteamCompMgrXWaylandServerMutex; static int g_wlserverNudgePipe[2] = {-1, -1}; void wlserver_run(void) { pthread_setname_np( pthread_self(), "gamescope-wl" ); if ( pipe2( g_wlserverNudgePipe, O_CLOEXEC | O_NONBLOCK ) != 0 ) { wl_log.errorf_errno( "wlserver: pipe2 failed" ); exit( 1 ); } struct pollfd pollfds[2] = { { .fd = wl_event_loop_get_fd( wlserver.event_loop ), .events = POLLIN, }, { .fd = g_wlserverNudgePipe[ 0 ], .events = POLLIN, }, }; wlserver.bWaylandServerRunning = true; wlserver.bWaylandServerRunning.notify_all(); while ( !g_bShutdownWLServer ) { int ret = poll( pollfds, 2, -1 ); if ( ret < 0 ) { if ( errno == EINTR || errno == EAGAIN ) continue; wl_log.errorf_errno( "poll failed" ); break; } if ( pollfds[ 0 ].revents & (POLLHUP | POLLERR) ) { wl_log.errorf( "socket %s", ( pollfds[ 0 ].revents & POLLERR ) ? "error" : "closed" ); break; } if ( pollfds[ 0 ].revents & POLLIN ) { // We have wayland stuff to do, do it while locked wlserver_lock(); wl_display_flush_clients(wlserver.display); int ret = wl_event_loop_dispatch(wlserver.event_loop, 0); if (ret < 0) { wlserver_unlock(); break; } wlserver_unlock(); } } wlserver.bWaylandServerRunning = false; wlserver.bWaylandServerRunning.notify_all(); { std::unique_lock lock3(wlserver.xdg_commit_lock); wlserver.xdg_commit_queue.clear(); } { std::unique_lock lock2(g_wlserver_xdg_shell_windows_lock); wlserver.xdg_wins.clear(); } #if HAVE_LIBEIS g_InputServer = nullptr; #endif // Released when steamcompmgr closes. std::unique_lock xwayland_server_guard(g_SteamCompMgrXWaylandServerMutex); // We need to shutdown Xwayland before disconnecting all clients, otherwise // wlroots will restart it automatically. wlserver_lock(); wlserver.wlr.xwayland_servers.clear(); wl_display_destroy_clients(wlserver.display); wl_display_destroy(wlserver.display); wlserver.display = NULL; wlserver_unlock(false); } void wlserver_shutdown() { assert( wlserver_is_lock_held() ); g_bShutdownWLServer = true; if (wlserver.display) { if ( write( g_wlserverNudgePipe[ 1 ], "\n", 1 ) < 0 ) wl_log.errorf_errno( "wlserver_force_shutdown: write failed" ); } } void wlserver_keyboardfocus( struct wlr_surface *surface, bool bConstrain ) { assert( wlserver_is_lock_held() ); if (wlserver.kb_focus_surface != surface) { auto old_wl_surf = get_wl_surface_info( wlserver.kb_focus_surface ); if (old_wl_surf && old_wl_surf->xdg_surface && old_wl_surf->xdg_surface->xdg_surface && old_wl_surf->xdg_surface->xdg_surface->toplevel) wlr_xdg_toplevel_set_activated(old_wl_surf->xdg_surface->xdg_surface->toplevel, false); auto new_wl_surf = get_wl_surface_info( surface ); if (new_wl_surf && new_wl_surf->xdg_surface && new_wl_surf->xdg_surface->xdg_surface && new_wl_surf->xdg_surface->xdg_surface->toplevel) wlr_xdg_toplevel_set_activated(new_wl_surf->xdg_surface->xdg_surface->toplevel, true); } assert( wlserver.wlr.virtual_keyboard_device != nullptr ); wlr_seat_set_keyboard( wlserver.wlr.seat, wlserver.wlr.virtual_keyboard_device ); struct wlr_keyboard *keyboard = wlr_seat_get_keyboard( wlserver.wlr.seat ); if ( keyboard == nullptr ) wlr_seat_keyboard_notify_enter( wlserver.wlr.seat, surface, nullptr, 0, nullptr); else wlr_seat_keyboard_notify_enter( wlserver.wlr.seat, surface, keyboard->keycodes, keyboard->num_keycodes, &keyboard->modifiers); wlserver.kb_focus_surface = surface; if ( bConstrain ) { struct wlr_pointer_constraint_v1 *constraint = wlr_pointer_constraints_v1_constraint_for_surface( wlserver.constraints, surface, wlserver.wlr.seat ); wlserver_constrain_cursor( constraint ); } } bool wlserver_process_hotkeys( wlr_keyboard *keyboard, uint32_t key, bool press ) { xkb_keycode_t keycode = key + 8; xkb_keysym_t keysym = xkb_state_key_get_one_sym( keyboard->xkb_state, keycode ); keysym = NormalizeKeysymForHotkey( keysym ); static std::unordered_set s_setPressedKeySyms; if ( press ) { s_setPressedKeySyms.emplace( keysym ); } else { s_setPressedKeySyms.erase( keysym ); } if ( log_binding.Enabled( LOG_DEBUG ) ) { std::string sPressedKeySymsDebugName = ComputeDebugName( s_setPressedKeySyms ); log_binding.debugf( "Looking for: [%s].", sPressedKeySymsDebugName.c_str() ); } { using namespace gamescope::WaylandServer; std::span ppBindings = CGamescopeActionBinding::GetBindings(); for ( CGamescopeActionBinding *pBinding : ppBindings ) { if ( !pBinding->IsArmed() ) continue; std::span pKeybinds = pBinding->GetKeyboardTriggers(); for ( const Keybind_t &keybind : pKeybinds ) { if ( !pBinding->IsArmed() ) break; if ( s_setPressedKeySyms != keybind.setKeySyms ) continue; if ( pBinding->Execute() ) return true; } } } return false; } void wlserver_key( uint32_t key, bool press, uint32_t time ) { assert( wlserver_is_lock_held() ); wlr_keyboard *keyboard = wlserver.wlr.virtual_keyboard_device; if ( !wlserver_process_hotkeys( keyboard, key, press ) ) { assert( keyboard != nullptr ); wlr_seat_set_keyboard( wlserver.wlr.seat, keyboard ); wlr_seat_keyboard_notify_key( wlserver.wlr.seat, time, key, press ); } bump_input_counter(); } struct wlr_surface *wlserver_surface_to_main_surface( struct wlr_surface *pSurface ) { if ( !pSurface ) return nullptr; wlserver_wl_surface_info *pInfo = get_wl_surface_info( pSurface ); if ( !pInfo ) return nullptr; if ( pInfo->x11_surface ) { wlr_surface *pMain = pInfo->x11_surface->main_surface.load(); if ( pMain ) pSurface = pMain; } return pSurface; } struct wlr_surface *wlserver_surface_to_override_surface( struct wlr_surface *pSurface ) { if ( !pSurface ) return nullptr; wlserver_wl_surface_info *pInfo = get_wl_surface_info( pSurface ); if ( !pInfo ) return nullptr; if ( pInfo->x11_surface ) { wlr_surface *pOverride = pInfo->x11_surface->override_surface.load(); if ( pOverride ) pSurface = pOverride; } return pSurface; } std::pair wlserver_get_surface_extent( struct wlr_surface *pSurface ) { assert( wlserver_is_lock_held() ); pSurface = wlserver_surface_to_override_surface( pSurface ); if ( !pSurface ) return std::make_pair( g_nNestedWidth, g_nNestedHeight ); return std::make_pair( pSurface->current.width, pSurface->current.height ); } bool ShouldDrawCursor(); void wlserver_oncursorevent() { // Don't repaint if we would use a nested cursor. if ( !ShouldDrawCursor() ) return; if ( !wlserver.bCursorHidden && wlserver.bCursorHasImage ) { hasRepaint = true; } } static std::pair wlserver_get_cursor_bounds() { auto [nWidth, nHeight] = wlserver_get_surface_extent( wlserver.mouse_focus_surface ); for ( auto iter : wlserver.current_dropdown_surfaces ) { auto [nDropdownX, nDropdownY] = iter.second; auto [nDropdownWidth, nDropdownHeight] = wlserver_get_surface_extent( iter.first ); nWidth = std::max( nWidth, nDropdownX + nDropdownWidth ); nHeight = std::max( nHeight, nDropdownY + nDropdownHeight ); } return std::make_pair( nWidth, nHeight ); } static void wlserver_clampcursor() { auto [nWidth, nHeight] = wlserver_get_cursor_bounds(); wlserver.mouse_surface_cursorx = std::clamp( wlserver.mouse_surface_cursorx, 0.0, double( std::max( nWidth - 1, 0 ) ) ); wlserver.mouse_surface_cursory = std::clamp( wlserver.mouse_surface_cursory, 0.0, double( std::max( nHeight - 1, 0 ) ) ); } void wlserver_mousefocus( struct wlr_surface *wlrsurface, int x /* = 0 */, int y /* = 0 */ ) { assert( wlserver_is_lock_held() ); if ( wlserver.mouse_focus_surface == wlrsurface ) { wlserver_clampcursor(); } else { wlserver.mouse_focus_surface = wlrsurface; auto [nWidth, nHeight] = wlserver_get_surface_extent( wlrsurface ); if ( x < nWidth && y < nHeight ) { wlserver.mouse_surface_cursorx = x; wlserver.mouse_surface_cursory = y; } else { wlserver.mouse_surface_cursorx = nWidth / 2.0; wlserver.mouse_surface_cursory = nHeight / 2.0; } } wlserver_oncursorevent(); wlr_seat_pointer_warp( wlserver.wlr.seat, wlserver.mouse_surface_cursorx, wlserver.mouse_surface_cursory ); wlr_seat_pointer_notify_enter( wlserver.wlr.seat, wlrsurface, wlserver.mouse_surface_cursorx, wlserver.mouse_surface_cursory ); } void wlserver_clear_dropdowns() { wlserver.current_dropdown_surfaces.clear(); } void wlserver_notify_dropdown( struct wlr_surface *wlrsurface, int nX, int nY ) { wlserver.current_dropdown_surfaces[wlrsurface] = std::make_pair( nX, nY ); } void wlserver_mousehide() { wlserver.ulLastMovedCursorTime = 0; if ( wlserver.bCursorHidden != true ) { wlserver.bCursorHidden = true; hasRepaint = true; } } struct GamescopePointerConstraint { struct wlr_pointer_constraint_v1 *pConstraint = nullptr; struct wl_listener set_region{}; struct wl_listener destroy{}; }; static void wlserver_warp_to_constraint_hint() { struct wlr_pointer_constraint_v1 *pConstraint = wlserver.GetCursorConstraint(); if (pConstraint->current.cursor_hint.enabled) { double sx = pConstraint->current.cursor_hint.x; double sy = pConstraint->current.cursor_hint.y; if ( wlserver.mouse_surface_cursorx == sx && wlserver.mouse_surface_cursory == sy ) return; wlserver.mouse_surface_cursorx = sx; wlserver.mouse_surface_cursory = sy; wlr_seat_pointer_warp( wlserver.wlr.seat, sx, sy ); uint64_t ulNow = get_time_in_nanos(); static uint32_t s_unSyntheticMoveCount = 0; // Add a heuristic for whether the cursor is continually moving, or if // this is just a simple warp to the saame place. bool bSynthetic = true; if ( wlserver.ulLastMovedCursorTime + 200'000'000 >= ulNow ) { if ( s_unSyntheticMoveCount++ >= 3 ) { bSynthetic = false; } } else { s_unSyntheticMoveCount = 0; } wlserver.ulLastMovedCursorTime = ulNow; if ( !bSynthetic ) wlserver.bCursorHidden = !wlserver.bCursorHasImage; } } static void wlserver_update_cursor_constraint() { struct wlr_pointer_constraint_v1 *pConstraint = wlserver.GetCursorConstraint(); pixman_region32_t *pRegion = &pConstraint->region; if ( wlserver.mouse_constraint_requires_warp && pConstraint->surface ) { wlserver.mouse_constraint_requires_warp = false; wlserver_warp_to_constraint_hint(); if (!pixman_region32_contains_point(pRegion, floor(wlserver.mouse_surface_cursorx), floor(wlserver.mouse_surface_cursory), NULL)) { int nboxes; pixman_box32_t *boxes = pixman_region32_rectangles(pRegion, &nboxes); if ( nboxes ) { wlserver.mouse_surface_cursorx = std::clamp( wlserver.mouse_surface_cursorx, boxes[0].x1, boxes[0].x2); wlserver.mouse_surface_cursory = std::clamp( wlserver.mouse_surface_cursory, boxes[0].y1, boxes[0].y2); wlr_seat_pointer_warp( wlserver.wlr.seat, wlserver.mouse_surface_cursorx, wlserver.mouse_surface_cursory ); } } } if (pConstraint->type == WLR_POINTER_CONSTRAINT_V1_CONFINED) pixman_region32_copy(&wlserver.confine, pRegion); else pixman_region32_clear(&wlserver.confine); } static void wlserver_constrain_cursor( struct wlr_pointer_constraint_v1 *pNewConstraint ) { struct wlr_pointer_constraint_v1 *pOldConstraint = wlserver.GetCursorConstraint(); if ( pOldConstraint == pNewConstraint ) return; if ( pOldConstraint ) { if ( !pNewConstraint ) wlserver_warp_to_constraint_hint(); wlr_pointer_constraint_v1_send_deactivated( pOldConstraint ); } wlserver.SetMouseConstraint( pNewConstraint ); if ( !pNewConstraint ) return; wlserver.mouse_constraint_requires_warp = true; wlserver_update_cursor_constraint(); wlr_pointer_constraint_v1_send_activated( pNewConstraint ); } static void handle_pointer_constraint_set_region(struct wl_listener *listener, void *data) { GamescopePointerConstraint *pGamescopeConstraint = wl_container_of(listener, pGamescopeConstraint, set_region); // If the region has been updated, we might need to warp again next commit. wlserver.mouse_constraint_requires_warp = true; } void handle_constraint_destroy(struct wl_listener *listener, void *data) { GamescopePointerConstraint *pGamescopeConstraint = wl_container_of(listener, pGamescopeConstraint, destroy); wl_list_remove(&pGamescopeConstraint->set_region.link); wl_list_remove(&pGamescopeConstraint->destroy.link); struct wlr_pointer_constraint_v1 *pCurrentConstraint = wlserver.GetCursorConstraint(); if ( pCurrentConstraint == pGamescopeConstraint->pConstraint ) { wlserver_warp_to_constraint_hint(); wlserver.SetMouseConstraint( nullptr ); } delete pGamescopeConstraint; } static void handle_pointer_constraint(struct wl_listener *listener, void *data) { struct wlr_pointer_constraint_v1 *pConstraint = (struct wlr_pointer_constraint_v1 *) data; GamescopePointerConstraint *pGamescopeConstraint = new GamescopePointerConstraint; pGamescopeConstraint->pConstraint = pConstraint; pGamescopeConstraint->set_region.notify = handle_pointer_constraint_set_region; wl_signal_add(&pConstraint->events.set_region, &pGamescopeConstraint->set_region); pGamescopeConstraint->destroy.notify = handle_constraint_destroy; wl_signal_add(&pConstraint->events.destroy, &pGamescopeConstraint->destroy); if ( wlserver.kb_focus_surface && wlserver.kb_focus_surface == pConstraint->surface ) wlserver_constrain_cursor(pConstraint); } static bool wlserver_apply_constraint( double *dx, double *dy ) { struct wlr_pointer_constraint_v1 *pConstraint = wlserver.GetCursorConstraint(); if ( pConstraint ) { if ( pConstraint->type == WLR_POINTER_CONSTRAINT_V1_LOCKED ) return false; double sx = wlserver.mouse_surface_cursorx; double sy = wlserver.mouse_surface_cursory; double sx_confined, sy_confined; if ( !wlr_region_confine( &wlserver.confine, sx, sy, sx + *dx, sy + *dy, &sx_confined, &sy_confined ) ) return false; *dx = sx_confined - sx; *dy = sy_confined - sy; if ( *dx == 0.0 && *dy == 0.0 ) return false; } return true; } void wlserver_mousemotion( double dx, double dy, uint32_t time ) { assert( wlserver_is_lock_held() ); dx *= g_mouseSensitivity; dy *= g_mouseSensitivity; wlserver_perform_rel_pointer_motion( dx, dy ); if ( !wlserver_apply_constraint( &dx, &dy ) ) { wlr_seat_pointer_notify_frame( wlserver.wlr.seat ); return; } wlserver.ulLastMovedCursorTime = get_time_in_nanos(); wlserver.bCursorHidden = !wlserver.bCursorHasImage; wlserver.mouse_surface_cursorx += dx; wlserver.mouse_surface_cursory += dy; wlserver_clampcursor(); wlserver_oncursorevent(); wlr_seat_pointer_notify_motion( wlserver.wlr.seat, time, wlserver.mouse_surface_cursorx, wlserver.mouse_surface_cursory ); wlr_seat_pointer_notify_frame( wlserver.wlr.seat ); } void wlserver_mousewarp( double x, double y, uint32_t time, bool bSynthetic ) { assert( wlserver_is_lock_held() ); wlserver.mouse_surface_cursorx = x; wlserver.mouse_surface_cursory = y; wlserver_clampcursor(); wlserver.ulLastMovedCursorTime = get_time_in_nanos(); if ( !bSynthetic ) wlserver.bCursorHidden = !wlserver.bCursorHasImage; wlserver_oncursorevent(); wlr_seat_pointer_notify_motion( wlserver.wlr.seat, time, wlserver.mouse_surface_cursorx, wlserver.mouse_surface_cursory ); wlr_seat_pointer_notify_frame( wlserver.wlr.seat ); } void wlserver_fake_mouse_pos( double x, double y ) { // Fake a pos for eg. hiding true cursor state from Steam. wlr_seat_pointer_notify_motion( wlserver.wlr.seat, 0, x, y ); wlr_seat_pointer_notify_frame( wlserver.wlr.seat ); } void wlserver_mousebutton( int button, bool press, uint32_t time ) { assert( wlserver_is_lock_held() ); wlserver.bCursorHidden = !wlserver.bCursorHasImage; wlserver_oncursorevent(); wlr_seat_pointer_notify_button( wlserver.wlr.seat, time, button, press ? WL_POINTER_BUTTON_STATE_PRESSED : WL_POINTER_BUTTON_STATE_RELEASED ); wlr_seat_pointer_notify_frame( wlserver.wlr.seat ); } void wlserver_mousewheel( double flX, double flY, uint32_t time ) { assert( wlserver_is_lock_held() ); wlr_seat_pointer_notify_axis( wlserver.wlr.seat, time, WL_POINTER_AXIS_HORIZONTAL_SCROLL, flX, flX * WLR_POINTER_AXIS_DISCRETE_STEP, WL_POINTER_AXIS_SOURCE_WHEEL, WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL ); wlr_seat_pointer_notify_axis( wlserver.wlr.seat, time, WL_POINTER_AXIS_VERTICAL_SCROLL, flY, flY * WLR_POINTER_AXIS_DISCRETE_STEP, WL_POINTER_AXIS_SOURCE_WHEEL, WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL ); wlr_seat_pointer_notify_frame( wlserver.wlr.seat ); } void wlserver_send_frame_done( struct wlr_surface *surf, const struct timespec *when ) { assert( wlserver_is_lock_held() ); wlr_surface_send_frame_done( surf, when ); } bool wlserver_surface_is_async( struct wlr_surface *surf ) { assert( wlserver_is_lock_held() ); auto wl_surf = get_wl_surface_info( surf ); if ( !wl_surf ) return false; // If we are using the Gamescope WSI layer, treat both immediate and mailbox as // "async", this is because we have a global tearing override for games. // When that is enabled we want anything not FIFO or explicitly vsynced to // have tearing. if ( wl_surf->oCurrentPresentMode ) { return wl_surf->oCurrentPresentMode == VK_PRESENT_MODE_IMMEDIATE_KHR || wl_surf->oCurrentPresentMode == VK_PRESENT_MODE_MAILBOX_KHR; } return false; } bool wlserver_surface_is_fifo( struct wlr_surface *surf ) { assert( wlserver_is_lock_held() ); auto wl_surf = get_wl_surface_info( surf ); if ( !wl_surf ) return false; if ( wl_surf->oCurrentPresentMode ) { return wl_surf->oCurrentPresentMode == VK_PRESENT_MODE_FIFO_KHR; } return false; } static std::shared_ptr s_NullFeedback; const std::shared_ptr& wlserver_surface_swapchain_feedback( struct wlr_surface *surf ) { assert( wlserver_is_lock_held() ); auto wl_surf = get_wl_surface_info( surf ); if ( !wl_surf ) return s_NullFeedback; return wl_surf->swapchain_feedback; } /* Handle the orientation of the touch inputs */ static void apply_touchscreen_orientation(double *x, double *y ) { double tx = 0; double ty = 0; // Use internal screen always for orientation purposes. switch ( GetBackend()->GetConnector( gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL )->GetCurrentOrientation() ) { default: case GAMESCOPE_PANEL_ORIENTATION_AUTO: case GAMESCOPE_PANEL_ORIENTATION_0: tx = *x; ty = *y; break; case GAMESCOPE_PANEL_ORIENTATION_90: tx = 1.0 - *y; ty = *x; break; case GAMESCOPE_PANEL_ORIENTATION_180: tx = 1.0 - *x; ty = 1.0 - *y; break; case GAMESCOPE_PANEL_ORIENTATION_270: tx = *y; ty = 1.0 - *x; break; } *x = tx; *y = ty; } void wlserver_touchmotion( double x, double y, int touch_id, uint32_t time, bool bAlwaysWarpCursor ) { assert( wlserver_is_lock_held() ); if ( wlserver.mouse_focus_surface != NULL ) { double tx = x; double ty = y; apply_touchscreen_orientation(&tx, &ty); tx *= g_nOutputWidth; ty *= g_nOutputHeight; tx += focusedWindowOffsetX; ty += focusedWindowOffsetY; tx *= focusedWindowScaleX; ty *= focusedWindowScaleY; auto [nWidth, nHeight] = wlserver_get_cursor_bounds(); tx = clamp( tx, 0.0, nWidth - 0.1 ); ty = clamp( ty, 0.0, nHeight - 0.1 ); double trackpad_dx, trackpad_dy; trackpad_dx = tx - wlserver.mouse_surface_cursorx; trackpad_dy = ty - wlserver.mouse_surface_cursory; gamescope::TouchClickMode eMode = GetBackend()->GetTouchClickMode(); if ( eMode == gamescope::TouchClickModes::Passthrough ) { wlr_seat_touch_notify_motion( wlserver.wlr.seat, time, touch_id, tx, ty ); if ( bAlwaysWarpCursor ) wlserver_mousewarp( tx, ty, time, false ); } else if ( eMode == gamescope::TouchClickModes::Disabled ) { return; } else if ( eMode == gamescope::TouchClickModes::Trackpad ) { wlserver_mousemotion( trackpad_dx, trackpad_dy, time ); } else { g_bPendingTouchMovement = true; wlserver_mousewarp( tx, ty, time, false ); } } bump_input_counter(); } void wlserver_touchdown( double x, double y, int touch_id, uint32_t time ) { assert( wlserver_is_lock_held() ); if ( wlserver.mouse_focus_surface != NULL ) { double tx = x; double ty = y; apply_touchscreen_orientation(&tx, &ty); tx *= g_nOutputWidth; ty *= g_nOutputHeight; tx += focusedWindowOffsetX; ty += focusedWindowOffsetY; tx *= focusedWindowScaleX; ty *= focusedWindowScaleY; gamescope::TouchClickMode eMode = GetBackend()->GetTouchClickMode(); if ( eMode == gamescope::TouchClickModes::Passthrough ) { wlr_seat_touch_notify_down( wlserver.wlr.seat, wlserver.mouse_focus_surface, time, touch_id, tx, ty ); wlserver.touch_down_ids.insert( touch_id ); } else if ( eMode == gamescope::TouchClickModes::Disabled ) { return; } else { g_bPendingTouchMovement = true; if ( eMode != gamescope::TouchClickModes::Trackpad ) { wlserver_mousewarp( tx, ty, time, false ); } uint32_t button = TouchClickModeToLinuxButton( eMode ); if ( button != 0 && eMode < WLSERVER_BUTTON_COUNT ) { wlr_seat_pointer_notify_button( wlserver.wlr.seat, time, button, WL_POINTER_BUTTON_STATE_PRESSED ); wlr_seat_pointer_notify_frame( wlserver.wlr.seat ); wlserver.button_held[ eMode ] = true; } } } bump_input_counter(); } void wlserver_touchup( int touch_id, uint32_t time ) { assert( wlserver_is_lock_held() ); if ( wlserver.mouse_focus_surface != NULL ) { bool bReleasedAny = false; for ( int i = 0; i < WLSERVER_BUTTON_COUNT; i++ ) { if ( wlserver.button_held[ i ] == true ) { uint32_t button = TouchClickModeToLinuxButton( (gamescope::TouchClickMode) i ); if ( button != 0 ) { wlr_seat_pointer_notify_button( wlserver.wlr.seat, time, button, WL_POINTER_BUTTON_STATE_RELEASED ); bReleasedAny = true; } wlserver.button_held[ i ] = false; } } if ( bReleasedAny == true ) { wlr_seat_pointer_notify_frame( wlserver.wlr.seat ); } if ( wlserver.touch_down_ids.count( touch_id ) > 0 ) { wlr_seat_touch_notify_up( wlserver.wlr.seat, time, touch_id ); wlserver.touch_down_ids.erase( touch_id ); } } bump_input_counter(); } gamescope_xwayland_server_t *wlserver_get_xwayland_server( size_t index ) { if (index >= wlserver.wlr.xwayland_servers.size() ) return NULL; return wlserver.wlr.xwayland_servers[index].get(); } const char *wlserver_get_wl_display_name( void ) { return wlserver.wl_display_name; } static void wlserver_x11_surface_info_set_wlr( struct wlserver_x11_surface_info *surf, struct wlr_surface *wlr_surf, bool override ) { assert( wlserver_is_lock_held() ); if (!override) { wl_list_remove( &surf->pending_link ); wl_list_init( &surf->pending_link ); } wlserver_wl_surface_info *wl_surf_info = get_wl_surface_info(wlr_surf); if (override) { if ( surf->override_surface ) { wlserver_wl_surface_info *wl_info = get_wl_surface_info(surf->override_surface); if (wl_info) wl_info->x11_surface = nullptr; } surf->override_surface = wlr_surf; } else { if ( surf->main_surface ) { wlserver_wl_surface_info *wl_info = get_wl_surface_info(surf->main_surface); if (wl_info) wl_info->x11_surface = nullptr; } surf->main_surface = wlr_surf; } wl_surf_info->x11_surface = surf; for (auto it = g_PendingCommits.begin(); it != g_PendingCommits.end();) { if (it->surf == wlr_surf) { PendingCommit_t pending = *it; // Still have the buffer lock from before... wlserver_x11_surface_info *wlserver_x11_surface_info = get_wl_surface_info(wlr_surf)->x11_surface; assert(wlserver_x11_surface_info); assert(wlserver_x11_surface_info->xwayland_server); wlserver_x11_surface_info->xwayland_server->wayland_commit( pending.surf, pending.buf ); it = g_PendingCommits.erase(it); } else { it++; } } } void wlserver_x11_surface_info_init( struct wlserver_x11_surface_info *surf, gamescope_xwayland_server_t *server, uint32_t x11_id ) { surf->wl_id = 0; surf->x11_id = x11_id; surf->main_surface = nullptr; surf->override_surface = nullptr; surf->xwayland_server = server; wl_list_init( &surf->pending_link ); } void gamescope_xwayland_server_t::set_wl_id( struct wlserver_x11_surface_info *surf, uint32_t id ) { if (surf->wl_id) { if (surf->main_surface) { struct wl_resource *old_resource = wl_client_get_object( xwayland_server->client, surf->wl_id ); if (!old_resource) { wl_log.errorf("wlserver_x11_surface_info had bad wl_id? Oh no! %d", surf->wl_id); return; } wlr_surface *old_wlr_surf = wlr_surface_from_resource( old_resource ); if (!old_wlr_surf) { wl_log.errorf("wlserver_x11_surface_info wl_id's was not a wlr_surf?! Oh no! %d", surf->wl_id); return; } wlserver_wl_surface_info *old_surface_info = get_wl_surface_info(old_wlr_surf); old_surface_info->x11_surface = nullptr; } else { wl_list_remove( &surf->pending_link ); wl_list_init( &surf->pending_link ); } } surf->wl_id = id; surf->main_surface = nullptr; surf->xwayland_server = this; wl_list_insert( &pending_surfaces, &surf->pending_link ); struct wlr_surface *wlr_override_surf = nullptr; struct wlr_surface *wlr_surf = nullptr; if ( content_overrides.count( surf->x11_id ) ) { wlr_override_surf = content_overrides[ surf->x11_id ]->surface; } struct wl_resource *resource = wl_client_get_object( xwayland_server->client, id ); if ( resource != nullptr ) wlr_surf = wlr_surface_from_resource( resource ); if ( wlr_surf != nullptr ) wlserver_x11_surface_info_set_wlr( surf, wlr_surf, false ); if ( wlr_override_surf != nullptr ) wlserver_x11_surface_info_set_wlr( surf, wlr_override_surf, true ); } bool gamescope_xwayland_server_t::is_xwayland_ready() const { return xwayland_ready; } _XDisplay *gamescope_xwayland_server_t::get_xdisplay() { return dpy; } const char *gamescope_xwayland_server_t::get_nested_display_name() const { return xwayland_server->display_name; } void wlserver_x11_surface_info_finish( struct wlserver_x11_surface_info *surf ) { assert( wlserver_is_lock_held() ); if (surf->main_surface) { surf->xwayland_server->destroy_content_override( surf, surf->main_surface ); wlserver_wl_surface_info *wl_info = get_wl_surface_info(surf->main_surface); if (wl_info) wl_info->x11_surface = nullptr; } if (surf->override_surface) { surf->xwayland_server->destroy_content_override( surf, surf->override_surface ); wlserver_wl_surface_info *wl_info = get_wl_surface_info(surf->override_surface); if (wl_info) wl_info->x11_surface = nullptr; } surf->wl_id = 0; surf->main_surface = nullptr; surf->override_surface = nullptr; wl_list_remove( &surf->pending_link ); } void wlserver_set_xwayland_server_mode( size_t idx, int w, int h, int nRefreshmHz ) { assert( wlserver_is_lock_held() ); gamescope_xwayland_server_t *server = wlserver_get_xwayland_server( idx ); if ( !server ) return; struct wlr_output *output = server->get_output(); struct wlr_output_state *output_state = server->get_output_state(); wlr_output_state_set_custom_mode(output_state, w, h, nRefreshmHz ); if (!wlr_output_commit_state(output, output_state)) { wl_log.errorf("Failed to commit headless output"); abort(); } wl_log.infof("Updating mode for xwayland server #%zu: %dx%d@%d", idx, w, h, gamescope::ConvertmHzToHz( nRefreshmHz ) ); } // Definitely not very efficient if we end up with // a lot of Wayland windows in the future. // // Lots of atomic reference stuff will happen whenever // this is called with a lot of windows. // May want to change this down the line. // // Given we are only expecting like 1-2 xdg-shell // windows for our current usecases (Waydroid, etc) // I think this is reasonable for now. std::vector> wlserver_get_xdg_shell_windows() { std::unique_lock lock(g_wlserver_xdg_shell_windows_lock); return wlserver.xdg_wins; } bool wlserver_xdg_dirty() { return wlserver.xdg_dirty.exchange(false); } std::vector wlserver_xdg_commit_queue() { std::vector commits; { std::lock_guard lock( wlserver.xdg_commit_lock ); commits = std::move(wlserver.xdg_commit_queue); } return commits; } uint32_t wlserver_make_new_xwayland_server() { assert( wlserver_is_lock_held() ); auto& server = wlserver.wlr.xwayland_servers.emplace_back(std::make_unique(wlserver.display)); while (!server->is_xwayland_ready()) { wl_display_flush_clients(wlserver.display); if (wl_event_loop_dispatch(wlserver.event_loop, -1) < 0) { wl_log.errorf("wl_event_loop_dispatch failed\n"); return ~0u; } } return uint32_t(wlserver.wlr.xwayland_servers.size() - 1); } void wlserver_destroy_xwayland_server(gamescope_xwayland_server_t *server) { assert( wlserver_is_lock_held() ); std::erase_if(wlserver.wlr.xwayland_servers, [=](const auto& other) { return other.get() == server; }); } ValveSoftware-gamescope-eb620ab/src/wlserver.hpp000066400000000000000000000221721502457270500221120ustar00rootroot00000000000000// Wayland stuff #pragma once #include #include #include #include #include #include #include #include #include #include #include "WaylandServer/WaylandDecls.h" #include "WaylandServer/WaylandServerLegacy.h" #include #include "vulkan_include.h" #include "steamcompmgr_shared.hpp" #if HAVE_DRM #define HAVE_SESSION 1 #endif #define WLSERVER_BUTTON_COUNT 7 struct _XDisplay; struct xwayland_ctx_t; struct GamescopeAcquireTimelineState { int32_t nEventFd = -1; bool bKnownReady = false; }; struct ResListEntry_t { struct wlr_surface *surf; struct wlr_buffer *buf; bool async; bool fifo; std::shared_ptr feedback; std::vector presentation_feedbacks; std::optional present_id; uint64_t desired_present_time; std::shared_ptr pAcquirePoint; std::shared_ptr pReleasePoint; }; struct wlserver_content_override; bool wlserver_is_lock_held(void); class gamescope_xwayland_server_t { public: gamescope_xwayland_server_t(wl_display *display); ~gamescope_xwayland_server_t(); void on_xwayland_ready(void *data); static void xwayland_ready_callback(struct wl_listener *listener, void *data); bool is_xwayland_ready() const; const char *get_nested_display_name() const; void set_wl_id( struct wlserver_x11_surface_info *surf, uint32_t id ); _XDisplay *get_xdisplay(); std::unique_ptr ctx; void wayland_commit(struct wlr_surface *surf, struct wlr_buffer *buf); std::vector& retrieve_commits(); void handle_override_window_content( struct wl_client *client, struct wl_resource *gamescope_swapchain_resource, struct wlr_surface *surface, uint32_t x11_window ); void destroy_content_override( struct wlserver_x11_surface_info *x11_surface, struct wlr_surface *surf); void destroy_content_override(struct wlserver_content_override *co); struct wl_client *get_client(); struct wlr_output *get_output(); struct wlr_output_state *get_output_state(); void update_output_info(); private: struct wlr_xwayland_server *xwayland_server = NULL; struct wl_listener xwayland_ready_listener = { .notify = xwayland_ready_callback }; struct wlr_output *output = nullptr; struct wlr_output_state *output_state = nullptr; std::unordered_map content_overrides; bool xwayland_ready = false; _XDisplay *dpy = NULL; std::mutex wayland_commit_lock; std::vector wayland_commit_queue; }; struct wlserver_t { struct wl_display *display; struct wl_event_loop *event_loop; char wl_display_name[32]; struct { struct wlr_backend *multi_backend; struct wlr_backend *headless_backend; struct wlr_backend *libinput_backend; struct wlr_renderer *renderer; struct wlr_compositor *compositor; struct wlr_session *session; struct wlr_seat *seat; // Used to simulate key events and set the keymap struct wlr_keyboard *virtual_keyboard_device; struct wlr_device *device; std::vector> xwayland_servers; } wlr; struct wlr_surface *mouse_focus_surface; struct wlr_surface *kb_focus_surface; std::unordered_map> current_dropdown_surfaces; double mouse_surface_cursorx = 0.0f; double mouse_surface_cursory = 0.0f; bool mouse_constraint_requires_warp = false; pixman_region32_t confine; std::atomic mouse_constraint = { nullptr }; void SetMouseConstraint( struct wlr_pointer_constraint_v1 *pConstraint ) { assert( wlserver_is_lock_held() ); // Set by wlserver only. Read by both wlserver + steamcompmgr with no // need to actually be sequentially consistent. mouse_constraint.store( pConstraint, std::memory_order_relaxed ); } struct wlr_pointer_constraint_v1 *GetCursorConstraint() const { assert( wlserver_is_lock_held() ); return mouse_constraint.load( std::memory_order_relaxed ); } bool HasMouseConstraint() const { // Does not need to be sequentially consistent. // Used by the steamcompmgr thread to check if there is currently a mouse constraint. return mouse_constraint.load( std::memory_order_relaxed ) != nullptr; } uint64_t ulLastMovedCursorTime = 0; bool bCursorHidden = true; bool bCursorHasImage = true; bool button_held[ WLSERVER_BUTTON_COUNT ]; std::set touch_down_ids; struct { char *name; char *description; int phys_width, phys_height; // millimeters } output_info; struct wl_listener session_active; struct wl_listener new_input_method; struct wlr_xdg_shell *xdg_shell; struct wlr_layer_shell_v1 *layer_shell_v1; struct wlr_relative_pointer_manager_v1 *relative_pointer_manager; struct wlr_pointer_constraints_v1 *constraints; struct wl_listener new_xdg_surface; struct wl_listener new_xdg_toplevel; struct wl_listener new_layer_shell_surface; struct wl_listener new_pointer_constraint; std::vector> xdg_wins; std::atomic xdg_dirty; std::mutex xdg_commit_lock; std::vector xdg_commit_queue; std::vector gamescope_controls; std::atomic bWaylandServerRunning = { false }; // Share one single keymap and state between all connected physical keyboards struct wlr_keyboard_group *keyboard_group; struct wl_listener keyboard_group_modifiers; struct wl_listener keyboard_group_key; }; extern struct wlserver_t wlserver; std::vector wlserver_xdg_commit_queue(); struct wlserver_pointer { struct wlr_pointer *wlr; struct wl_listener motion; struct wl_listener button; struct wl_listener axis; struct wl_listener frame; }; struct wlserver_touch { struct wlr_touch *wlr; struct wl_listener down; struct wl_listener up; struct wl_listener motion; }; void xwayland_surface_commit(struct wlr_surface *wlr_surface); bool wlsession_init( void ); int wlsession_open_kms( const char *device_name ); void wlsession_close_kms(); bool wlserver_init( void ); void wlserver_run(void); void wlserver_lock(void); void wlserver_unlock(bool flush = true); bool wlserver_is_lock_held(void); void wlserver_keyboardfocus( struct wlr_surface *surface, bool bConstrain = true ); void wlserver_key( uint32_t key, bool press, uint32_t time ); void wlserver_mousefocus( struct wlr_surface *wlrsurface, int x = 0, int y = 0 ); void wlserver_clear_dropdowns(); void wlserver_notify_dropdown( struct wlr_surface *wlrsurface, int nX, int nY ); void wlserver_mousemotion( double x, double y, uint32_t time ); void wlserver_mousehide(); void wlserver_mousewarp( double x, double y, uint32_t time, bool bSynthetic ); void wlserver_mousebutton( int button, bool press, uint32_t time ); void wlserver_mousewheel( double x, double y, uint32_t time ); void wlserver_touchmotion( double x, double y, int touch_id, uint32_t time, bool bAlwaysWarpCursor = false ); void wlserver_touchdown( double x, double y, int touch_id, uint32_t time ); void wlserver_touchup( int touch_id, uint32_t time ); void wlserver_send_frame_done( struct wlr_surface *surf, const struct timespec *when ); bool wlserver_surface_is_async( struct wlr_surface *surf ); bool wlserver_surface_is_fifo( struct wlr_surface *surf ); const std::shared_ptr& wlserver_surface_swapchain_feedback( struct wlr_surface *surf ); std::vector> wlserver_get_xdg_shell_windows(); bool wlserver_xdg_dirty(); struct wlserver_output_info { const char *description; int phys_width, phys_height; // millimeters }; void wlserver_set_output_info( const wlserver_output_info *info ); gamescope_xwayland_server_t *wlserver_get_xwayland_server( size_t index ); const char *wlserver_get_wl_display_name( void ); void wlserver_x11_surface_info_init( struct wlserver_x11_surface_info *surf, gamescope_xwayland_server_t *server, uint32_t x11_id ); void wlserver_x11_surface_info_finish( struct wlserver_x11_surface_info *surf ); void wlserver_set_xwayland_server_mode( size_t idx, int w, int h, int refresh ); extern std::atomic g_bPendingTouchMovement; void wlserver_open_steam_menu( bool qam ); uint32_t wlserver_make_new_xwayland_server(); void wlserver_destroy_xwayland_server(gamescope_xwayland_server_t *server); void wlserver_presentation_feedback_presented( struct wlr_surface *surface, std::vector& presentation_feedbacks, uint64_t last_refresh_nsec, uint64_t refresh_cycle ); void wlserver_presentation_feedback_discard( struct wlr_surface *surface, std::vector& presentation_feedbacks ); void wlserver_past_present_timing( struct wlr_surface *surface, uint32_t present_id, uint64_t desired_present_time, uint64_t actual_present_time, uint64_t earliest_present_time, uint64_t present_margin ); void wlserver_refresh_cycle( struct wlr_surface *surface, uint64_t refresh_cycle ); void wlserver_shutdown(); void wlserver_send_gamescope_control( wl_resource *control ); bool wlsession_active(); void wlserver_fake_mouse_pos( double x, double y ); void wlserver_mousewheel2( int32_t nDiscreteX, int32_t nDiscreteY, double flX, double flY, uint32_t uTime ); ValveSoftware-gamescope-eb620ab/src/x11cursor.cpp000066400000000000000000000025411502457270500221010ustar00rootroot00000000000000#include #include #include "backend.h" #include "Utils/Defer.h" #include "xwayland_ctx.hpp" extern const char *g_pOriginalDisplay; namespace gamescope { std::shared_ptr GetX11HostCursor() { if ( !g_pOriginalDisplay ) return nullptr; Display *display = XOpenDisplay( g_pOriginalDisplay ); if ( !display ) return nullptr; defer( XCloseDisplay( display ) ); int xfixes_event, xfixes_error; if ( !XFixesQueryExtension( display, &xfixes_event, &xfixes_error ) ) { xwm_log.errorf("No XFixes extension on current compositor"); return nullptr; } XFixesCursorImage *image = XFixesGetCursorImage( display ); if ( !image ) return nullptr; defer( XFree( image ) ); // image->pixels is `unsigned long*` :/ // Thanks X11. std::vector cursorData = std::vector( image->width * image->height ); for (uint32_t y = 0; y < image->height; y++) { for (uint32_t x = 0; x < image->width; x++) { cursorData[y * image->width + x] = static_cast( image->pixels[image->height * y + x] ); } } return std::make_shared(INestedHints::CursorInfo { .pPixels = std::move( cursorData ), .uWidth = image->width, .uHeight = image->height, .uXHotspot = image->xhot, .uYHotspot = image->yhot, }); } } ValveSoftware-gamescope-eb620ab/src/xwayland_ctx.hpp000066400000000000000000000151051502457270500227440ustar00rootroot00000000000000#pragma once #include "backend.h" #include "waitable.h" #include #include #include #include #include #include #include #include #include #include #include #include class gamescope_xwayland_server_t; struct ignore; struct steamcompmgr_win_t; class MouseCursor; extern LogScope xwm_log; struct focus_t { steamcompmgr_win_t *focusWindow; steamcompmgr_win_t *inputFocusWindow; uint32_t inputFocusMode; steamcompmgr_win_t *overlayWindow; steamcompmgr_win_t *externalOverlayWindow; steamcompmgr_win_t *notificationWindow; steamcompmgr_win_t *overrideWindow; bool outdatedInteractiveFocus; bool bResetToCorner = false; bool bResetToCenter = false; uint64_t ulCurrentFocusSerial = UINT64_MAX; bool IsDirty(); }; struct CommitDoneEntry_t { uint64_t winSeq; uint64_t commitID; uint64_t desiredPresentTime; uint64_t earliestPresentTime; uint64_t earliestLatchTime; bool fifo; }; struct CommitDoneList_t { std::mutex listCommitsDoneLock; std::vector< CommitDoneEntry_t > listCommitsDone; }; struct xwayland_ctx_t final : public gamescope::IWaitable { gamescope_xwayland_server_t *xwayland_server; Display *dpy; // Not used for most of steamcompmgr thread. Just to sync whenever // wlserver wants it. std::mutex list_mutex; steamcompmgr_win_t *list; int scr; Window root; XserverRegion allDamage; bool clipChanged; int root_height, root_width; int xfixes_event, xfixes_error; int damage_event, damage_error; int composite_event, composite_error; int render_event, render_error; int xshape_event, xshape_error; int composite_opcode; int xinput_opcode, xinput_event, xinput_error; Window ourWindow; focus_t focus; Window currentKeyboardFocusWindow; Window focusControlWindow; std::unique_ptr cursor; CommitDoneList_t doneCommits; double accum_x = 0.0; double accum_y = 0.0; bool force_windows_fullscreen = false; std::vector< steamcompmgr_win_t* > GetPossibleFocusWindows(); void DetermineAndApplyFocus( const std::vector< steamcompmgr_win_t* > &vecPossibleFocusWindows ); struct { Atom steamAtom; Atom gameAtom; Atom overlayAtom; Atom externalOverlayAtom; Atom gamesRunningAtom; Atom screenZoomAtom; Atom screenScaleAtom; Atom opacityAtom; Atom winTypeAtom; Atom winDesktopAtom; Atom winDockAtom; Atom winToolbarAtom; Atom winMenuAtom; Atom winUtilAtom; Atom winSplashAtom; Atom winDialogAtom; Atom winNormalAtom; Atom sizeHintsAtom; Atom netWMStateFullscreenAtom; Atom activeWindowAtom; Atom netWMStateAtom; Atom WMTransientForAtom; Atom netWMStateHiddenAtom; Atom netWMStateFocusedAtom; Atom netWMStateSkipTaskbarAtom; Atom netWMStateSkipPagerAtom; Atom WLSurfaceIDAtom; Atom WMStateAtom; Atom steamInputFocusAtom; Atom WMChangeStateAtom; Atom steamTouchClickModeAtom; Atom utf8StringAtom; Atom netWMNameAtom; Atom netWMIcon; Atom netSystemTrayOpcodeAtom; Atom steamStreamingClientAtom; Atom steamStreamingClientVideoAtom; Atom gamescopeFocusableAppsAtom; Atom gamescopeFocusableWindowsAtom; Atom gamescopeFocusedWindowAtom; Atom gamescopeFocusedAppAtom; Atom gamescopeFocusedAppGfxAtom; Atom gamescopeCtrlAppIDAtom; Atom gamescopeCtrlWindowAtom; Atom gamescopeInputCounterAtom; Atom gamescopeScreenShotAtom; Atom gamescopeDebugScreenShotAtom; Atom gamescopeFocusDisplay; Atom gamescopeMouseFocusDisplay; Atom gamescopeKeyboardFocusDisplay; Atom gamescopeTuneableVBlankRedZone; Atom gamescopeTuneableRateOfDecay; Atom gamescopeScalingFilter; Atom gamescopeFSRSharpness; Atom gamescopeSharpness; Atom gamescopeXWaylandModeControl; Atom gamescopeFPSLimit; Atom gamescopeDynamicRefresh[gamescope::GAMESCOPE_SCREEN_TYPE_COUNT]; Atom gamescopeLowLatency; Atom gamescopeFSRFeedback; Atom gamescopeBlurMode; Atom gamescopeBlurRadius; Atom gamescopeBlurFadeDuration; Atom gamescopeCompositeForce; Atom gamescopeCompositeDebug; Atom gamescopeAllowTearing; Atom gamescopeDisplayForceInternal; Atom gamescopeDisplayModeNudge; Atom gamescopeDisplayIsExternal; Atom gamescopeDisplayModeListExternal; Atom gamescopeCursorVisibleFeedback; Atom gamescopeSteamMaxHeight; Atom gamescopeVRRCapable; Atom gamescopeVRREnabled; Atom gamescopeVRRInUse; Atom gamescopeNewScalingFilter; Atom gamescopeNewScalingScaler; Atom gamescopeDisplayEdidPath; Atom gamescopeXwaylandServerId; Atom gamescopeDisplaySupportsHDR; Atom gamescopeDisplayHDREnabled; Atom gamescopeDebugForceHDR10Output; Atom gamescopeDebugForceHDRSupport; Atom gamescopeDebugHDRHeatmap; Atom gamescopeDebugHDRHeatmap_MSWCG; Atom gamescopeHDROutputFeedback; Atom gamescopeSDROnHDRContentBrightness; Atom gamescopeHDRInputGain; Atom gamescopeSDRInputGain; Atom gamescopeHDRItmEnable; Atom gamescopeHDRItmSDRNits; Atom gamescopeHDRItmTargetNits; Atom gamescopeColorLookPQ; Atom gamescopeColorLookG22; Atom gamescopeColorOutputVirtualWhite; Atom gamescopeHDRTonemapDisplayMetadata; Atom gamescopeHDRTonemapSourceMetadata; Atom gamescopeHDRTonemapOperator; Atom gamescopeForceWindowsFullscreen; Atom gamescopeColorLut3DOverride; Atom gamescopeColorShaperLutOverride; Atom gamescopeColorSDRGamutWideness; Atom gamescopeColorNightMode; // amount, hue, saturation Atom gamescopeColorManagementDisable; Atom gamescopeColorAppWantsHDRFeedback; Atom gamescopeColorAppHDRMetadataFeedback; Atom gamescopeColorSliderInUse; Atom gamescopeColorChromaticAdaptationMode; Atom gamescopeColorMuraCorrectionImage[gamescope::GAMESCOPE_SCREEN_TYPE_COUNT]; Atom gamescopeColorMuraScale[gamescope::GAMESCOPE_SCREEN_TYPE_COUNT]; Atom gamescopeColorMuraCorrectionDisabled[gamescope::GAMESCOPE_SCREEN_TYPE_COUNT]; Atom gamescopeCreateXWaylandServer; Atom gamescopeCreateXWaylandServerFeedback; Atom gamescopeDestroyXWaylandServer; Atom gamescopeReshadeEffect; Atom gamescopeReshadeTechniqueIdx; Atom gamescopeDisplayRefreshRateFeedback; Atom gamescopeDisplayDynamicRefreshBasedOnGamePresence; Atom wineHwndStyle; Atom wineHwndStyleEx; Atom clipboard; Atom primarySelection; Atom targets; } atoms; bool HasQueuedEvents(); void Dispatch(); int GetFD() final { return XConnectionNumber( dpy ); } void OnPollIn() final { Dispatch(); } void OnPollHangUp() final { xwm_log.errorf( "XWayland server hung up! This is fatal. Aborting..." ); abort(); } }; ValveSoftware-gamescope-eb620ab/subprojects/000077500000000000000000000000001502457270500213005ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/subprojects/.gitignore000066400000000000000000000000271502457270500232670ustar00rootroot00000000000000glm-* packagecache stb ValveSoftware-gamescope-eb620ab/subprojects/glm.wrap000066400000000000000000000002311502457270500227460ustar00rootroot00000000000000[wrap-git] directory = glm url = https://github.com/g-truc/glm.git revision = 0af55ccecd98d4e5a8d1fad7de25ba429d60e863 depth = 1 patch_directory = glm ValveSoftware-gamescope-eb620ab/subprojects/libdisplay-info/000077500000000000000000000000001502457270500243655ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/subprojects/libliftoff/000077500000000000000000000000001502457270500234205ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/subprojects/openvr/000077500000000000000000000000001502457270500226115ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/subprojects/packagefiles/000077500000000000000000000000001502457270500237165ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/subprojects/packagefiles/glm/000077500000000000000000000000001502457270500244755ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/subprojects/packagefiles/glm/meson.build000066400000000000000000000002641502457270500266410ustar00rootroot00000000000000project('glm', 'cpp', version: '1.0.1', license: 'MIT') glm_dep = declare_dependency( include_directories: include_directories('.') ) meson.override_dependency('glm', glm_dep) ValveSoftware-gamescope-eb620ab/subprojects/packagefiles/stb/000077500000000000000000000000001502457270500245065ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/subprojects/packagefiles/stb/meson.build000066400000000000000000000003461502457270500266530ustar00rootroot00000000000000project('stb', 'c', version : '0.1.0', license : 'MIT') stb_dep = declare_dependency( include_directories : include_directories('.'), version : meson.project_version() ) meson.override_dependency('stb', stb_dep) ValveSoftware-gamescope-eb620ab/subprojects/stb.wrap000066400000000000000000000002331502457270500227610ustar00rootroot00000000000000[wrap-git] directory = stb url = https://github.com/nothings/stb.git revision = 5736b15f7ea0ffb08dd38af21067c314d6a3aae9 depth = 1 patch_directory = stb ValveSoftware-gamescope-eb620ab/subprojects/vkroots/000077500000000000000000000000001502457270500230075ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/subprojects/wlroots/000077500000000000000000000000001502457270500230115ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/thirdparty/000077500000000000000000000000001502457270500211275ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/thirdparty/SPIRV-Headers/000077500000000000000000000000001502457270500234035ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/thirdparty/sol/000077500000000000000000000000001502457270500217245ustar00rootroot00000000000000ValveSoftware-gamescope-eb620ab/thirdparty/sol/config.hpp000066400000000000000000000040411502457270500237010ustar00rootroot00000000000000// The MIT License (MIT) // Copyright (c) 2013-2020 Rapptz, ThePhD and contributors // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // This file was generated with a script. // Generated 2024-09-03 03:46:49.901324 UTC // This header was generated with sol v3.3.1 (revision 2b0d2fe8) // https://github.com/ThePhD/sol2 #ifndef SOL_SINGLE_SOL_CONFIG_HPP #define SOL_SINGLE_SOL_CONFIG_HPP // beginning of sol/config.hpp /* Base, empty configuration file! To override, place a file in your include paths of the form: . (your include path here) | sol (directory, or equivalent) | config.hpp (your config.hpp file) So that when sol2 includes the file #include it gives you the configuration values you desire. Configuration values can be seen in the safety.rst of the doc/src, or at https://sol2.readthedocs.io/en/latest/safety.html ! You can also pass them through the build system, or the command line options of your compiler. */ #define SOL_NO_EXCEPTIONS 1 // end of sol/config.hpp #endif // SOL_SINGLE_SOL_CONFIG_HPP ValveSoftware-gamescope-eb620ab/thirdparty/sol/forward.hpp000066400000000000000000001106741502457270500241120ustar00rootroot00000000000000// The MIT License (MIT) // Copyright (c) 2013-2020 Rapptz, ThePhD and contributors // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // This file was generated with a script. // Generated 2024-09-03 03:46:49.899874 UTC // This header was generated with sol v3.3.1 (revision 2b0d2fe8) // https://github.com/ThePhD/sol2 #ifndef SOL_SINGLE_INCLUDE_SOL_FORWARD_HPP #define SOL_SINGLE_INCLUDE_SOL_FORWARD_HPP // beginning of sol/forward.hpp #ifndef SOL_FORWARD_HPP #define SOL_FORWARD_HPP // beginning of sol/version.hpp #include #define SOL_VERSION_MAJOR 3 #define SOL_VERSION_MINOR 2 #define SOL_VERSION_PATCH 3 #define SOL_VERSION_STRING "3.2.3" #define SOL_VERSION ((SOL_VERSION_MAJOR * 100000) + (SOL_VERSION_MINOR * 100) + (SOL_VERSION_PATCH)) #define SOL_TOKEN_TO_STRING_POST_EXPANSION_I_(_TOKEN) #_TOKEN #define SOL_TOKEN_TO_STRING_I_(_TOKEN) SOL_TOKEN_TO_STRING_POST_EXPANSION_I_(_TOKEN) #define SOL_CONCAT_TOKENS_POST_EXPANSION_I_(_LEFT, _RIGHT) _LEFT##_RIGHT #define SOL_CONCAT_TOKENS_I_(_LEFT, _RIGHT) SOL_CONCAT_TOKENS_POST_EXPANSION_I_(_LEFT, _RIGHT) #define SOL_RAW_IS_ON(OP_SYMBOL) ((3 OP_SYMBOL 3) != 0) #define SOL_RAW_IS_OFF(OP_SYMBOL) ((3 OP_SYMBOL 3) == 0) #define SOL_RAW_IS_DEFAULT_ON(OP_SYMBOL) ((3 OP_SYMBOL 3) > 3) #define SOL_RAW_IS_DEFAULT_OFF(OP_SYMBOL) ((3 OP_SYMBOL 3 OP_SYMBOL 3) < 0) #define SOL_IS_ON(OP_SYMBOL) SOL_RAW_IS_ON(OP_SYMBOL ## _I_) #define SOL_IS_OFF(OP_SYMBOL) SOL_RAW_IS_OFF(OP_SYMBOL ## _I_) #define SOL_IS_DEFAULT_ON(OP_SYMBOL) SOL_RAW_IS_DEFAULT_ON(OP_SYMBOL ## _I_) #define SOL_IS_DEFAULT_OFF(OP_SYMBOL) SOL_RAW_IS_DEFAULT_OFF(OP_SYMBOL ## _I_) #define SOL_ON | #define SOL_OFF ^ #define SOL_DEFAULT_ON + #define SOL_DEFAULT_OFF - #if defined(SOL_BUILD_CXX_MODE) #if (SOL_BUILD_CXX_MODE != 0) #define SOL_BUILD_CXX_MODE_I_ SOL_ON #else #define SOL_BUILD_CXX_MODE_I_ SOL_OFF #endif #elif defined(__cplusplus) #define SOL_BUILD_CXX_MODE_I_ SOL_DEFAULT_ON #else #define SOL_BUILD_CXX_MODE_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_BUILD_C_MODE) #if (SOL_BUILD_C_MODE != 0) #define SOL_BUILD_C_MODE_I_ SOL_ON #else #define SOL_BUILD_C_MODE_I_ SOL_OFF #endif #elif defined(__STDC__) #define SOL_BUILD_C_MODE_I_ SOL_DEFAULT_ON #else #define SOL_BUILD_C_MODE_I_ SOL_DEFAULT_OFF #endif #if SOL_IS_ON(SOL_BUILD_C_MODE) #include #include #include #else #include #include #include #endif #if defined(SOL_HAS_BUILTIN) #define SOL_HAS_BUILTIN_I_(...) SOL_HAS_BUILTIN(__VA_ARGS__) #elif defined(__has_builtin) #define SOL_HAS_BUILTIN_I_(...) __has_builtin(__VA_ARGS__) #else #define SOL_HAS_BUILTIN_I_(...) 0 #endif #if defined(SOL_COMPILER_VCXX) #if (SOL_COMPILER_VCXX != 0) #define SOL_COMPILER_VCXX_I_ SOL_ON #else #define SOL_COMPILER_VCXX_I_ SOL_OFF #endif #elif defined(_MSC_VER) #define SOL_COMPILER_VCXX_I_ SOL_DEFAULT_ON #else #define SOL_COMPILER_VCXX_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_COMPILER_GCC) #if (SOL_COMPILER_GCC != 0) #define SOL_COMPILER_GCC_I_ SOL_ON #else #define SOL_COMPILER_GCC_I_ SOL_OFF #endif #elif defined(__GNUC__) #define SOL_COMPILER_GCC_I_ SOL_DEFAULT_ON #else #define SOL_COMPILER_GCC_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_COMPILER_CLANG) #if (SOL_COMPILER_CLANG != 0) #define SOL_COMPILER_CLANG_I_ SOL_ON #else #define SOL_COMPILER_CLANG_I_ SOL_OFF #endif #elif defined(__clang__) #define SOL_COMPILER_CLANG_I_ SOL_DEFAULT_ON #else #define SOL_COMPILER_CLANG_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_COMPILER_EDG) #if (SOL_COMPILER_EDG != 0) #define SOL_COMPILER_EDG_I_ SOL_ON #else #define SOL_COMPILER_EDG_I_ SOL_OFF #endif #else #define SOL_COMPILER_EDG_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_COMPILER_MINGW) #if (SOL_COMPILER_MINGW != 0) #define SOL_COMPILER_MINGW_I_ SOL_ON #else #define SOL_COMPILER_MINGW_I_ SOL_OFF #endif #elif defined(__MINGW32__) #define SOL_COMPILER_MINGW_I_ SOL_DEFAULT_ON #else #define SOL_COMPILER_MINGW_I_ SOL_DEFAULT_OFF #endif #if SIZE_MAX <= 0xFFFFULL #define SOL_PLATFORM_X16_I_ SOL_ON #define SOL_PLATFORM_X86_I_ SOL_OFF #define SOL_PLATFORM_X64_I_ SOL_OFF #elif SIZE_MAX <= 0xFFFFFFFFULL #define SOL_PLATFORM_X16_I_ SOL_OFF #define SOL_PLATFORM_X86_I_ SOL_ON #define SOL_PLATFORM_X64_I_ SOL_OFF #else #define SOL_PLATFORM_X16_I_ SOL_OFF #define SOL_PLATFORM_X86_I_ SOL_OFF #define SOL_PLATFORM_X64_I_ SOL_ON #endif #define SOL_PLATFORM_ARM32_I_ SOL_OFF #define SOL_PLATFORM_ARM64_I_ SOL_OFF #if defined(SOL_PLATFORM_WINDOWS) #if (SOL_PLATFORM_WINDOWS != 0) #define SOL_PLATFORM_WINDOWS_I_ SOL_ON #else #define SOL_PLATFORM_WINDOWS_I_ SOL_OFF #endif #elif defined(_WIN32) #define SOL_PLATFORM_WINDOWS_I_ SOL_DEFAULT_ON #else #define SOL_PLATFORM_WINDOWS_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_PLATFORM_CYGWIN) #if (SOL_PLATFORM_CYGWIN != 0) #define SOL_PLATFORM_CYGWIN_I_ SOL_ON #else #define SOL_PLATFORM_CYGWIN_I_ SOL_ON #endif #elif defined(__CYGWIN__) #define SOL_PLATFORM_CYGWIN_I_ SOL_DEFAULT_ON #else #define SOL_PLATFORM_CYGWIN_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_PLATFORM_APPLE) #if (SOL_PLATFORM_APPLE != 0) #define SOL_PLATFORM_APPLE_I_ SOL_ON #else #define SOL_PLATFORM_APPLE_I_ SOL_OFF #endif #elif defined(__APPLE__) #define SOL_PLATFORM_APPLE_I_ SOL_DEFAULT_ON #else #define SOL_PLATFORM_APPLE_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_PLATFORM_UNIX) #if (SOL_PLATFORM_UNIX != 0) #define SOL_PLATFORM_UNIXLIKE_I_ SOL_ON #else #define SOL_PLATFORM_UNIXLIKE_I_ SOL_OFF #endif #elif defined(__unix__) #define SOL_PLATFORM_UNIXLIKE_I_ SOL_DEFAULT_ON #else #define SOL_PLATFORM_UNIXLIKE_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_PLATFORM_LINUX) #if (SOL_PLATFORM_LINUX != 0) #define SOL_PLATFORM_LINUXLIKE_I_ SOL_ON #else #define SOL_PLATFORM_LINUXLIKE_I_ SOL_OFF #endif #elif defined(__LINUX__) #define SOL_PLATFORM_LINUXLIKE_I_ SOL_DEFAULT_ON #else #define SOL_PLATFORM_LINUXLIKE_I_ SOL_DEFAULT_OFF #endif #define SOL_PLATFORM_APPLE_IPHONE_I_ SOL_OFF #define SOL_PLATFORM_BSDLIKE_I_ SOL_OFF #if defined(SOL_IN_DEBUG_DETECTED) #if (SOL_IN_DEBUG_DETECTED != 0) #define SOL_DEBUG_BUILD_I_ SOL_ON #else #define SOL_DEBUG_BUILD_I_ SOL_OFF #endif #elif !defined(NDEBUG) #if SOL_IS_ON(SOL_COMPILER_VCXX) && defined(_DEBUG) #define SOL_DEBUG_BUILD_I_ SOL_ON #elif (SOL_IS_ON(SOL_COMPILER_CLANG) || SOL_IS_ON(SOL_COMPILER_GCC)) && !defined(__OPTIMIZE__) #define SOL_DEBUG_BUILD_I_ SOL_ON #else #define SOL_DEBUG_BUILD_I_ SOL_OFF #endif #else #define SOL_DEBUG_BUILD_I_ SOL_DEFAULT_OFF #endif // We are in a debug mode of some sort #if defined(SOL_NO_EXCEPTIONS) #if (SOL_NO_EXCEPTIONS != 0) #define SOL_EXCEPTIONS_I_ SOL_OFF #else #define SOL_EXCEPTIONS_I_ SOL_ON #endif #elif SOL_IS_ON(SOL_COMPILER_VCXX) #if !defined(_CPPUNWIND) #define SOL_EXCEPTIONS_I_ SOL_OFF #else #define SOL_EXCEPTIONS_I_ SOL_ON #endif #elif SOL_IS_ON(SOL_COMPILER_CLANG) || SOL_IS_ON(SOL_COMPILER_GCC) #if !defined(__EXCEPTIONS) #define SOL_EXCEPTIONS_I_ SOL_OFF #else #define SOL_EXCEPTIONS_I_ SOL_ON #endif #else #define SOL_EXCEPTIONS_I_ SOL_DEFAULT_ON #endif #if defined(SOL_NO_RTTI) #if (SOL_NO_RTTI != 0) #define SOL_RTTI_I_ SOL_OFF #else #define SOL_RTTI_I_ SOL_ON #endif #elif SOL_IS_ON(SOL_COMPILER_VCXX) #if !defined(_CPPRTTI) #define SOL_RTTI_I_ SOL_OFF #else #define SOL_RTTI_I_ SOL_ON #endif #elif SOL_IS_ON(SOL_COMPILER_CLANG) || SOL_IS_ON(SOL_COMPILER_GCC) #if !defined(__GXX_RTTI) #define SOL_RTTI_I_ SOL_OFF #else #define SOL_RTTI_I_ SOL_ON #endif #else #define SOL_RTTI_I_ SOL_DEFAULT_ON #endif #if defined(SOL_NO_THREAD_LOCAL) #if (SOL_NO_THREAD_LOCAL != 0) #define SOL_USE_THREAD_LOCAL_I_ SOL_OFF #else #define SOL_USE_THREAD_LOCAL_I_ SOL_ON #endif #else #define SOL_USE_THREAD_LOCAL_I_ SOL_DEFAULT_ON #endif // thread_local keyword is bjorked on some platforms #if defined(SOL_ALL_SAFETIES_ON) #if (SOL_ALL_SAFETIES_ON != 0) #define SOL_ALL_SAFETIES_ON_I_ SOL_ON #else #define SOL_ALL_SAFETIES_ON_I_ SOL_OFF #endif #else #define SOL_ALL_SAFETIES_ON_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_SAFE_GETTER) #if (SOL_SAFE_GETTER != 0) #define SOL_SAFE_GETTER_I_ SOL_ON #else #define SOL_SAFE_GETTER_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_SAFE_GETTER_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_SAFE_GETTER_I_ SOL_DEFAULT_ON #else #define SOL_SAFE_GETTER_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_SAFE_USERTYPE) #if (SOL_SAFE_USERTYPE != 0) #define SOL_SAFE_USERTYPE_I_ SOL_ON #else #define SOL_SAFE_USERTYPE_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_SAFE_USERTYPE_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_SAFE_USERTYPE_I_ SOL_DEFAULT_ON #else #define SOL_SAFE_USERTYPE_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_SAFE_REFERENCES) #if (SOL_SAFE_REFERENCES != 0) #define SOL_SAFE_REFERENCES_I_ SOL_ON #else #define SOL_SAFE_REFERENCES_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_SAFE_REFERENCES_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_SAFE_REFERENCES_I_ SOL_DEFAULT_ON #else #define SOL_SAFE_REFERENCES_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_SAFE_FUNCTIONS) #if (SOL_SAFE_FUNCTIONS != 0) #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_ON #else #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_OFF #endif #elif defined (SOL_SAFE_FUNCTION_OBJECTS) #if (SOL_SAFE_FUNCTION_OBJECTS != 0) #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_ON #else #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_DEFAULT_ON #else #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_SAFE_FUNCTION_CALLS) #if (SOL_SAFE_FUNCTION_CALLS != 0) #define SOL_SAFE_FUNCTION_CALLS_I_ SOL_ON #else #define SOL_SAFE_FUNCTION_CALLS_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_SAFE_FUNCTION_CALLS_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_SAFE_FUNCTION_CALLS_I_ SOL_DEFAULT_ON #else #define SOL_SAFE_FUNCTION_CALLS_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_SAFE_PROXIES) #if (SOL_SAFE_PROXIES != 0) #define SOL_SAFE_PROXIES_I_ SOL_ON #else #define SOL_SAFE_PROXIES_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_SAFE_PROXIES_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_SAFE_PROXIES_I_ SOL_DEFAULT_ON #else #define SOL_SAFE_PROXIES_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_SAFE_NUMERICS) #if (SOL_SAFE_NUMERICS != 0) #define SOL_SAFE_NUMERICS_I_ SOL_ON #else #define SOL_SAFE_NUMERICS_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_SAFE_NUMERICS_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_SAFE_NUMERICS_I_ SOL_DEFAULT_ON #else #define SOL_SAFE_NUMERICS_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_ALL_INTEGER_VALUES_FIT) #if (SOL_ALL_INTEGER_VALUES_FIT != 0) #define SOL_ALL_INTEGER_VALUES_FIT_I_ SOL_ON #else #define SOL_ALL_INTEGER_VALUES_FIT_I_ SOL_OFF #endif #elif !SOL_IS_DEFAULT_OFF(SOL_SAFE_NUMERICS) && SOL_IS_OFF(SOL_SAFE_NUMERICS) // if numerics is intentionally turned off, flip this on #define SOL_ALL_INTEGER_VALUES_FIT_I_ SOL_DEFAULT_ON #else // default to off #define SOL_ALL_INTEGER_VALUES_FIT_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_SAFE_STACK_CHECK) #if (SOL_SAFE_STACK_CHECK != 0) #define SOL_SAFE_STACK_CHECK_I_ SOL_ON #else #define SOL_SAFE_STACK_CHECK_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_SAFE_STACK_CHECK_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_SAFE_STACK_CHECK_I_ SOL_DEFAULT_ON #else #define SOL_SAFE_STACK_CHECK_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_NO_CHECK_NUMBER_PRECISION) #if (SOL_NO_CHECK_NUMBER_PRECISION != 0) #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_OFF #else #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_ON #endif #elif defined(SOL_NO_CHECKING_NUMBER_PRECISION) #if (SOL_NO_CHECKING_NUMBER_PRECISION != 0) #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_OFF #else #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_ON #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_ON #elif SOL_IS_ON(SOL_SAFE_NUMERICS) #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_DEFAULT_ON #else #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_STRINGS_ARE_NUMBERS) #if (SOL_STRINGS_ARE_NUMBERS != 0) #define SOL_STRINGS_ARE_NUMBERS_I_ SOL_ON #else #define SOL_STRINGS_ARE_NUMBERS_I_ SOL_OFF #endif #else #define SOL_STRINGS_ARE_NUMBERS_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_ENABLE_INTEROP) #if (SOL_ENABLE_INTEROP != 0) #define SOL_USE_INTEROP_I_ SOL_ON #else #define SOL_USE_INTEROP_I_ SOL_OFF #endif #elif defined(SOL_USE_INTEROP) #if (SOL_USE_INTEROP != 0) #define SOL_USE_INTEROP_I_ SOL_ON #else #define SOL_USE_INTEROP_I_ SOL_OFF #endif #else #define SOL_USE_INTEROP_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_NO_NIL) #if (SOL_NO_NIL != 0) #define SOL_NIL_I_ SOL_OFF #else #define SOL_NIL_I_ SOL_ON #endif #elif defined(__MAC_OS_X_VERSION_MAX_ALLOWED) || defined(__OBJC__) || defined(nil) #define SOL_NIL_I_ SOL_DEFAULT_OFF #else #define SOL_NIL_I_ SOL_DEFAULT_ON #endif #if defined(SOL_USERTYPE_TYPE_BINDING_INFO) #if (SOL_USERTYPE_TYPE_BINDING_INFO != 0) #define SOL_USERTYPE_TYPE_BINDING_INFO_I_ SOL_ON #else #define SOL_USERTYPE_TYPE_BINDING_INFO_I_ SOL_OFF #endif #else #define SOL_USERTYPE_TYPE_BINDING_INFO_I_ SOL_DEFAULT_ON #endif // We should generate a my_type.__type table with lots of class information for usertypes #if defined(SOL_AUTOMAGICAL_TYPES_BY_DEFAULT) #if (SOL_AUTOMAGICAL_TYPES_BY_DEFAULT != 0) #define SOL_DEFAULT_AUTOMAGICAL_USERTYPES_I_ SOL_ON #else #define SOL_DEFAULT_AUTOMAGICAL_USERTYPES_I_ SOL_OFF #endif #elif defined(SOL_DEFAULT_AUTOMAGICAL_USERTYPES) #if (SOL_DEFAULT_AUTOMAGICAL_USERTYPES != 0) #define SOL_DEFAULT_AUTOMAGICAL_USERTYPES_I_ SOL_ON #else #define SOL_DEFAULT_AUTOMAGICAL_USERTYPES_I_ SOL_OFF #endif #else #define SOL_DEFAULT_AUTOMAGICAL_USERTYPES_I_ SOL_DEFAULT_ON #endif // make is_automagical on/off by default #if defined(SOL_STD_VARIANT) #if (SOL_STD_VARIANT != 0) #define SOL_STD_VARIANT_I_ SOL_ON #else #define SOL_STD_VARIANT_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_COMPILER_CLANG) && SOL_IS_ON(SOL_PLATFORM_APPLE) #if defined(__has_include) #if __has_include() #define SOL_STD_VARIANT_I_ SOL_DEFAULT_ON #else #define SOL_STD_VARIANT_I_ SOL_DEFAULT_OFF #endif #else #define SOL_STD_VARIANT_I_ SOL_DEFAULT_OFF #endif #else #define SOL_STD_VARIANT_I_ SOL_DEFAULT_ON #endif #endif // make is_automagical on/off by default #if defined(SOL_NOEXCEPT_FUNCTION_TYPE) #if (SOL_NOEXCEPT_FUNCTION_TYPE != 0) #define SOL_USE_NOEXCEPT_FUNCTION_TYPE_I_ SOL_ON #else #define SOL_USE_NOEXCEPT_FUNCTION_TYPE_I_ SOL_OFF #endif #else #if defined(__cpp_noexcept_function_type) #define SOL_USE_NOEXCEPT_FUNCTION_TYPE_I_ SOL_ON #elif SOL_IS_ON(SOL_COMPILER_VCXX) && (defined(_MSVC_LANG) && (_MSVC_LANG < 201403L)) // There is a bug in the VC++ compiler?? // on /std:c++latest under x86 conditions (VS 15.5.2), // compiler errors are tossed for noexcept markings being on function types // that are identical in every other way to their non-noexcept marked types function types... // VS 2019: There is absolutely a bug. #define SOL_USE_NOEXCEPT_FUNCTION_TYPE_I_ SOL_OFF #else #define SOL_USE_NOEXCEPT_FUNCTION_TYPE_I_ SOL_DEFAULT_ON #endif #endif // noexcept is part of a function's type #if defined(SOL_STACK_STRING_OPTIMIZATION_SIZE) && (SOL_STACK_STRING_OPTIMIZATION_SIZE > 0) #define SOL_OPTIMIZATION_STRING_CONVERSION_STACK_SIZE_I_ SOL_STACK_STRING_OPTIMIZATION_SIZE #else #define SOL_OPTIMIZATION_STRING_CONVERSION_STACK_SIZE_I_ 1024 #endif #if defined(SOL_ID_SIZE) && (SOL_ID_SIZE > 0) #define SOL_ID_SIZE_I_ SOL_ID_SIZE #else #define SOL_ID_SIZE_I_ 512 #endif #if defined(LUA_IDSIZE) && (LUA_IDSIZE > 0) #define SOL_FILE_ID_SIZE_I_ LUA_IDSIZE #elif defined(SOL_ID_SIZE) && SOL_ID_SIZE > 0 #define SOL_FILE_ID_SIZE_I_ SOL_FILE_ID_SIZE #else #define SOL_FILE_ID_SIZE_I_ 2048 #endif #if defined(SOL_PRINT_ERRORS) #if (SOL_PRINT_ERRORS != 0) #define SOL_PRINT_ERRORS_I_ SOL_ON #else #define SOL_PRINT_ERRORS_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_PRINT_ERRORS_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_PRINT_ERRORS_I_ SOL_DEFAULT_ON #else #define SOL_PRINT_ERRORS_I_ SOL_OFF #endif #endif #if defined(SOL_DEFAULT_PASS_ON_ERROR) #if (SOL_DEFAULT_PASS_ON_ERROR != 0) #define SOL_DEFAULT_PASS_ON_ERROR_I_ SOL_ON #else #define SOL_DEFAULT_PASS_ON_ERROR_I_ SOL_OFF #endif #else #define SOL_DEFAULT_PASS_ON_ERROR_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_USING_CXX_LUA) #if (SOL_USING_CXX_LUA != 0) #define SOL_USING_CXX_LUA_I_ SOL_ON #else #define SOL_USING_CXX_LUA_I_ SOL_OFF #endif #elif defined(SOL_USE_CXX_LUA) // alternative spelling #if (SOL_USE_CXX_LUA != 0) #define SOL_USING_CXX_LUA_I_ SOL_ON #else #define SOL_USING_CXX_LUA_I_ SOL_OFF #endif #else #define SOL_USING_CXX_LUA_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_USING_CXX_LUAJIT) #if (SOL_USING_CXX_LUAJIT != 0) #define SOL_USING_CXX_LUAJIT_I_ SOL_ON #else #define SOL_USING_CXX_LUAJIT_I_ SOL_OFF #endif #elif defined(SOL_USE_CXX_LUAJIT) #if (SOL_USE_CXX_LUAJIT != 0) #define SOL_USING_CXX_LUAJIT_I_ SOL_ON #else #define SOL_USING_CXX_LUAJIT_I_ SOL_OFF #endif #else #define SOL_USING_CXX_LUAJIT_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_NO_LUA_HPP) #if (SOL_NO_LUA_HPP != 0) #define SOL_USE_LUA_HPP_I_ SOL_OFF #else #define SOL_USE_LUA_HPP_I_ SOL_ON #endif #elif SOL_IS_ON(SOL_USING_CXX_LUA) #define SOL_USE_LUA_HPP_I_ SOL_OFF #elif defined(__has_include) #if __has_include() #define SOL_USE_LUA_HPP_I_ SOL_ON #else #define SOL_USE_LUA_HPP_I_ SOL_OFF #endif #else #define SOL_USE_LUA_HPP_I_ SOL_DEFAULT_ON #endif #if defined(SOL_CONTAINERS_START) #define SOL_CONTAINER_START_INDEX_I_ SOL_CONTAINERS_START #elif defined(SOL_CONTAINERS_START_INDEX) #define SOL_CONTAINER_START_INDEX_I_ SOL_CONTAINERS_START_INDEX #elif defined(SOL_CONTAINER_START_INDEX) #define SOL_CONTAINER_START_INDEX_I_ SOL_CONTAINER_START_INDEX #else #define SOL_CONTAINER_START_INDEX_I_ 1 #endif #if defined (SOL_NO_MEMORY_ALIGNMENT) #if (SOL_NO_MEMORY_ALIGNMENT != 0) #define SOL_ALIGN_MEMORY_I_ SOL_OFF #else #define SOL_ALIGN_MEMORY_I_ SOL_ON #endif #else #define SOL_ALIGN_MEMORY_I_ SOL_DEFAULT_ON #endif #if defined(SOL_USE_BOOST) #if (SOL_USE_BOOST != 0) #define SOL_USE_BOOST_I_ SOL_ON #else #define SOL_USE_BOOST_I_ SOL_OFF #endif #else #define SOL_USE_BOOST_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_USE_UNSAFE_BASE_LOOKUP) #if (SOL_USE_UNSAFE_BASE_LOOKUP != 0) #define SOL_USE_UNSAFE_BASE_LOOKUP_I_ SOL_ON #else #define SOL_USE_UNSAFE_BASE_LOOKUP_I_ SOL_OFF #endif #else #define SOL_USE_UNSAFE_BASE_LOOKUP_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_INSIDE_UNREAL) #if (SOL_INSIDE_UNREAL != 0) #define SOL_INSIDE_UNREAL_ENGINE_I_ SOL_ON #else #define SOL_INSIDE_UNREAL_ENGINE_I_ SOL_OFF #endif #else #if defined(UE_BUILD_DEBUG) || defined(UE_BUILD_DEVELOPMENT) || defined(UE_BUILD_TEST) || defined(UE_BUILD_SHIPPING) || defined(UE_SERVER) #define SOL_INSIDE_UNREAL_ENGINE_I_ SOL_DEFAULT_ON #else #define SOL_INSIDE_UNREAL_ENGINE_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_NO_COMPAT) #if (SOL_NO_COMPAT != 0) #define SOL_USE_COMPATIBILITY_LAYER_I_ SOL_OFF #else #define SOL_USE_COMPATIBILITY_LAYER_I_ SOL_ON #endif #else #define SOL_USE_COMPATIBILITY_LAYER_I_ SOL_DEFAULT_ON #endif #if defined(SOL_GET_FUNCTION_POINTER_UNSAFE) #if (SOL_GET_FUNCTION_POINTER_UNSAFE != 0) #define SOL_GET_FUNCTION_POINTER_UNSAFE_I_ SOL_ON #else #define SOL_GET_FUNCTION_POINTER_UNSAFE_I_ SOL_OFF #endif #else #define SOL_GET_FUNCTION_POINTER_UNSAFE_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_CONTAINER_CHECK_IS_EXHAUSTIVE) #if (SOL_CONTAINER_CHECK_IS_EXHAUSTIVE != 0) #define SOL_CONTAINER_CHECK_IS_EXHAUSTIVE_I_ SOL_ON #else #define SOL_CONTAINER_CHECK_IS_EXHAUSTIVE_I_ SOL_OFF #endif #else #define SOL_CONTAINER_CHECK_IS_EXHAUSTIVE_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_FUNCTION_CALL_VALUE_SEMANTICS) #if (SOL_FUNCTION_CALL_VALUE_SEMANTICS != 0) #define SOL_FUNCTION_CALL_VALUE_SEMANTICS_I_ SOL_ON #else #define SOL_FUNCTION_CALL_VALUE_SEMANTICS_I_ SOL_OFF #endif #else #define SOL_FUNCTION_CALL_VALUE_SEMANTICS_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_MINGW_CCTYPE_IS_POISONED) #if (SOL_MINGW_CCTYPE_IS_POISONED != 0) #define SOL_MINGW_CCTYPE_IS_POISONED_I_ SOL_ON #else #define SOL_MINGW_CCTYPE_IS_POISONED_I_ SOL_OFF #endif #elif SOL_IS_ON(SOL_COMPILER_MINGW) && defined(__GNUC__) && (__GNUC__ < 6) // MinGW is off its rocker in some places... #define SOL_MINGW_CCTYPE_IS_POISONED_I_ SOL_DEFAULT_ON #else #define SOL_MINGW_CCTYPE_IS_POISONED_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_CHAR8_T) #if (SOL_CHAR8_T != 0) #define SOL_CHAR8_T_I_ SOL_ON #else #define SOL_CHAR8_T_I_ SOL_OFF #endif #else #if defined(__cpp_char8_t) #define SOL_CHAR8_T_I_ SOL_DEFAULT_ON #else #define SOL_CHAR8_T_I_ SOL_DEFAULT_OFF #endif #endif #if SOL_IS_ON(SOL_USE_BOOST) #include #if BOOST_VERSION >= 107500 // Since Boost 1.75.0 boost::none is constexpr #define SOL_BOOST_NONE_CONSTEXPR_I_ constexpr #else #define SOL_BOOST_NONE_CONSTEXPR_I_ const #endif // BOOST_VERSION #else // assume boost isn't using a garbage version #define SOL_BOOST_NONE_CONSTEXPR_I_ constexpr #endif #if defined(SOL2_CI) #if (SOL2_CI != 0) #define SOL2_CI_I_ SOL_ON #else #define SOL2_CI_I_ SOL_OFF #endif #else #define SOL2_CI_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_ASSERT) #define SOL_USER_ASSERT_I_ SOL_ON #else #define SOL_USER_ASSERT_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_ASSERT_MSG) #define SOL_USER_ASSERT_MSG_I_ SOL_ON #else #define SOL_USER_ASSERT_MSG_I_ SOL_DEFAULT_OFF #endif // beginning of sol/prologue.hpp #if defined(SOL_PROLOGUE_I_) #error "[sol2] Library Prologue was already included in translation unit and not properly ended with an epilogue." #endif #define SOL_PROLOGUE_I_ 1 #if SOL_IS_ON(SOL_BUILD_CXX_MODE) #define _FWD(...) static_cast( __VA_ARGS__ ) #if SOL_IS_ON(SOL_COMPILER_GCC) || SOL_IS_ON(SOL_COMPILER_CLANG) #define _MOVE(...) static_cast<__typeof( __VA_ARGS__ )&&>( __VA_ARGS__ ) #else #include #define _MOVE(...) static_cast<::std::remove_reference_t<( __VA_ARGS__ )>&&>( __VA_OPT__(,) ) #endif #endif // end of sol/prologue.hpp // beginning of sol/epilogue.hpp #if !defined(SOL_PROLOGUE_I_) #error "[sol2] Library Prologue is missing from this translation unit." #else #undef SOL_PROLOGUE_I_ #endif #if SOL_IS_ON(SOL_BUILD_CXX_MODE) #undef _FWD #undef _MOVE #endif // end of sol/epilogue.hpp // beginning of sol/detail/build_version.hpp #if defined(SOL_DLL) #if (SOL_DLL != 0) #define SOL_DLL_I_ SOL_ON #else #define SOL_DLL_I_ SOL_OFF #endif #elif SOL_IS_ON(SOL_COMPILER_VCXX) && (defined(DLL_) || defined(_DLL)) #define SOL_DLL_I_ SOL_DEFAULT_ON #else #define SOL_DLL_I_ SOL_DEFAULT_OFF #endif // DLL definition #if defined(SOL_HEADER_ONLY) #if (SOL_HEADER_ONLY != 0) #define SOL_HEADER_ONLY_I_ SOL_ON #else #define SOL_HEADER_ONLY_I_ SOL_OFF #endif #else #define SOL_HEADER_ONLY_I_ SOL_DEFAULT_OFF #endif // Header only library #if defined(SOL_BUILD) #if (SOL_BUILD != 0) #define SOL_BUILD_I_ SOL_ON #else #define SOL_BUILD_I_ SOL_OFF #endif #elif SOL_IS_ON(SOL_HEADER_ONLY) #define SOL_BUILD_I_ SOL_DEFAULT_OFF #else #define SOL_BUILD_I_ SOL_DEFAULT_ON #endif #if defined(SOL_UNITY_BUILD) #if (SOL_UNITY_BUILD != 0) #define SOL_UNITY_BUILD_I_ SOL_ON #else #define SOL_UNITY_BUILD_I_ SOL_OFF #endif #else #define SOL_UNITY_BUILD_I_ SOL_DEFAULT_OFF #endif // Header only library #if defined(SOL_C_FUNCTION_LINKAGE) #define SOL_C_FUNCTION_LINKAGE_I_ SOL_C_FUNCTION_LINKAGE #else #if SOL_IS_ON(SOL_BUILD_CXX_MODE) // C++ #define SOL_C_FUNCTION_LINKAGE_I_ extern "C" #else // normal #define SOL_C_FUNCTION_LINKAGE_I_ #endif // C++ or not #endif // Linkage specification for C functions #if defined(SOL_API_LINKAGE) #define SOL_API_LINKAGE_I_ SOL_API_LINKAGE #else #if SOL_IS_ON(SOL_DLL) #if SOL_IS_ON(SOL_COMPILER_VCXX) || SOL_IS_ON(SOL_PLATFORM_WINDOWS) || SOL_IS_ON(SOL_PLATFORM_CYGWIN) // MSVC Compiler; or, Windows, or Cygwin platforms #if SOL_IS_ON(SOL_BUILD) // Building the library #if SOL_IS_ON(SOL_COMPILER_GCC) // Using GCC #define SOL_API_LINKAGE_I_ __attribute__((dllexport)) #else // Using Clang, MSVC, etc... #define SOL_API_LINKAGE_I_ __declspec(dllexport) #endif #else #if SOL_IS_ON(SOL_COMPILER_GCC) #define SOL_API_LINKAGE_I_ __attribute__((dllimport)) #else #define SOL_API_LINKAGE_I_ __declspec(dllimport) #endif #endif #else // extern if building normally on non-MSVC #define SOL_API_LINKAGE_I_ extern #endif #elif SOL_IS_ON(SOL_UNITY_BUILD) // Built-in library, like how stb typical works #if SOL_IS_ON(SOL_HEADER_ONLY) // Header only, so functions are defined "inline" #define SOL_API_LINKAGE_I_ inline #else // Not header only, so seperately compiled files #define SOL_API_LINKAGE_I_ extern #endif #else // Normal static library #if SOL_IS_ON(SOL_BUILD_CXX_MODE) #define SOL_API_LINKAGE_I_ #else #define SOL_API_LINKAGE_I_ extern #endif #endif // DLL or not #endif // Build definitions #if defined(SOL_PUBLIC_FUNC_DECL) #define SOL_PUBLIC_FUNC_DECL_I_ SOL_PUBLIC_FUNC_DECL #else #define SOL_PUBLIC_FUNC_DECL_I_ SOL_API_LINKAGE_I_ #endif #if defined(SOL_INTERNAL_FUNC_DECL_) #define SOL_INTERNAL_FUNC_DECL_I_ SOL_INTERNAL_FUNC_DECL_ #else #define SOL_INTERNAL_FUNC_DECL_I_ SOL_API_LINKAGE_I_ #endif #if defined(SOL_PUBLIC_FUNC_DEF) #define SOL_PUBLIC_FUNC_DEF_I_ SOL_PUBLIC_FUNC_DEF #else #define SOL_PUBLIC_FUNC_DEF_I_ SOL_API_LINKAGE_I_ #endif #if defined(SOL_INTERNAL_FUNC_DEF) #define SOL_INTERNAL_FUNC_DEF_I_ SOL_INTERNAL_FUNC_DEF #else #define SOL_INTERNAL_FUNC_DEF_I_ SOL_API_LINKAGE_I_ #endif #if defined(SOL_FUNC_DECL) #define SOL_FUNC_DECL_I_ SOL_FUNC_DECL #elif SOL_IS_ON(SOL_HEADER_ONLY) #define SOL_FUNC_DECL_I_ #elif SOL_IS_ON(SOL_DLL) #if SOL_IS_ON(SOL_COMPILER_VCXX) #if SOL_IS_ON(SOL_BUILD) #define SOL_FUNC_DECL_I_ extern __declspec(dllexport) #else #define SOL_FUNC_DECL_I_ extern __declspec(dllimport) #endif #elif SOL_IS_ON(SOL_COMPILER_GCC) || SOL_IS_ON(SOL_COMPILER_CLANG) #define SOL_FUNC_DECL_I_ extern __attribute__((visibility("default"))) #else #define SOL_FUNC_DECL_I_ extern #endif #endif #if defined(SOL_FUNC_DEFN) #define SOL_FUNC_DEFN_I_ SOL_FUNC_DEFN #elif SOL_IS_ON(SOL_HEADER_ONLY) #define SOL_FUNC_DEFN_I_ inline #elif SOL_IS_ON(SOL_DLL) #if SOL_IS_ON(SOL_COMPILER_VCXX) #if SOL_IS_ON(SOL_BUILD) #define SOL_FUNC_DEFN_I_ __declspec(dllexport) #else #define SOL_FUNC_DEFN_I_ __declspec(dllimport) #endif #elif SOL_IS_ON(SOL_COMPILER_GCC) || SOL_IS_ON(SOL_COMPILER_CLANG) #define SOL_FUNC_DEFN_I_ __attribute__((visibility("default"))) #else #define SOL_FUNC_DEFN_I_ #endif #endif #if defined(SOL_HIDDEN_FUNC_DECL) #define SOL_HIDDEN_FUNC_DECL_I_ SOL_HIDDEN_FUNC_DECL #elif SOL_IS_ON(SOL_HEADER_ONLY) #define SOL_HIDDEN_FUNC_DECL_I_ #elif SOL_IS_ON(SOL_DLL) #if SOL_IS_ON(SOL_COMPILER_VCXX) #if SOL_IS_ON(SOL_BUILD) #define SOL_HIDDEN_FUNC_DECL_I_ extern __declspec(dllexport) #else #define SOL_HIDDEN_FUNC_DECL_I_ extern __declspec(dllimport) #endif #elif SOL_IS_ON(SOL_COMPILER_GCC) || SOL_IS_ON(SOL_COMPILER_CLANG) #define SOL_HIDDEN_FUNC_DECL_I_ extern __attribute__((visibility("default"))) #else #define SOL_HIDDEN_FUNC_DECL_I_ extern #endif #endif #if defined(SOL_HIDDEN_FUNC_DEFN) #define SOL_HIDDEN_FUNC_DEFN_I_ SOL_HIDDEN_FUNC_DEFN #elif SOL_IS_ON(SOL_HEADER_ONLY) #define SOL_HIDDEN_FUNC_DEFN_I_ inline #elif SOL_IS_ON(SOL_DLL) #if SOL_IS_ON(SOL_COMPILER_VCXX) #if SOL_IS_ON(SOL_BUILD) #define SOL_HIDDEN_FUNC_DEFN_I_ #else #define SOL_HIDDEN_FUNC_DEFN_I_ #endif #elif SOL_IS_ON(SOL_COMPILER_GCC) || SOL_IS_ON(SOL_COMPILER_CLANG) #define SOL_HIDDEN_FUNC_DEFN_I_ __attribute__((visibility("hidden"))) #else #define SOL_HIDDEN_FUNC_DEFN_I_ #endif #endif // end of sol/detail/build_version.hpp // end of sol/version.hpp #include #include #include #if SOL_IS_ON(SOL_USING_CXX_LUA) || SOL_IS_ON(SOL_USING_CXX_LUAJIT) struct lua_State; #else extern "C" { struct lua_State; } #endif // C++ Mangling for Lua vs. Not namespace sol { enum class type; class stateless_reference; template class basic_reference; using reference = basic_reference; using main_reference = basic_reference; class stateless_stack_reference; class stack_reference; template class basic_bytecode; struct lua_value; struct proxy_base_tag; template struct proxy_base; template struct table_proxy; template class basic_table_core; template using table_core = basic_table_core; template using main_table_core = basic_table_core; template using stack_table_core = basic_table_core; template using basic_table = basic_table_core; using table = table_core; using global_table = table_core; using main_table = main_table_core; using main_global_table = main_table_core; using stack_table = stack_table_core; using stack_global_table = stack_table_core; template struct basic_lua_table; using lua_table = basic_lua_table; using stack_lua_table = basic_lua_table; template class basic_usertype; template using usertype = basic_usertype; template using stack_usertype = basic_usertype; template class basic_metatable; using metatable = basic_metatable; using stack_metatable = basic_metatable; template struct basic_environment; using environment = basic_environment; using main_environment = basic_environment; using stack_environment = basic_environment; template class basic_function; template class basic_protected_function; using unsafe_function = basic_function; using safe_function = basic_protected_function; using main_unsafe_function = basic_function; using main_safe_function = basic_protected_function; using stack_unsafe_function = basic_function; using stack_safe_function = basic_protected_function; using stack_aligned_unsafe_function = basic_function; using stack_aligned_safe_function = basic_protected_function; using protected_function = safe_function; using main_protected_function = main_safe_function; using stack_protected_function = stack_safe_function; using stack_aligned_protected_function = stack_aligned_safe_function; #if SOL_IS_ON(SOL_SAFE_FUNCTION_OBJECTS) using function = protected_function; using main_function = main_protected_function; using stack_function = stack_protected_function; using stack_aligned_function = stack_aligned_safe_function; #else using function = unsafe_function; using main_function = main_unsafe_function; using stack_function = stack_unsafe_function; using stack_aligned_function = stack_aligned_unsafe_function; #endif using stack_aligned_stack_handler_function = basic_protected_function; struct unsafe_function_result; struct protected_function_result; using safe_function_result = protected_function_result; #if SOL_IS_ON(SOL_SAFE_FUNCTION_OBJECTS) using function_result = safe_function_result; #else using function_result = unsafe_function_result; #endif template class basic_object_base; template class basic_object; template class basic_userdata; template class basic_lightuserdata; template class basic_coroutine; template class basic_packaged_coroutine; template class basic_thread; using object = basic_object; using userdata = basic_userdata; using lightuserdata = basic_lightuserdata; using thread = basic_thread; using coroutine = basic_coroutine; using packaged_coroutine = basic_packaged_coroutine; using main_object = basic_object; using main_userdata = basic_userdata; using main_lightuserdata = basic_lightuserdata; using main_coroutine = basic_coroutine; using stack_object = basic_object; using stack_userdata = basic_userdata; using stack_lightuserdata = basic_lightuserdata; using stack_thread = basic_thread; using stack_coroutine = basic_coroutine; struct stack_proxy_base; struct stack_proxy; struct variadic_args; struct variadic_results; struct stack_count; struct this_state; struct this_main_state; struct this_environment; class state_view; class state; template struct as_table_t; template struct as_container_t; template struct nested; template struct light; template struct user; template struct as_args_t; template struct protect_t; template struct policy_wrapper; template struct usertype_traits; template struct unique_usertype_traits; template struct types { typedef std::make_index_sequence indices; static constexpr std::size_t size() { return sizeof...(Args); } }; template struct derive : std::false_type { typedef types<> type; }; template struct base : std::false_type { typedef types<> type; }; template struct weak_derive { static bool value; }; template bool weak_derive::value = false; namespace stack { struct record; } #if SOL_IS_OFF(SOL_USE_BOOST) template class optional; template class optional; #endif using check_handler_type = int(lua_State*, int, type, type, const char*); } // namespace sol #define SOL_BASE_CLASSES(T, ...) \ namespace sol { \ template <> \ struct base : std::true_type { \ typedef ::sol::types<__VA_ARGS__> type; \ }; \ } \ static_assert(true, "") #define SOL_DERIVED_CLASSES(T, ...) \ namespace sol { \ template <> \ struct derive : std::true_type { \ typedef ::sol::types<__VA_ARGS__> type; \ }; \ } \ static_assert(true, "") #endif // SOL_FORWARD_HPP // end of sol/forward.hpp #endif // SOL_SINGLE_INCLUDE_SOL_FORWARD_HPP ValveSoftware-gamescope-eb620ab/thirdparty/sol/sol.hpp000066400000000000000000035767601502457270500232620ustar00rootroot00000000000000// The MIT License (MIT) // Copyright (c) 2013-2020 Rapptz, ThePhD and contributors // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // This file was generated with a script. // Generated 2024-09-03 03:46:49.867770 UTC // This header was generated with sol v3.3.1 (revision 2b0d2fe8) // https://github.com/ThePhD/sol2 #ifndef SOL_SINGLE_INCLUDE_SOL_HPP #define SOL_SINGLE_INCLUDE_SOL_HPP // beginning of sol/sol.hpp #ifndef SOL_HPP #define SOL_HPP // beginning of sol/version.hpp #include #define SOL_VERSION_MAJOR 3 #define SOL_VERSION_MINOR 2 #define SOL_VERSION_PATCH 3 #define SOL_VERSION_STRING "3.2.3" #define SOL_VERSION ((SOL_VERSION_MAJOR * 100000) + (SOL_VERSION_MINOR * 100) + (SOL_VERSION_PATCH)) #define SOL_TOKEN_TO_STRING_POST_EXPANSION_I_(_TOKEN) #_TOKEN #define SOL_TOKEN_TO_STRING_I_(_TOKEN) SOL_TOKEN_TO_STRING_POST_EXPANSION_I_(_TOKEN) #define SOL_CONCAT_TOKENS_POST_EXPANSION_I_(_LEFT, _RIGHT) _LEFT##_RIGHT #define SOL_CONCAT_TOKENS_I_(_LEFT, _RIGHT) SOL_CONCAT_TOKENS_POST_EXPANSION_I_(_LEFT, _RIGHT) #define SOL_RAW_IS_ON(OP_SYMBOL) ((3 OP_SYMBOL 3) != 0) #define SOL_RAW_IS_OFF(OP_SYMBOL) ((3 OP_SYMBOL 3) == 0) #define SOL_RAW_IS_DEFAULT_ON(OP_SYMBOL) ((3 OP_SYMBOL 3) > 3) #define SOL_RAW_IS_DEFAULT_OFF(OP_SYMBOL) ((3 OP_SYMBOL 3 OP_SYMBOL 3) < 0) #define SOL_IS_ON(OP_SYMBOL) SOL_RAW_IS_ON(OP_SYMBOL ## _I_) #define SOL_IS_OFF(OP_SYMBOL) SOL_RAW_IS_OFF(OP_SYMBOL ## _I_) #define SOL_IS_DEFAULT_ON(OP_SYMBOL) SOL_RAW_IS_DEFAULT_ON(OP_SYMBOL ## _I_) #define SOL_IS_DEFAULT_OFF(OP_SYMBOL) SOL_RAW_IS_DEFAULT_OFF(OP_SYMBOL ## _I_) #define SOL_ON | #define SOL_OFF ^ #define SOL_DEFAULT_ON + #define SOL_DEFAULT_OFF - #if defined(SOL_BUILD_CXX_MODE) #if (SOL_BUILD_CXX_MODE != 0) #define SOL_BUILD_CXX_MODE_I_ SOL_ON #else #define SOL_BUILD_CXX_MODE_I_ SOL_OFF #endif #elif defined(__cplusplus) #define SOL_BUILD_CXX_MODE_I_ SOL_DEFAULT_ON #else #define SOL_BUILD_CXX_MODE_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_BUILD_C_MODE) #if (SOL_BUILD_C_MODE != 0) #define SOL_BUILD_C_MODE_I_ SOL_ON #else #define SOL_BUILD_C_MODE_I_ SOL_OFF #endif #elif defined(__STDC__) #define SOL_BUILD_C_MODE_I_ SOL_DEFAULT_ON #else #define SOL_BUILD_C_MODE_I_ SOL_DEFAULT_OFF #endif #if SOL_IS_ON(SOL_BUILD_C_MODE) #include #include #include #else #include #include #include #endif #if defined(SOL_HAS_BUILTIN) #define SOL_HAS_BUILTIN_I_(...) SOL_HAS_BUILTIN(__VA_ARGS__) #elif defined(__has_builtin) #define SOL_HAS_BUILTIN_I_(...) __has_builtin(__VA_ARGS__) #else #define SOL_HAS_BUILTIN_I_(...) 0 #endif #if defined(SOL_COMPILER_VCXX) #if (SOL_COMPILER_VCXX != 0) #define SOL_COMPILER_VCXX_I_ SOL_ON #else #define SOL_COMPILER_VCXX_I_ SOL_OFF #endif #elif defined(_MSC_VER) #define SOL_COMPILER_VCXX_I_ SOL_DEFAULT_ON #else #define SOL_COMPILER_VCXX_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_COMPILER_GCC) #if (SOL_COMPILER_GCC != 0) #define SOL_COMPILER_GCC_I_ SOL_ON #else #define SOL_COMPILER_GCC_I_ SOL_OFF #endif #elif defined(__GNUC__) #define SOL_COMPILER_GCC_I_ SOL_DEFAULT_ON #else #define SOL_COMPILER_GCC_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_COMPILER_CLANG) #if (SOL_COMPILER_CLANG != 0) #define SOL_COMPILER_CLANG_I_ SOL_ON #else #define SOL_COMPILER_CLANG_I_ SOL_OFF #endif #elif defined(__clang__) #define SOL_COMPILER_CLANG_I_ SOL_DEFAULT_ON #else #define SOL_COMPILER_CLANG_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_COMPILER_EDG) #if (SOL_COMPILER_EDG != 0) #define SOL_COMPILER_EDG_I_ SOL_ON #else #define SOL_COMPILER_EDG_I_ SOL_OFF #endif #else #define SOL_COMPILER_EDG_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_COMPILER_MINGW) #if (SOL_COMPILER_MINGW != 0) #define SOL_COMPILER_MINGW_I_ SOL_ON #else #define SOL_COMPILER_MINGW_I_ SOL_OFF #endif #elif defined(__MINGW32__) #define SOL_COMPILER_MINGW_I_ SOL_DEFAULT_ON #else #define SOL_COMPILER_MINGW_I_ SOL_DEFAULT_OFF #endif #if SIZE_MAX <= 0xFFFFULL #define SOL_PLATFORM_X16_I_ SOL_ON #define SOL_PLATFORM_X86_I_ SOL_OFF #define SOL_PLATFORM_X64_I_ SOL_OFF #elif SIZE_MAX <= 0xFFFFFFFFULL #define SOL_PLATFORM_X16_I_ SOL_OFF #define SOL_PLATFORM_X86_I_ SOL_ON #define SOL_PLATFORM_X64_I_ SOL_OFF #else #define SOL_PLATFORM_X16_I_ SOL_OFF #define SOL_PLATFORM_X86_I_ SOL_OFF #define SOL_PLATFORM_X64_I_ SOL_ON #endif #define SOL_PLATFORM_ARM32_I_ SOL_OFF #define SOL_PLATFORM_ARM64_I_ SOL_OFF #if defined(SOL_PLATFORM_WINDOWS) #if (SOL_PLATFORM_WINDOWS != 0) #define SOL_PLATFORM_WINDOWS_I_ SOL_ON #else #define SOL_PLATFORM_WINDOWS_I_ SOL_OFF #endif #elif defined(_WIN32) #define SOL_PLATFORM_WINDOWS_I_ SOL_DEFAULT_ON #else #define SOL_PLATFORM_WINDOWS_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_PLATFORM_CYGWIN) #if (SOL_PLATFORM_CYGWIN != 0) #define SOL_PLATFORM_CYGWIN_I_ SOL_ON #else #define SOL_PLATFORM_CYGWIN_I_ SOL_ON #endif #elif defined(__CYGWIN__) #define SOL_PLATFORM_CYGWIN_I_ SOL_DEFAULT_ON #else #define SOL_PLATFORM_CYGWIN_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_PLATFORM_APPLE) #if (SOL_PLATFORM_APPLE != 0) #define SOL_PLATFORM_APPLE_I_ SOL_ON #else #define SOL_PLATFORM_APPLE_I_ SOL_OFF #endif #elif defined(__APPLE__) #define SOL_PLATFORM_APPLE_I_ SOL_DEFAULT_ON #else #define SOL_PLATFORM_APPLE_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_PLATFORM_UNIX) #if (SOL_PLATFORM_UNIX != 0) #define SOL_PLATFORM_UNIXLIKE_I_ SOL_ON #else #define SOL_PLATFORM_UNIXLIKE_I_ SOL_OFF #endif #elif defined(__unix__) #define SOL_PLATFORM_UNIXLIKE_I_ SOL_DEFAULT_ON #else #define SOL_PLATFORM_UNIXLIKE_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_PLATFORM_LINUX) #if (SOL_PLATFORM_LINUX != 0) #define SOL_PLATFORM_LINUXLIKE_I_ SOL_ON #else #define SOL_PLATFORM_LINUXLIKE_I_ SOL_OFF #endif #elif defined(__LINUX__) #define SOL_PLATFORM_LINUXLIKE_I_ SOL_DEFAULT_ON #else #define SOL_PLATFORM_LINUXLIKE_I_ SOL_DEFAULT_OFF #endif #define SOL_PLATFORM_APPLE_IPHONE_I_ SOL_OFF #define SOL_PLATFORM_BSDLIKE_I_ SOL_OFF #if defined(SOL_IN_DEBUG_DETECTED) #if (SOL_IN_DEBUG_DETECTED != 0) #define SOL_DEBUG_BUILD_I_ SOL_ON #else #define SOL_DEBUG_BUILD_I_ SOL_OFF #endif #elif !defined(NDEBUG) #if SOL_IS_ON(SOL_COMPILER_VCXX) && defined(_DEBUG) #define SOL_DEBUG_BUILD_I_ SOL_ON #elif (SOL_IS_ON(SOL_COMPILER_CLANG) || SOL_IS_ON(SOL_COMPILER_GCC)) && !defined(__OPTIMIZE__) #define SOL_DEBUG_BUILD_I_ SOL_ON #else #define SOL_DEBUG_BUILD_I_ SOL_OFF #endif #else #define SOL_DEBUG_BUILD_I_ SOL_DEFAULT_OFF #endif // We are in a debug mode of some sort #if defined(SOL_NO_EXCEPTIONS) #if (SOL_NO_EXCEPTIONS != 0) #define SOL_EXCEPTIONS_I_ SOL_OFF #else #define SOL_EXCEPTIONS_I_ SOL_ON #endif #elif SOL_IS_ON(SOL_COMPILER_VCXX) #if !defined(_CPPUNWIND) #define SOL_EXCEPTIONS_I_ SOL_OFF #else #define SOL_EXCEPTIONS_I_ SOL_ON #endif #elif SOL_IS_ON(SOL_COMPILER_CLANG) || SOL_IS_ON(SOL_COMPILER_GCC) #if !defined(__EXCEPTIONS) #define SOL_EXCEPTIONS_I_ SOL_OFF #else #define SOL_EXCEPTIONS_I_ SOL_ON #endif #else #define SOL_EXCEPTIONS_I_ SOL_DEFAULT_ON #endif #if defined(SOL_NO_RTTI) #if (SOL_NO_RTTI != 0) #define SOL_RTTI_I_ SOL_OFF #else #define SOL_RTTI_I_ SOL_ON #endif #elif SOL_IS_ON(SOL_COMPILER_VCXX) #if !defined(_CPPRTTI) #define SOL_RTTI_I_ SOL_OFF #else #define SOL_RTTI_I_ SOL_ON #endif #elif SOL_IS_ON(SOL_COMPILER_CLANG) || SOL_IS_ON(SOL_COMPILER_GCC) #if !defined(__GXX_RTTI) #define SOL_RTTI_I_ SOL_OFF #else #define SOL_RTTI_I_ SOL_ON #endif #else #define SOL_RTTI_I_ SOL_DEFAULT_ON #endif #if defined(SOL_NO_THREAD_LOCAL) #if (SOL_NO_THREAD_LOCAL != 0) #define SOL_USE_THREAD_LOCAL_I_ SOL_OFF #else #define SOL_USE_THREAD_LOCAL_I_ SOL_ON #endif #else #define SOL_USE_THREAD_LOCAL_I_ SOL_DEFAULT_ON #endif // thread_local keyword is bjorked on some platforms #if defined(SOL_ALL_SAFETIES_ON) #if (SOL_ALL_SAFETIES_ON != 0) #define SOL_ALL_SAFETIES_ON_I_ SOL_ON #else #define SOL_ALL_SAFETIES_ON_I_ SOL_OFF #endif #else #define SOL_ALL_SAFETIES_ON_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_SAFE_GETTER) #if (SOL_SAFE_GETTER != 0) #define SOL_SAFE_GETTER_I_ SOL_ON #else #define SOL_SAFE_GETTER_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_SAFE_GETTER_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_SAFE_GETTER_I_ SOL_DEFAULT_ON #else #define SOL_SAFE_GETTER_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_SAFE_USERTYPE) #if (SOL_SAFE_USERTYPE != 0) #define SOL_SAFE_USERTYPE_I_ SOL_ON #else #define SOL_SAFE_USERTYPE_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_SAFE_USERTYPE_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_SAFE_USERTYPE_I_ SOL_DEFAULT_ON #else #define SOL_SAFE_USERTYPE_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_SAFE_REFERENCES) #if (SOL_SAFE_REFERENCES != 0) #define SOL_SAFE_REFERENCES_I_ SOL_ON #else #define SOL_SAFE_REFERENCES_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_SAFE_REFERENCES_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_SAFE_REFERENCES_I_ SOL_DEFAULT_ON #else #define SOL_SAFE_REFERENCES_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_SAFE_FUNCTIONS) #if (SOL_SAFE_FUNCTIONS != 0) #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_ON #else #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_OFF #endif #elif defined (SOL_SAFE_FUNCTION_OBJECTS) #if (SOL_SAFE_FUNCTION_OBJECTS != 0) #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_ON #else #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_DEFAULT_ON #else #define SOL_SAFE_FUNCTION_OBJECTS_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_SAFE_FUNCTION_CALLS) #if (SOL_SAFE_FUNCTION_CALLS != 0) #define SOL_SAFE_FUNCTION_CALLS_I_ SOL_ON #else #define SOL_SAFE_FUNCTION_CALLS_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_SAFE_FUNCTION_CALLS_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_SAFE_FUNCTION_CALLS_I_ SOL_DEFAULT_ON #else #define SOL_SAFE_FUNCTION_CALLS_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_SAFE_PROXIES) #if (SOL_SAFE_PROXIES != 0) #define SOL_SAFE_PROXIES_I_ SOL_ON #else #define SOL_SAFE_PROXIES_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_SAFE_PROXIES_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_SAFE_PROXIES_I_ SOL_DEFAULT_ON #else #define SOL_SAFE_PROXIES_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_SAFE_NUMERICS) #if (SOL_SAFE_NUMERICS != 0) #define SOL_SAFE_NUMERICS_I_ SOL_ON #else #define SOL_SAFE_NUMERICS_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_SAFE_NUMERICS_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_SAFE_NUMERICS_I_ SOL_DEFAULT_ON #else #define SOL_SAFE_NUMERICS_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_ALL_INTEGER_VALUES_FIT) #if (SOL_ALL_INTEGER_VALUES_FIT != 0) #define SOL_ALL_INTEGER_VALUES_FIT_I_ SOL_ON #else #define SOL_ALL_INTEGER_VALUES_FIT_I_ SOL_OFF #endif #elif !SOL_IS_DEFAULT_OFF(SOL_SAFE_NUMERICS) && SOL_IS_OFF(SOL_SAFE_NUMERICS) // if numerics is intentionally turned off, flip this on #define SOL_ALL_INTEGER_VALUES_FIT_I_ SOL_DEFAULT_ON #else // default to off #define SOL_ALL_INTEGER_VALUES_FIT_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_SAFE_STACK_CHECK) #if (SOL_SAFE_STACK_CHECK != 0) #define SOL_SAFE_STACK_CHECK_I_ SOL_ON #else #define SOL_SAFE_STACK_CHECK_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_SAFE_STACK_CHECK_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_SAFE_STACK_CHECK_I_ SOL_DEFAULT_ON #else #define SOL_SAFE_STACK_CHECK_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_NO_CHECK_NUMBER_PRECISION) #if (SOL_NO_CHECK_NUMBER_PRECISION != 0) #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_OFF #else #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_ON #endif #elif defined(SOL_NO_CHECKING_NUMBER_PRECISION) #if (SOL_NO_CHECKING_NUMBER_PRECISION != 0) #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_OFF #else #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_ON #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_ON #elif SOL_IS_ON(SOL_SAFE_NUMERICS) #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_DEFAULT_ON #else #define SOL_NUMBER_PRECISION_CHECKS_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_STRINGS_ARE_NUMBERS) #if (SOL_STRINGS_ARE_NUMBERS != 0) #define SOL_STRINGS_ARE_NUMBERS_I_ SOL_ON #else #define SOL_STRINGS_ARE_NUMBERS_I_ SOL_OFF #endif #else #define SOL_STRINGS_ARE_NUMBERS_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_ENABLE_INTEROP) #if (SOL_ENABLE_INTEROP != 0) #define SOL_USE_INTEROP_I_ SOL_ON #else #define SOL_USE_INTEROP_I_ SOL_OFF #endif #elif defined(SOL_USE_INTEROP) #if (SOL_USE_INTEROP != 0) #define SOL_USE_INTEROP_I_ SOL_ON #else #define SOL_USE_INTEROP_I_ SOL_OFF #endif #else #define SOL_USE_INTEROP_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_NO_NIL) #if (SOL_NO_NIL != 0) #define SOL_NIL_I_ SOL_OFF #else #define SOL_NIL_I_ SOL_ON #endif #elif defined(__MAC_OS_X_VERSION_MAX_ALLOWED) || defined(__OBJC__) || defined(nil) #define SOL_NIL_I_ SOL_DEFAULT_OFF #else #define SOL_NIL_I_ SOL_DEFAULT_ON #endif #if defined(SOL_USERTYPE_TYPE_BINDING_INFO) #if (SOL_USERTYPE_TYPE_BINDING_INFO != 0) #define SOL_USERTYPE_TYPE_BINDING_INFO_I_ SOL_ON #else #define SOL_USERTYPE_TYPE_BINDING_INFO_I_ SOL_OFF #endif #else #define SOL_USERTYPE_TYPE_BINDING_INFO_I_ SOL_DEFAULT_ON #endif // We should generate a my_type.__type table with lots of class information for usertypes #if defined(SOL_AUTOMAGICAL_TYPES_BY_DEFAULT) #if (SOL_AUTOMAGICAL_TYPES_BY_DEFAULT != 0) #define SOL_DEFAULT_AUTOMAGICAL_USERTYPES_I_ SOL_ON #else #define SOL_DEFAULT_AUTOMAGICAL_USERTYPES_I_ SOL_OFF #endif #elif defined(SOL_DEFAULT_AUTOMAGICAL_USERTYPES) #if (SOL_DEFAULT_AUTOMAGICAL_USERTYPES != 0) #define SOL_DEFAULT_AUTOMAGICAL_USERTYPES_I_ SOL_ON #else #define SOL_DEFAULT_AUTOMAGICAL_USERTYPES_I_ SOL_OFF #endif #else #define SOL_DEFAULT_AUTOMAGICAL_USERTYPES_I_ SOL_DEFAULT_ON #endif // make is_automagical on/off by default #if defined(SOL_STD_VARIANT) #if (SOL_STD_VARIANT != 0) #define SOL_STD_VARIANT_I_ SOL_ON #else #define SOL_STD_VARIANT_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_COMPILER_CLANG) && SOL_IS_ON(SOL_PLATFORM_APPLE) #if defined(__has_include) #if __has_include() #define SOL_STD_VARIANT_I_ SOL_DEFAULT_ON #else #define SOL_STD_VARIANT_I_ SOL_DEFAULT_OFF #endif #else #define SOL_STD_VARIANT_I_ SOL_DEFAULT_OFF #endif #else #define SOL_STD_VARIANT_I_ SOL_DEFAULT_ON #endif #endif // make is_automagical on/off by default #if defined(SOL_NOEXCEPT_FUNCTION_TYPE) #if (SOL_NOEXCEPT_FUNCTION_TYPE != 0) #define SOL_USE_NOEXCEPT_FUNCTION_TYPE_I_ SOL_ON #else #define SOL_USE_NOEXCEPT_FUNCTION_TYPE_I_ SOL_OFF #endif #else #if defined(__cpp_noexcept_function_type) #define SOL_USE_NOEXCEPT_FUNCTION_TYPE_I_ SOL_ON #elif SOL_IS_ON(SOL_COMPILER_VCXX) && (defined(_MSVC_LANG) && (_MSVC_LANG < 201403L)) // There is a bug in the VC++ compiler?? // on /std:c++latest under x86 conditions (VS 15.5.2), // compiler errors are tossed for noexcept markings being on function types // that are identical in every other way to their non-noexcept marked types function types... // VS 2019: There is absolutely a bug. #define SOL_USE_NOEXCEPT_FUNCTION_TYPE_I_ SOL_OFF #else #define SOL_USE_NOEXCEPT_FUNCTION_TYPE_I_ SOL_DEFAULT_ON #endif #endif // noexcept is part of a function's type #if defined(SOL_STACK_STRING_OPTIMIZATION_SIZE) && (SOL_STACK_STRING_OPTIMIZATION_SIZE > 0) #define SOL_OPTIMIZATION_STRING_CONVERSION_STACK_SIZE_I_ SOL_STACK_STRING_OPTIMIZATION_SIZE #else #define SOL_OPTIMIZATION_STRING_CONVERSION_STACK_SIZE_I_ 1024 #endif #if defined(SOL_ID_SIZE) && (SOL_ID_SIZE > 0) #define SOL_ID_SIZE_I_ SOL_ID_SIZE #else #define SOL_ID_SIZE_I_ 512 #endif #if defined(LUA_IDSIZE) && (LUA_IDSIZE > 0) #define SOL_FILE_ID_SIZE_I_ LUA_IDSIZE #elif defined(SOL_ID_SIZE) && SOL_ID_SIZE > 0 #define SOL_FILE_ID_SIZE_I_ SOL_FILE_ID_SIZE #else #define SOL_FILE_ID_SIZE_I_ 2048 #endif #if defined(SOL_PRINT_ERRORS) #if (SOL_PRINT_ERRORS != 0) #define SOL_PRINT_ERRORS_I_ SOL_ON #else #define SOL_PRINT_ERRORS_I_ SOL_OFF #endif #else #if SOL_IS_ON(SOL_ALL_SAFETIES_ON) #define SOL_PRINT_ERRORS_I_ SOL_ON #elif SOL_IS_ON(SOL_DEBUG_BUILD) #define SOL_PRINT_ERRORS_I_ SOL_DEFAULT_ON #else #define SOL_PRINT_ERRORS_I_ SOL_OFF #endif #endif #if defined(SOL_DEFAULT_PASS_ON_ERROR) #if (SOL_DEFAULT_PASS_ON_ERROR != 0) #define SOL_DEFAULT_PASS_ON_ERROR_I_ SOL_ON #else #define SOL_DEFAULT_PASS_ON_ERROR_I_ SOL_OFF #endif #else #define SOL_DEFAULT_PASS_ON_ERROR_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_USING_CXX_LUA) #if (SOL_USING_CXX_LUA != 0) #define SOL_USING_CXX_LUA_I_ SOL_ON #else #define SOL_USING_CXX_LUA_I_ SOL_OFF #endif #elif defined(SOL_USE_CXX_LUA) // alternative spelling #if (SOL_USE_CXX_LUA != 0) #define SOL_USING_CXX_LUA_I_ SOL_ON #else #define SOL_USING_CXX_LUA_I_ SOL_OFF #endif #else #define SOL_USING_CXX_LUA_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_USING_CXX_LUAJIT) #if (SOL_USING_CXX_LUAJIT != 0) #define SOL_USING_CXX_LUAJIT_I_ SOL_ON #else #define SOL_USING_CXX_LUAJIT_I_ SOL_OFF #endif #elif defined(SOL_USE_CXX_LUAJIT) #if (SOL_USE_CXX_LUAJIT != 0) #define SOL_USING_CXX_LUAJIT_I_ SOL_ON #else #define SOL_USING_CXX_LUAJIT_I_ SOL_OFF #endif #else #define SOL_USING_CXX_LUAJIT_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_NO_LUA_HPP) #if (SOL_NO_LUA_HPP != 0) #define SOL_USE_LUA_HPP_I_ SOL_OFF #else #define SOL_USE_LUA_HPP_I_ SOL_ON #endif #elif SOL_IS_ON(SOL_USING_CXX_LUA) #define SOL_USE_LUA_HPP_I_ SOL_OFF #elif defined(__has_include) #if __has_include() #define SOL_USE_LUA_HPP_I_ SOL_ON #else #define SOL_USE_LUA_HPP_I_ SOL_OFF #endif #else #define SOL_USE_LUA_HPP_I_ SOL_DEFAULT_ON #endif #if defined(SOL_CONTAINERS_START) #define SOL_CONTAINER_START_INDEX_I_ SOL_CONTAINERS_START #elif defined(SOL_CONTAINERS_START_INDEX) #define SOL_CONTAINER_START_INDEX_I_ SOL_CONTAINERS_START_INDEX #elif defined(SOL_CONTAINER_START_INDEX) #define SOL_CONTAINER_START_INDEX_I_ SOL_CONTAINER_START_INDEX #else #define SOL_CONTAINER_START_INDEX_I_ 1 #endif #if defined (SOL_NO_MEMORY_ALIGNMENT) #if (SOL_NO_MEMORY_ALIGNMENT != 0) #define SOL_ALIGN_MEMORY_I_ SOL_OFF #else #define SOL_ALIGN_MEMORY_I_ SOL_ON #endif #else #define SOL_ALIGN_MEMORY_I_ SOL_DEFAULT_ON #endif #if defined(SOL_USE_BOOST) #if (SOL_USE_BOOST != 0) #define SOL_USE_BOOST_I_ SOL_ON #else #define SOL_USE_BOOST_I_ SOL_OFF #endif #else #define SOL_USE_BOOST_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_USE_UNSAFE_BASE_LOOKUP) #if (SOL_USE_UNSAFE_BASE_LOOKUP != 0) #define SOL_USE_UNSAFE_BASE_LOOKUP_I_ SOL_ON #else #define SOL_USE_UNSAFE_BASE_LOOKUP_I_ SOL_OFF #endif #else #define SOL_USE_UNSAFE_BASE_LOOKUP_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_INSIDE_UNREAL) #if (SOL_INSIDE_UNREAL != 0) #define SOL_INSIDE_UNREAL_ENGINE_I_ SOL_ON #else #define SOL_INSIDE_UNREAL_ENGINE_I_ SOL_OFF #endif #else #if defined(UE_BUILD_DEBUG) || defined(UE_BUILD_DEVELOPMENT) || defined(UE_BUILD_TEST) || defined(UE_BUILD_SHIPPING) || defined(UE_SERVER) #define SOL_INSIDE_UNREAL_ENGINE_I_ SOL_DEFAULT_ON #else #define SOL_INSIDE_UNREAL_ENGINE_I_ SOL_DEFAULT_OFF #endif #endif #if defined(SOL_NO_COMPAT) #if (SOL_NO_COMPAT != 0) #define SOL_USE_COMPATIBILITY_LAYER_I_ SOL_OFF #else #define SOL_USE_COMPATIBILITY_LAYER_I_ SOL_ON #endif #else #define SOL_USE_COMPATIBILITY_LAYER_I_ SOL_DEFAULT_ON #endif #if defined(SOL_GET_FUNCTION_POINTER_UNSAFE) #if (SOL_GET_FUNCTION_POINTER_UNSAFE != 0) #define SOL_GET_FUNCTION_POINTER_UNSAFE_I_ SOL_ON #else #define SOL_GET_FUNCTION_POINTER_UNSAFE_I_ SOL_OFF #endif #else #define SOL_GET_FUNCTION_POINTER_UNSAFE_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_CONTAINER_CHECK_IS_EXHAUSTIVE) #if (SOL_CONTAINER_CHECK_IS_EXHAUSTIVE != 0) #define SOL_CONTAINER_CHECK_IS_EXHAUSTIVE_I_ SOL_ON #else #define SOL_CONTAINER_CHECK_IS_EXHAUSTIVE_I_ SOL_OFF #endif #else #define SOL_CONTAINER_CHECK_IS_EXHAUSTIVE_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_FUNCTION_CALL_VALUE_SEMANTICS) #if (SOL_FUNCTION_CALL_VALUE_SEMANTICS != 0) #define SOL_FUNCTION_CALL_VALUE_SEMANTICS_I_ SOL_ON #else #define SOL_FUNCTION_CALL_VALUE_SEMANTICS_I_ SOL_OFF #endif #else #define SOL_FUNCTION_CALL_VALUE_SEMANTICS_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_MINGW_CCTYPE_IS_POISONED) #if (SOL_MINGW_CCTYPE_IS_POISONED != 0) #define SOL_MINGW_CCTYPE_IS_POISONED_I_ SOL_ON #else #define SOL_MINGW_CCTYPE_IS_POISONED_I_ SOL_OFF #endif #elif SOL_IS_ON(SOL_COMPILER_MINGW) && defined(__GNUC__) && (__GNUC__ < 6) // MinGW is off its rocker in some places... #define SOL_MINGW_CCTYPE_IS_POISONED_I_ SOL_DEFAULT_ON #else #define SOL_MINGW_CCTYPE_IS_POISONED_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_CHAR8_T) #if (SOL_CHAR8_T != 0) #define SOL_CHAR8_T_I_ SOL_ON #else #define SOL_CHAR8_T_I_ SOL_OFF #endif #else #if defined(__cpp_char8_t) #define SOL_CHAR8_T_I_ SOL_DEFAULT_ON #else #define SOL_CHAR8_T_I_ SOL_DEFAULT_OFF #endif #endif #if SOL_IS_ON(SOL_USE_BOOST) #include #if BOOST_VERSION >= 107500 // Since Boost 1.75.0 boost::none is constexpr #define SOL_BOOST_NONE_CONSTEXPR_I_ constexpr #else #define SOL_BOOST_NONE_CONSTEXPR_I_ const #endif // BOOST_VERSION #else // assume boost isn't using a garbage version #define SOL_BOOST_NONE_CONSTEXPR_I_ constexpr #endif #if defined(SOL2_CI) #if (SOL2_CI != 0) #define SOL2_CI_I_ SOL_ON #else #define SOL2_CI_I_ SOL_OFF #endif #else #define SOL2_CI_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_ASSERT) #define SOL_USER_ASSERT_I_ SOL_ON #else #define SOL_USER_ASSERT_I_ SOL_DEFAULT_OFF #endif #if defined(SOL_ASSERT_MSG) #define SOL_USER_ASSERT_MSG_I_ SOL_ON #else #define SOL_USER_ASSERT_MSG_I_ SOL_DEFAULT_OFF #endif // beginning of sol/prologue.hpp #if defined(SOL_PROLOGUE_I_) #error "[sol2] Library Prologue was already included in translation unit and not properly ended with an epilogue." #endif #define SOL_PROLOGUE_I_ 1 #if SOL_IS_ON(SOL_BUILD_CXX_MODE) #define _FWD(...) static_cast( __VA_ARGS__ ) #if SOL_IS_ON(SOL_COMPILER_GCC) || SOL_IS_ON(SOL_COMPILER_CLANG) #define _MOVE(...) static_cast<__typeof( __VA_ARGS__ )&&>( __VA_ARGS__ ) #else #include #define _MOVE(...) static_cast<::std::remove_reference_t<( __VA_ARGS__ )>&&>( __VA_OPT__(,) ) #endif #endif // end of sol/prologue.hpp // beginning of sol/epilogue.hpp #if !defined(SOL_PROLOGUE_I_) #error "[sol2] Library Prologue is missing from this translation unit." #else #undef SOL_PROLOGUE_I_ #endif #if SOL_IS_ON(SOL_BUILD_CXX_MODE) #undef _FWD #undef _MOVE #endif // end of sol/epilogue.hpp // beginning of sol/detail/build_version.hpp #if defined(SOL_DLL) #if (SOL_DLL != 0) #define SOL_DLL_I_ SOL_ON #else #define SOL_DLL_I_ SOL_OFF #endif #elif SOL_IS_ON(SOL_COMPILER_VCXX) && (defined(DLL_) || defined(_DLL)) #define SOL_DLL_I_ SOL_DEFAULT_ON #else #define SOL_DLL_I_ SOL_DEFAULT_OFF #endif // DLL definition #if defined(SOL_HEADER_ONLY) #if (SOL_HEADER_ONLY != 0) #define SOL_HEADER_ONLY_I_ SOL_ON #else #define SOL_HEADER_ONLY_I_ SOL_OFF #endif #else #define SOL_HEADER_ONLY_I_ SOL_DEFAULT_OFF #endif // Header only library #if defined(SOL_BUILD) #if (SOL_BUILD != 0) #define SOL_BUILD_I_ SOL_ON #else #define SOL_BUILD_I_ SOL_OFF #endif #elif SOL_IS_ON(SOL_HEADER_ONLY) #define SOL_BUILD_I_ SOL_DEFAULT_OFF #else #define SOL_BUILD_I_ SOL_DEFAULT_ON #endif #if defined(SOL_UNITY_BUILD) #if (SOL_UNITY_BUILD != 0) #define SOL_UNITY_BUILD_I_ SOL_ON #else #define SOL_UNITY_BUILD_I_ SOL_OFF #endif #else #define SOL_UNITY_BUILD_I_ SOL_DEFAULT_OFF #endif // Header only library #if defined(SOL_C_FUNCTION_LINKAGE) #define SOL_C_FUNCTION_LINKAGE_I_ SOL_C_FUNCTION_LINKAGE #else #if SOL_IS_ON(SOL_BUILD_CXX_MODE) // C++ #define SOL_C_FUNCTION_LINKAGE_I_ extern "C" #else // normal #define SOL_C_FUNCTION_LINKAGE_I_ #endif // C++ or not #endif // Linkage specification for C functions #if defined(SOL_API_LINKAGE) #define SOL_API_LINKAGE_I_ SOL_API_LINKAGE #else #if SOL_IS_ON(SOL_DLL) #if SOL_IS_ON(SOL_COMPILER_VCXX) || SOL_IS_ON(SOL_PLATFORM_WINDOWS) || SOL_IS_ON(SOL_PLATFORM_CYGWIN) // MSVC Compiler; or, Windows, or Cygwin platforms #if SOL_IS_ON(SOL_BUILD) // Building the library #if SOL_IS_ON(SOL_COMPILER_GCC) // Using GCC #define SOL_API_LINKAGE_I_ __attribute__((dllexport)) #else // Using Clang, MSVC, etc... #define SOL_API_LINKAGE_I_ __declspec(dllexport) #endif #else #if SOL_IS_ON(SOL_COMPILER_GCC) #define SOL_API_LINKAGE_I_ __attribute__((dllimport)) #else #define SOL_API_LINKAGE_I_ __declspec(dllimport) #endif #endif #else // extern if building normally on non-MSVC #define SOL_API_LINKAGE_I_ extern #endif #elif SOL_IS_ON(SOL_UNITY_BUILD) // Built-in library, like how stb typical works #if SOL_IS_ON(SOL_HEADER_ONLY) // Header only, so functions are defined "inline" #define SOL_API_LINKAGE_I_ inline #else // Not header only, so seperately compiled files #define SOL_API_LINKAGE_I_ extern #endif #else // Normal static library #if SOL_IS_ON(SOL_BUILD_CXX_MODE) #define SOL_API_LINKAGE_I_ #else #define SOL_API_LINKAGE_I_ extern #endif #endif // DLL or not #endif // Build definitions #if defined(SOL_PUBLIC_FUNC_DECL) #define SOL_PUBLIC_FUNC_DECL_I_ SOL_PUBLIC_FUNC_DECL #else #define SOL_PUBLIC_FUNC_DECL_I_ SOL_API_LINKAGE_I_ #endif #if defined(SOL_INTERNAL_FUNC_DECL_) #define SOL_INTERNAL_FUNC_DECL_I_ SOL_INTERNAL_FUNC_DECL_ #else #define SOL_INTERNAL_FUNC_DECL_I_ SOL_API_LINKAGE_I_ #endif #if defined(SOL_PUBLIC_FUNC_DEF) #define SOL_PUBLIC_FUNC_DEF_I_ SOL_PUBLIC_FUNC_DEF #else #define SOL_PUBLIC_FUNC_DEF_I_ SOL_API_LINKAGE_I_ #endif #if defined(SOL_INTERNAL_FUNC_DEF) #define SOL_INTERNAL_FUNC_DEF_I_ SOL_INTERNAL_FUNC_DEF #else #define SOL_INTERNAL_FUNC_DEF_I_ SOL_API_LINKAGE_I_ #endif #if defined(SOL_FUNC_DECL) #define SOL_FUNC_DECL_I_ SOL_FUNC_DECL #elif SOL_IS_ON(SOL_HEADER_ONLY) #define SOL_FUNC_DECL_I_ #elif SOL_IS_ON(SOL_DLL) #if SOL_IS_ON(SOL_COMPILER_VCXX) #if SOL_IS_ON(SOL_BUILD) #define SOL_FUNC_DECL_I_ extern __declspec(dllexport) #else #define SOL_FUNC_DECL_I_ extern __declspec(dllimport) #endif #elif SOL_IS_ON(SOL_COMPILER_GCC) || SOL_IS_ON(SOL_COMPILER_CLANG) #define SOL_FUNC_DECL_I_ extern __attribute__((visibility("default"))) #else #define SOL_FUNC_DECL_I_ extern #endif #endif #if defined(SOL_FUNC_DEFN) #define SOL_FUNC_DEFN_I_ SOL_FUNC_DEFN #elif SOL_IS_ON(SOL_HEADER_ONLY) #define SOL_FUNC_DEFN_I_ inline #elif SOL_IS_ON(SOL_DLL) #if SOL_IS_ON(SOL_COMPILER_VCXX) #if SOL_IS_ON(SOL_BUILD) #define SOL_FUNC_DEFN_I_ __declspec(dllexport) #else #define SOL_FUNC_DEFN_I_ __declspec(dllimport) #endif #elif SOL_IS_ON(SOL_COMPILER_GCC) || SOL_IS_ON(SOL_COMPILER_CLANG) #define SOL_FUNC_DEFN_I_ __attribute__((visibility("default"))) #else #define SOL_FUNC_DEFN_I_ #endif #endif #if defined(SOL_HIDDEN_FUNC_DECL) #define SOL_HIDDEN_FUNC_DECL_I_ SOL_HIDDEN_FUNC_DECL #elif SOL_IS_ON(SOL_HEADER_ONLY) #define SOL_HIDDEN_FUNC_DECL_I_ #elif SOL_IS_ON(SOL_DLL) #if SOL_IS_ON(SOL_COMPILER_VCXX) #if SOL_IS_ON(SOL_BUILD) #define SOL_HIDDEN_FUNC_DECL_I_ extern __declspec(dllexport) #else #define SOL_HIDDEN_FUNC_DECL_I_ extern __declspec(dllimport) #endif #elif SOL_IS_ON(SOL_COMPILER_GCC) || SOL_IS_ON(SOL_COMPILER_CLANG) #define SOL_HIDDEN_FUNC_DECL_I_ extern __attribute__((visibility("default"))) #else #define SOL_HIDDEN_FUNC_DECL_I_ extern #endif #endif #if defined(SOL_HIDDEN_FUNC_DEFN) #define SOL_HIDDEN_FUNC_DEFN_I_ SOL_HIDDEN_FUNC_DEFN #elif SOL_IS_ON(SOL_HEADER_ONLY) #define SOL_HIDDEN_FUNC_DEFN_I_ inline #elif SOL_IS_ON(SOL_DLL) #if SOL_IS_ON(SOL_COMPILER_VCXX) #if SOL_IS_ON(SOL_BUILD) #define SOL_HIDDEN_FUNC_DEFN_I_ #else #define SOL_HIDDEN_FUNC_DEFN_I_ #endif #elif SOL_IS_ON(SOL_COMPILER_GCC) || SOL_IS_ON(SOL_COMPILER_CLANG) #define SOL_HIDDEN_FUNC_DEFN_I_ __attribute__((visibility("hidden"))) #else #define SOL_HIDDEN_FUNC_DEFN_I_ #endif #endif // end of sol/detail/build_version.hpp // end of sol/version.hpp #if SOL_IS_ON(SOL_INSIDE_UNREAL_ENGINE) #ifdef check #pragma push_macro("check") #undef check #endif #endif // Unreal Engine 4 Bullshit #if SOL_IS_ON(SOL_COMPILER_GCC) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wconversion" #if __GNUC__ > 6 #pragma GCC diagnostic ignored "-Wnoexcept-type" #endif #elif SOL_IS_ON(SOL_COMPILER_CLANG) #elif SOL_IS_ON(SOL_COMPILER_VCXX) #pragma warning(push) #pragma warning(disable : 4505) // unreferenced local function has been removed GEE THANKS #endif // clang++ vs. g++ vs. VC++ // beginning of sol/forward.hpp #ifndef SOL_FORWARD_HPP #define SOL_FORWARD_HPP #include #include #include #if SOL_IS_ON(SOL_USING_CXX_LUA) || SOL_IS_ON(SOL_USING_CXX_LUAJIT) struct lua_State; #else extern "C" { struct lua_State; } #endif // C++ Mangling for Lua vs. Not namespace sol { enum class type; class stateless_reference; template class basic_reference; using reference = basic_reference; using main_reference = basic_reference; class stateless_stack_reference; class stack_reference; template class basic_bytecode; struct lua_value; struct proxy_base_tag; template struct proxy_base; template struct table_proxy; template class basic_table_core; template using table_core = basic_table_core; template using main_table_core = basic_table_core; template using stack_table_core = basic_table_core; template using basic_table = basic_table_core; using table = table_core; using global_table = table_core; using main_table = main_table_core; using main_global_table = main_table_core; using stack_table = stack_table_core; using stack_global_table = stack_table_core; template struct basic_lua_table; using lua_table = basic_lua_table; using stack_lua_table = basic_lua_table; template class basic_usertype; template using usertype = basic_usertype; template using stack_usertype = basic_usertype; template class basic_metatable; using metatable = basic_metatable; using stack_metatable = basic_metatable; template struct basic_environment; using environment = basic_environment; using main_environment = basic_environment; using stack_environment = basic_environment; template class basic_function; template class basic_protected_function; using unsafe_function = basic_function; using safe_function = basic_protected_function; using main_unsafe_function = basic_function; using main_safe_function = basic_protected_function; using stack_unsafe_function = basic_function; using stack_safe_function = basic_protected_function; using stack_aligned_unsafe_function = basic_function; using stack_aligned_safe_function = basic_protected_function; using protected_function = safe_function; using main_protected_function = main_safe_function; using stack_protected_function = stack_safe_function; using stack_aligned_protected_function = stack_aligned_safe_function; #if SOL_IS_ON(SOL_SAFE_FUNCTION_OBJECTS) using function = protected_function; using main_function = main_protected_function; using stack_function = stack_protected_function; using stack_aligned_function = stack_aligned_safe_function; #else using function = unsafe_function; using main_function = main_unsafe_function; using stack_function = stack_unsafe_function; using stack_aligned_function = stack_aligned_unsafe_function; #endif using stack_aligned_stack_handler_function = basic_protected_function; struct unsafe_function_result; struct protected_function_result; using safe_function_result = protected_function_result; #if SOL_IS_ON(SOL_SAFE_FUNCTION_OBJECTS) using function_result = safe_function_result; #else using function_result = unsafe_function_result; #endif template class basic_object_base; template class basic_object; template class basic_userdata; template class basic_lightuserdata; template class basic_coroutine; template class basic_packaged_coroutine; template class basic_thread; using object = basic_object; using userdata = basic_userdata; using lightuserdata = basic_lightuserdata; using thread = basic_thread; using coroutine = basic_coroutine; using packaged_coroutine = basic_packaged_coroutine; using main_object = basic_object; using main_userdata = basic_userdata; using main_lightuserdata = basic_lightuserdata; using main_coroutine = basic_coroutine; using stack_object = basic_object; using stack_userdata = basic_userdata; using stack_lightuserdata = basic_lightuserdata; using stack_thread = basic_thread; using stack_coroutine = basic_coroutine; struct stack_proxy_base; struct stack_proxy; struct variadic_args; struct variadic_results; struct stack_count; struct this_state; struct this_main_state; struct this_environment; class state_view; class state; template struct as_table_t; template struct as_container_t; template struct nested; template struct light; template struct user; template struct as_args_t; template struct protect_t; template struct policy_wrapper; template struct usertype_traits; template struct unique_usertype_traits; template struct types { typedef std::make_index_sequence indices; static constexpr std::size_t size() { return sizeof...(Args); } }; template struct derive : std::false_type { typedef types<> type; }; template struct base : std::false_type { typedef types<> type; }; template struct weak_derive { static bool value; }; template bool weak_derive::value = false; namespace stack { struct record; } #if SOL_IS_OFF(SOL_USE_BOOST) template class optional; template class optional; #endif using check_handler_type = int(lua_State*, int, type, type, const char*); } // namespace sol #define SOL_BASE_CLASSES(T, ...) \ namespace sol { \ template <> \ struct base : std::true_type { \ typedef ::sol::types<__VA_ARGS__> type; \ }; \ } \ static_assert(true, "") #define SOL_DERIVED_CLASSES(T, ...) \ namespace sol { \ template <> \ struct derive : std::true_type { \ typedef ::sol::types<__VA_ARGS__> type; \ }; \ } \ static_assert(true, "") #endif // SOL_FORWARD_HPP // end of sol/forward.hpp // beginning of sol/forward_detail.hpp #ifndef SOL_FORWARD_DETAIL_HPP #define SOL_FORWARD_DETAIL_HPP // beginning of sol/traits.hpp // beginning of sol/tuple.hpp // beginning of sol/base_traits.hpp #include namespace sol { namespace detail { struct unchecked_t { }; const unchecked_t unchecked = unchecked_t {}; } // namespace detail namespace meta { using sfinae_yes_t = std::true_type; using sfinae_no_t = std::false_type; template using void_t = void; template using unqualified = std::remove_cv>; template using unqualified_t = typename unqualified::type; namespace meta_detail { template struct unqualified_non_alias : unqualified { }; template