├── .clang-format ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── CMakeLists.txt ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── details.png ├── errors-fixed.png └── errors.png ├── icon ├── LICENSE ├── icon.ico ├── icon.rc ├── icon.svg └── rasterize.ps1 ├── sign_target.cmake ├── src ├── APILayer.hpp ├── APILayerDetails.cpp ├── APILayerStore.hpp ├── CMakeLists.txt ├── Config.in.hpp ├── ConstexprString.hpp ├── GUI.cpp ├── GUI.hpp ├── GetActiveRuntimePath.hpp ├── LayerRules.cpp ├── LayerRules.hpp ├── Linter.cpp ├── Linter.hpp ├── Runtime.hpp ├── SaveReport.cpp ├── SaveReport.hpp ├── StringTemplateParameter.hpp ├── linters │ ├── BadInstallationLinter.cpp │ ├── DuplicatesLinter.cpp │ ├── OrderingLinter.cpp │ └── windows │ │ ├── NotADWORDLinter.cpp │ │ ├── OpenXRToolkitLinter.cpp │ │ ├── OutdatedOpenKneeboardLinter.cpp │ │ ├── ProgramFilesLinter.cpp │ │ ├── UnsignedDllLinter.cpp │ │ └── XRNeckSaferLinter.cpp ├── main.cpp ├── manifest.xml ├── portability │ └── filesystem.hpp ├── std23 │ └── ranges.hpp ├── stubs │ ├── APILayerStore.cpp │ └── PlatformGUI.cpp ├── version.in.rc └── windows │ ├── CheckForUpdates.cpp │ ├── CheckForUpdates.hpp │ ├── GetActiveRuntimePath.cpp │ ├── GetKnownFolderPath.hpp │ ├── PlatformGUI.cpp │ ├── SaveReport_wWinMain.cpp │ ├── WindowsAPILayerStore.cpp │ ├── WindowsAPILayerStore.hpp │ └── wWinMain.cpp ├── third-party ├── CMakeLists.txt └── vicius.cmake └── vcpkg.json /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | AlignAfterOpenBracket: AlwaysBreak 3 | AlignConsecutiveDeclarations: 'None' 4 | AlignOperands: false 5 | AlignTrailingComments: false 6 | AllowAllParametersOfDeclarationOnNextLine: false 7 | AllowShortBlocksOnASingleLine: 'Never' 8 | AllowShortFunctionsOnASingleLine: 'None' 9 | AllowShortIfStatementsOnASingleLine: 'false' 10 | AllowShortLoopsOnASingleLine: false 11 | BinPackArguments: false 12 | BinPackParameters: false 13 | BreakBeforeBinaryOperators: true 14 | ColumnLimit: 80 15 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 16 | ConstructorInitializerIndentWidth: 2 17 | ContinuationIndentWidth: 2 18 | DeriveLineEnding: false 19 | DerivePointerAlignment: false 20 | IncludeBlocks: Regroup 21 | IncludeCategories: 22 | - Regex: '^$' 23 | Priority: 30 24 | - Regex: '^' 25 | Priority: 30 26 | - Regex: '^' 27 | Priority: 30 28 | SortPriority: 31 29 | - Regex: '^' 33 | Priority: 33 34 | - Regex: '^$' # Usually interop headers, e.g. windows.data.pdf.interop 39 | Priority: 37 40 | - Regex: '^<[a-z_]+/.+\.h(pp)?>$' 41 | Priority: 999 42 | SortPriority: 41 43 | - Regex: '^<[a-z_/]+>$' 44 | Priority: 42 45 | CaseSensitive: true 46 | - Regex: '^<[a-z0-9_]+\.h>$' 47 | Priority: 43 48 | IndentWidth: 2 49 | LineEnding: 'LF' 50 | MaxEmptyLinesToKeep: 1 51 | PointerAlignment: Left 52 | ReflowComments: true 53 | SpaceBeforeCpp11BracedList: true 54 | SpaceBeforeAssignmentOperators: true 55 | SpaceBeforeRangeBasedForLoopColon: false 56 | SpacesBeforeTrailingComments: 0 57 | Standard: c++20 58 | TabWidth: 2 59 | UseTab: Never 60 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | 2 | on: 3 | workflow_call: 4 | outputs: 5 | version: 6 | description: The built version 7 | value: ${{jobs.build.outputs.version}} 8 | jobs: 9 | build: 10 | name: build/${{matrix.os-arch}}/${{matrix.build-type}} 11 | runs-on: ${{matrix.runs-on}} 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | submodules: true 16 | - name: Make build directory 17 | run: cmake -E make_directory build 18 | - uses: actions/github-script@v7 19 | with: 20 | script: | 21 | core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); 22 | core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); 23 | - name: "Initialize vcpkg" 24 | working-directory: build 25 | run: ../third-party/vcpkg/bootstrap-vcpkg.bat 26 | - name: "Install dependencies with vcpkg" 27 | working-directory: build 28 | run: | 29 | ../third-party/vcpkg/vcpkg.exe install ` 30 | --binarysource="clear;x-gha,readwrite" ` 31 | --x-install-root=$(Get-Location)/vcpkg_installed ` 32 | --triplet ${{matrix.vcpkg-arch}} 33 | - name: Configure 34 | working-directory: build 35 | id: configure 36 | run: | 37 | $args = @() 38 | 39 | cmake .. ` 40 | -A ${{matrix.cmake-arch}} ` 41 | -DVCPKG_TARGET_TRIPLET=${{matrix.vcpkg-arch}} ` 42 | @args 43 | 44 | Add-Content $Env:GITHUB_OUTPUT "version=$(Get-Content version.txt)" 45 | shell: pwsh 46 | - name: Compile 47 | working-directory: build 48 | run: cmake --build . --parallel --config ${{matrix.build-type}} --verbose 49 | - name: Install 50 | working-directory: build 51 | run: | 52 | cmake --install . --config ${{matrix.build-type}} --prefix ${{runner.temp}}/install 53 | cmake --install . --config ${{matrix.build-type}} --prefix ${{runner.temp}}/install-symbols --component DebugSymbols 54 | - name: Upload Executable Artifacts 55 | uses: actions/upload-artifact@v4 56 | if: ${{ matrix.build-type != 'Debug' && matrix.os-arch == 'Win64' }} 57 | with: 58 | name: "OpenXR-API-Layers-GUI-v${{steps.configure.outputs.version}}" 59 | path: ${{runner.temp}}/install 60 | - name: Upload Debug Symbol Artifacts 61 | uses: actions/upload-artifact@v4 62 | if: ${{ matrix.build-type != 'Debug' && matrix.os-arch == 'Win64' }} 63 | with: 64 | name: "OpenXR-API-Layers-GUI-v${{steps.configure.outputs.version}}-DebugSymbols" 65 | path: ${{runner.temp}}/install-symbols 66 | outputs: 67 | version: ${{steps.configure.outputs.version}} 68 | strategy: 69 | fail-fast: false 70 | matrix: 71 | os-arch: [Win64] 72 | build-type: [RelWithDebInfo, Debug] 73 | include: 74 | - os-arch: Win64 75 | runs-on: windows-latest 76 | cmake-arch: x64 77 | vcpkg-arch: x64-windows-static -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: Build 6 | uses: ./.github/workflows/build.yml 7 | secrets: inherit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*build*/ 2 | .DS_Store 3 | docs/_site 4 | *.swp 5 | vcpkg_installed 6 | .idea/ 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third-party/vcpkg"] 2 | path = third-party/vcpkg 3 | url = https://github.com/microsoft/vcpkg.git 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "ios": "cpp", 4 | "string": "cpp", 5 | "stdexcept": "cpp", 6 | "format": "cpp", 7 | "istream": "cpp", 8 | "memory": "cpp", 9 | "type_traits": "cpp", 10 | "algorithm": "cpp", 11 | "cmath": "cpp", 12 | "any": "cpp", 13 | "array": "cpp", 14 | "atomic": "cpp", 15 | "bit": "cpp", 16 | "cctype": "cpp", 17 | "charconv": "cpp", 18 | "chrono": "cpp", 19 | "clocale": "cpp", 20 | "codecvt": "cpp", 21 | "compare": "cpp", 22 | "concepts": "cpp", 23 | "cstddef": "cpp", 24 | "cstdint": "cpp", 25 | "cstdio": "cpp", 26 | "cstdlib": "cpp", 27 | "cstring": "cpp", 28 | "ctime": "cpp", 29 | "cwchar": "cpp", 30 | "exception": "cpp", 31 | "forward_list": "cpp", 32 | "list": "cpp", 33 | "set": "cpp", 34 | "unordered_map": "cpp", 35 | "vector": "cpp", 36 | "filesystem": "cpp", 37 | "fstream": "cpp", 38 | "functional": "cpp", 39 | "initializer_list": "cpp", 40 | "iomanip": "cpp", 41 | "iosfwd": "cpp", 42 | "iostream": "cpp", 43 | "iterator": "cpp", 44 | "limits": "cpp", 45 | "locale": "cpp", 46 | "map": "cpp", 47 | "mutex": "cpp", 48 | "new": "cpp", 49 | "numeric": "cpp", 50 | "optional": "cpp", 51 | "ostream": "cpp", 52 | "ranges": "cpp", 53 | "ratio": "cpp", 54 | "span": "cpp", 55 | "sstream": "cpp", 56 | "stop_token": "cpp", 57 | "streambuf": "cpp", 58 | "system_error": "cpp", 59 | "thread": "cpp", 60 | "tuple": "cpp", 61 | "typeinfo": "cpp", 62 | "utility": "cpp", 63 | "valarray": "cpp", 64 | "xfacet": "cpp", 65 | "xhash": "cpp", 66 | "xiosbase": "cpp", 67 | "xlocale": "cpp", 68 | "xlocbuf": "cpp", 69 | "xlocinfo": "cpp", 70 | "xlocmes": "cpp", 71 | "xlocmon": "cpp", 72 | "xlocnum": "cpp", 73 | "xloctime": "cpp", 74 | "xmemory": "cpp", 75 | "xstring": "cpp", 76 | "xtr1common": "cpp", 77 | "xtree": "cpp", 78 | "xutility": "cpp", 79 | "__bit_reference": "cpp", 80 | "__config": "cpp", 81 | "__debug": "cpp", 82 | "__errc": "cpp", 83 | "__hash_table": "cpp", 84 | "__locale": "cpp", 85 | "__mutex_base": "cpp", 86 | "__node_handle": "cpp", 87 | "__split_buffer": "cpp", 88 | "__threading_support": "cpp", 89 | "__tree": "cpp", 90 | "__verbose_abort": "cpp", 91 | "bitset": "cpp", 92 | "complex": "cpp", 93 | "cstdarg": "cpp", 94 | "cwctype": "cpp", 95 | "deque": "cpp", 96 | "queue": "cpp", 97 | "stack": "cpp", 98 | "string_view": "cpp", 99 | "variant": "cpp", 100 | "__std_stream": "cpp" 101 | }, 102 | "editor.formatOnSave": true, 103 | "cmake.configureOnOpen": true, 104 | "editor.tabSize": 2 105 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Fred Emmott 2 | # SPDX-License-Identifier: ISC 3 | 4 | cmake_minimum_required(VERSION 3.10...3.30 FATAL_ERROR) 5 | 6 | # Enable CMAKE_MSVC_RUNTIME_LIBRARY variable 7 | cmake_policy(SET CMP0091 NEW) 8 | 9 | option(ENABLE_ASAN "Enable Address Sanitizer" OFF) 10 | 11 | if(ENABLE_ASAN) 12 | list( 13 | APPEND 14 | VCPKG_CXX_FLAGS 15 | "$,MSVC>,/,->fsanitize=address" 16 | ) 17 | endif() 18 | 19 | set(X_VCPKG_APPLOCAL_DEPS_INSTALL ON) 20 | 21 | if("${VCPKG_TARGET_TRIPLET}" MATCHES "-static$") 22 | # https://github.com/microsoft/WindowsAppSDK/blob/main/docs/Coding-Guidelines/HybridCRT.md 23 | set( 24 | CMAKE_MSVC_RUNTIME_LIBRARY 25 | "MultiThreaded$<$:Debug>" 26 | ) 27 | add_link_options( 28 | "/DEFAULTLIB:ucrt$<$:d>.lib" # include the dynamic UCRT 29 | "/NODEFAULTLIB:libucrt$<$:d>.lib" # remove the static UCRT 30 | ) 31 | endif() 32 | 33 | set( 34 | CMAKE_TOOLCHAIN_FILE 35 | "${CMAKE_CURRENT_SOURCE_DIR}/third-party/vcpkg/scripts/buildsystems/vcpkg.cmake" 36 | CACHE STRING "Vcpkg toolchain file" 37 | ) 38 | 39 | set(CMAKE_CXX_STANDARD 20) 40 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 41 | set(CMAKE_CXX_EXTENSIONS OFF) 42 | 43 | if(DEFINED ENV{GITHUB_RUN_NUMBER}) 44 | set(VERSION_BUILD $ENV{GITHUB_RUN_NUMBER}) 45 | else() 46 | set(VERSION_BUILD 0) 47 | endif() 48 | 49 | 50 | set( 51 | BUILD_TARGET_ID 52 | "${BUILD_TARGET_ID}" 53 | CACHE 54 | STRING 55 | "Identifier for the current build, e.g. OS, architecture, HKLM vs HKCU" 56 | ) 57 | 58 | set(BUILD_VERSION_STRING "2025.03.08.${VERSION_BUILD}") 59 | project(OpenXR-Layers-GUI VERSION ${BUILD_VERSION_STRING} LANGUAGES CXX C) 60 | 61 | message(STATUS "C++ compiler: ${CMAKE_CXX_COMPILER_ID}") 62 | 63 | # Handy for CI 64 | file(WRITE "${CMAKE_BINARY_DIR}/version.txt" "${BUILD_VERSION_STRING}") 65 | 66 | option(USE_EMOJI "Use emoji for status symbols" ON) 67 | 68 | 69 | message(STATUS "Building OpenXR-Layers-GUI v${CMAKE_PROJECT_VERSION}") 70 | 71 | include(sign_target.cmake) 72 | 73 | if(ENABLE_ASAN) 74 | list( 75 | APPEND 76 | VCPKG_CXX_FLAGS 77 | "$,MSVC>,/,->fsanitize=address" 78 | ) 79 | endif() 80 | 81 | add_subdirectory("third-party") 82 | add_subdirectory("src") 83 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributors Guide 2 | 3 | ## Requirements 4 | 5 | - a C++20 compiler, such as Visual Studio 2022 or a recent Clang version 6 | - CMake 7 | - git 8 | 9 | ## Note for Windows developers 10 | 11 | While in general this is a terrible idea, for this project, you may want to run your IDE/debugger elevated, as the program requires elevation. 12 | 13 | This is because the API layer community in general has decided to install their layers into `HKEY_LOCAL_MACHINE`, rather than `HKEY_CURRENT_USER`, which requires elevation to write to. Individual API layers can't really migrate to HKCU, because layer order is only defined (ish) within one registry hive, and layer ordering matters. 14 | 15 | ## Building 16 | 17 | 1. Clone the repository 18 | 2. `git submodule update --init` (needed once) 19 | 3. Open with Visual Studio Code and use the CMake extensions, or configure and build with separate `cmake` command 20 | 21 | You may want to set the `ENABLE_ASAN` CMake option to enable Address Sanitizer, which will automatically detect many common memory errors at runtime. 22 | 23 | See [the GitHub Actions configuration](.github/workflows/ci.yml) for a full example. 24 | 25 | ## General Guidelines 26 | 27 | - `clang-format` should be used ('format document' in Visual Studio Code will automatically do this) 28 | - OS-specific functions should be avoided if possible 29 | - Where unavoidable, they *must* be isolated to `*_windows*` files (e.g. `src/APILayers_windows.cpp`) or in a `windows` subfolder (e.g. `src/linters/windows/ProgramFilesLinter.cpp`). 30 | 31 | ## Adding Additional Warnings 32 | 33 | "Warnings" in the UI are instances of `LintError`, which are generated by a `Linter`. A `LintError` *can* be a `FixableLintError`, which has a `Fix()` method. 34 | 35 | - `LintError` can be instantiated directly 36 | - `FixableLintError` **must** be extended 37 | - `OrderingLintError` is a final subclass of `FixableLintError` for moving a layer to above or below another layer 38 | - `InvalidLayerLintError` is a final subclass of `FixableLintError` that removes completely undesirable layers, such as registry keys referring to missing files. 39 | 40 | ### Layer Ordering Warnings 41 | 42 | Additional ordering checks can be added by describing the layer in [`LayerRules.cpp`](src/LayerRules.cpp); the fields 43 | are documented in [`LayerRules.hpp`](src/LayerRules.hpp). 44 | 45 | ### Other Warnings 46 | 47 | Add a new `Linter` subclass (using existing linters as an example), and additional subclasses of `LintError` or `FixableLintError` if needed. If your new error is not automatically fixable, you probably just want to use `LintError`. 48 | 49 | Layer implementation files contain a single static instance of the linter, e.g.: 50 | 51 | ```C++ 52 | class OrderingLinter final : public Linter { 53 | // ... 54 | }; 55 | 56 | static OrderingLinter gInstance; 57 | ``` 58 | 59 | The static instance is needed as the constructor registers the new linter 60 | with the linter system. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The majority of this project is licensed under the MIT license (below). 2 | 3 | The application icon and images in the 'icons' subfolder may not be reused or distributed outside of this product. 4 | 5 | A separate license for these images may be purchased from https://glyphicons.com 6 | 7 | ----- 8 | 9 | Copyright 2023 Fred Emmott 10 | 11 | Permission to use, copy, modify, and/or distribute this software for any purpose 12 | with or without fee is hereby granted, provided that the above copyright notice 13 | and this permission notice appear in all copies. 14 | 15 | THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 16 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 17 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 18 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 19 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 20 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 21 | THIS SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenXR API Layers GUI 2 | 3 | This is a tool for: 4 | - detecting and fixing common problems with OpenXR API layers 5 | - viewing information about OpenXR API layers 6 | - enabling, disabling, adding, or removing OpenXR API layers 7 | - re-ordering OpenXR API layers 8 | 9 | There is no general way to detect errors with OpenXR API layers, so this tool is only able to detect problems that it knows about. If it doesn't show any errors, that just means that you have no particularly common problems, not necessarily that everything is set up correctly. 10 | 11 | ## Getting Started 12 | 13 | 1. Download [the latest version](https://github.com/fredemmott/OpenXR-API-Layers-GUI/releases/latest) 14 | 2. Extract it somewhere handy 15 | 3. Run `OpenXR-API-Layers-GUI.exe` :) 16 | 17 | While this project is designed to be easily portable, it currently only supports Windows - [contributions are very welcome :)](CONTRIBUTING.md). 18 | 19 | ## FAQ 20 | 21 | ### Why does OpenXR Explorer show the layers in a different order? 22 | 23 | OpenXR Explorer v1.4 (latest as of 2024-04-04) and below show API layers in alphabetical order; OpenXR API Layers GUI instead shows API layers in their actual order. 24 | 25 | ## Getting Help 26 | 27 | No help is available for this tool, as every previous request for help has been for help with a specific API layer or game, not the tool, and I am unable to offer support for other people's software. 28 | 29 | - If you have a problem with a specific layer, or aren't sure which order things should be in, search for how to get help for the layers in question 30 | - If you have a problem with a game but don't know which layer is the problem, try disabling all layers. If that fixes it, enable them one at a time until you have problems, then look up how to get help for the layers. If you're stuck, try the forums, Discord, or Reddit for the gaame or headset you're trying to use - whichever is more relevant to the problem. 31 | - If the game or API layers are unsupported, you might want to try forums/Discord/reddit for the game or headset you're trying to use - whichever is more relevant to your problem. I do not support other people's software, even if it's no longer supported by the authors. 32 | 33 | ## Screenshots 34 | 35 | Common errors are automatically detected: 36 | 37 | ![Lots of errors](docs/errors.png) 38 | 39 | Most errors can be automatically fixed; the 'Fix Them!' button in the screenshot produces this: 40 | 41 | ![Mostly fixed](docs/errors-fixed.png) 42 | 43 | Detailed information about OpenXR API layers is also shown: 44 | 45 | ![Name, description, exposed extensions](docs/details.png) 46 | 47 | ## Contributing 48 | 49 | See [CONTRIBUTING.md](CONTRIBUTING.md). 50 | 51 | ## License 52 | 53 | The majority of this project is licensed under the MIT license (below). 54 | 55 | The images in the 'icons' subfolder may not be reused or distributed outside of this product. 56 | 57 | A separate license for these images may be purchased from https://glyphicons.com 58 | 59 | ------ 60 | 61 | Copyright (c) 2023 Fred Emmott. 62 | 63 | Permission to use, copy, modify, and/or distribute this software for any purpose 64 | with or without fee is hereby granted, provided that the above copyright notice 65 | and this permission notice appear in all copies. 66 | 67 | THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 68 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 69 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 70 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 71 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 72 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 73 | THIS SOFTWARE. 74 | -------------------------------------------------------------------------------- /docs/details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fredemmott/OpenXR-API-Layers-GUI/b0198f4bb29c9ed4705c04d43d34ae9269c176f5/docs/details.png -------------------------------------------------------------------------------- /docs/errors-fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fredemmott/OpenXR-API-Layers-GUI/b0198f4bb29c9ed4705c04d43d34ae9269c176f5/docs/errors-fixed.png -------------------------------------------------------------------------------- /docs/errors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fredemmott/OpenXR-API-Layers-GUI/b0198f4bb29c9ed4705c04d43d34ae9269c176f5/docs/errors.png -------------------------------------------------------------------------------- /icon/LICENSE: -------------------------------------------------------------------------------- 1 | These images not be reused or distributed outside of this product. 2 | 3 | A separate license for these images may be purchased from https://glyphicons.com 4 | -------------------------------------------------------------------------------- /icon/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fredemmott/OpenXR-API-Layers-GUI/b0198f4bb29c9ed4705c04d43d34ae9269c176f5/icon/icon.ico -------------------------------------------------------------------------------- /icon/icon.rc: -------------------------------------------------------------------------------- 1 | appIcon ICON "icon.ico" 2 | -------------------------------------------------------------------------------- /icon/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 14 | 32 | 36 | 40 | 41 | -------------------------------------------------------------------------------- /icon/rasterize.ps1: -------------------------------------------------------------------------------- 1 | $nativeSize = 32 2 | $nativeDpi = 96 3 | $sizes = 16, 20, 24, 30, 32, 36, 40, 48, 60, 64, 72, 80, 96, 128, 256 4 | $intermediates = @() 5 | foreach ($size in $sizes) { 6 | $out = "icon-${size}.png" 7 | magick convert ` 8 | -background none ` 9 | -density $(($nativeDpi * $size) / $nativeSize) ` 10 | "$(Get-Location)\icon.svg" ` 11 | -gravity center ` 12 | -extent "${size}x${size}" ` 13 | png:$out 14 | $intermediates += $out 15 | } 16 | Remove-Item icon.ico 17 | magick convert ` 18 | -background none ` 19 | @intermediates ` 20 | icon.ico 21 | Remove-Item $intermediates 22 | -------------------------------------------------------------------------------- /sign_target.cmake: -------------------------------------------------------------------------------- 1 | get_filename_component( 2 | WINDOWS_10_KITS_ROOT 3 | "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots;KitsRoot10]" 4 | ABSOLUTE CACHE 5 | ) 6 | set(WINDOWS_10_KIT_DIR "${WINDOWS_10_KITS_ROOT}/bin/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}" CACHE PATH "Current Windows 10 kit directory") 7 | set(SIGNTOOL_KEY_ARGS "" CACHE STRING "Key arguments for signtool.exe - separate with ';'") 8 | find_program( 9 | SIGNTOOL_EXE 10 | signtool 11 | PATHS 12 | "${WINDOWS_10_KIT_DIR}/x64" 13 | "${WINDOWS_10_KIT_DIR}/x86" 14 | DOC "Path to signtool.exe if SIGNTOOL_KEY_ARGS is set" 15 | ) 16 | 17 | function(sign_target_file TARGET FILE) 18 | if(SIGNTOOL_KEY_ARGS AND WIN32) 19 | add_custom_command( 20 | TARGET ${TARGET} POST_BUILD 21 | COMMAND 22 | "${SIGNTOOL_EXE}" 23 | ARGS 24 | sign 25 | ${SIGNTOOL_KEY_ARGS} 26 | /t http://timestamp.digicert.com 27 | /fd SHA256 28 | "${FILE}" 29 | ) 30 | endif() 31 | endfunction() 32 | 33 | function(sign_target TARGET) 34 | sign_target_file("${TARGET}" "$") 35 | endfunction() 36 | 37 | macro(add_signed_script TARGET SOURCE) 38 | get_filename_component(FILE_NAME "${SOURCE}" NAME) 39 | add_custom_target( 40 | ${TARGET} 41 | ALL 42 | COMMAND 43 | "${CMAKE_COMMAND}" 44 | -E 45 | copy_if_different 46 | "${SOURCE}" 47 | "${CMAKE_CURRENT_BINARY_DIR}/${FILE_NAME}" 48 | WORKING_DIRECTORY 49 | "${CMAKE_CURRENT_SOURCE_DIR}" 50 | SOURCES 51 | "${SOURCE}" 52 | ) 53 | 54 | sign_target_file( 55 | ${TARGET} 56 | "${CMAKE_CURRENT_BINARY_DIR}/${FILE_NAME}" 57 | ) 58 | 59 | install( 60 | FILES 61 | "${CMAKE_CURRENT_BINARY_DIR}/${FILE_NAME}" 62 | ${ARGN} 63 | ) 64 | endmacro() 65 | -------------------------------------------------------------------------------- /src/APILayer.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | namespace FredEmmott::OpenXRLayers { 9 | 10 | /** The basic information about an API layer. 11 | * 12 | * This contains the information that is available in the list of 13 | * API layers, e.g. the Windows registry, not data from the manifest. 14 | * 15 | * Manifest data is available via `APILayerDetails`. 16 | */ 17 | struct APILayer { 18 | enum class Value { 19 | Enabled, 20 | Disabled, 21 | Win32_NotDWORD, 22 | }; 23 | 24 | std::filesystem::path mJSONPath; 25 | Value mValue; 26 | 27 | constexpr bool IsEnabled() const noexcept { 28 | return mValue == Value::Enabled; 29 | }; 30 | 31 | bool operator==(const APILayer&) const noexcept = default; 32 | }; 33 | 34 | struct Extension { 35 | std::string mName; 36 | std::string mVersion; 37 | 38 | bool operator==(const Extension&) const noexcept = default; 39 | }; 40 | 41 | /// Information from the API layer manifest 42 | struct APILayerDetails { 43 | APILayerDetails(const std::filesystem::path& jsonPath); 44 | enum class State { 45 | Uninitialized, 46 | NoJsonFile, 47 | UnreadableJsonFile, 48 | InvalidJson, 49 | MissingData, 50 | Loaded, 51 | }; 52 | State mState {State::Uninitialized}; 53 | 54 | std::string mFileFormatVersion; 55 | std::string mName; 56 | std::filesystem::path mLibraryPath; 57 | std::string mDescription; 58 | std::string mAPIVersion; 59 | std::string mImplementationVersion; 60 | std::vector mExtensions; 61 | 62 | bool operator==(const APILayerDetails&) const noexcept = default; 63 | 64 | std::string StateAsString() const noexcept; 65 | }; 66 | 67 | }// namespace FredEmmott::OpenXRLayers 68 | -------------------------------------------------------------------------------- /src/APILayerDetails.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "APILayer.hpp" 10 | 11 | namespace FredEmmott::OpenXRLayers { 12 | 13 | static void SetStringOrNumber( 14 | std::string& variable, 15 | const nlohmann::json& json, 16 | const auto& key) { 17 | if (!json.contains(key)) { 18 | return; 19 | } 20 | const auto value = json.at(key); 21 | 22 | if (value.is_string()) { 23 | variable = value; 24 | return; 25 | } 26 | 27 | if (value.is_number_integer()) { 28 | variable = fmt::to_string(value.template get()); 29 | return; 30 | } 31 | } 32 | 33 | APILayerDetails::APILayerDetails(const std::filesystem::path& jsonPath) { 34 | if (!std::filesystem::exists(jsonPath)) { 35 | mState = State::NoJsonFile; 36 | return; 37 | } 38 | 39 | std::ifstream f(jsonPath); 40 | if (!f) { 41 | mState = State::UnreadableJsonFile; 42 | return; 43 | } 44 | 45 | nlohmann::json json; 46 | try { 47 | json = nlohmann::json::parse(f); 48 | } catch (const std::runtime_error&) { 49 | mState = State::InvalidJson; 50 | return; 51 | } 52 | 53 | mFileFormatVersion = json.value("file_format_version", std::string {}); 54 | if (!json.contains("api_layer")) { 55 | mState = State::MissingData; 56 | return; 57 | } 58 | auto layer = json.at("api_layer"); 59 | 60 | mName = layer.value("name", std::string {}); 61 | 62 | auto libraryPath 63 | = std::filesystem::path(layer.value("library_path", std::string {})); 64 | if (!libraryPath.empty()) { 65 | if (libraryPath.is_absolute()) { 66 | mLibraryPath = libraryPath; 67 | } else { 68 | libraryPath = jsonPath.parent_path() / libraryPath; 69 | if (std::filesystem::exists(libraryPath)) { 70 | mLibraryPath = std::filesystem::canonical(libraryPath); 71 | } else { 72 | mLibraryPath = std::filesystem::weakly_canonical(libraryPath); 73 | } 74 | } 75 | } 76 | 77 | mAPIVersion = layer.value("api_version", std::string {}); 78 | mDescription = layer.value("description", std::string {}); 79 | 80 | if (layer.contains("instance_extensions")) { 81 | auto extensions = layer.at("instance_extensions"); 82 | if (extensions.is_array()) { 83 | for (const auto& extension: extensions) { 84 | std::string version; 85 | SetStringOrNumber(version, extension, "extension_version"); 86 | 87 | mExtensions.push_back(Extension { 88 | .mName = extension.value("name", std::string {}), 89 | .mVersion = version, 90 | }); 91 | } 92 | } 93 | } 94 | 95 | SetStringOrNumber(mImplementationVersion, layer, "implementation_version"); 96 | 97 | mState = State::Loaded; 98 | } 99 | 100 | std::string APILayerDetails::StateAsString() const noexcept { 101 | switch (mState) { 102 | case State::Loaded: 103 | return "Loaded"; 104 | case State::Uninitialized: 105 | return "Internal error"; 106 | case State::NoJsonFile: 107 | return "The file does not exist"; 108 | case State::UnreadableJsonFile: 109 | return "The JSON file is unreadable"; 110 | case State::InvalidJson: 111 | return "The file does not contain valid JSON"; 112 | case State::MissingData: 113 | return "The file does not contain data required by OpenXR"; 114 | default: 115 | return fmt::format( 116 | "Internal error ({})", 117 | static_cast>(mState)); 118 | } 119 | } 120 | 121 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/APILayerStore.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #pragma once 5 | #include 6 | #include 7 | 8 | #include "APILayer.hpp" 9 | 10 | namespace FredEmmott::OpenXRLayers { 11 | 12 | class APILayerStore { 13 | public: 14 | virtual ~APILayerStore() = default; 15 | 16 | // e.g. "Win64-HKLM" 17 | virtual std::string GetDisplayName() const noexcept = 0; 18 | virtual std::vector GetAPILayers() const noexcept = 0; 19 | 20 | virtual bool Poll() const noexcept = 0; 21 | 22 | static std::span Get() noexcept; 23 | }; 24 | 25 | class ReadWriteAPILayerStore : public virtual APILayerStore { 26 | public: 27 | virtual bool SetAPILayers(const std::vector&) const noexcept = 0; 28 | 29 | static std::span Get() noexcept; 30 | }; 31 | 32 | }// namespace FredEmmott::OpenXRLayers 33 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Fred Emmott 2 | # SPDX-License-Identifier: ISC 3 | 4 | find_package(imgui CONFIG REQUIRED) 5 | find_package(ImGui-SFML CONFIG REQUIRED) 6 | find_package(nlohmann_json CONFIG REQUIRED) 7 | # Using fmt instead of fmt::format until MacOS/libc++ catch up 8 | find_package(fmt CONFIG REQUIRED) 9 | 10 | if(WIN32) 11 | find_package(cppwinrt CONFIG REQUIRED) 12 | find_package(wil CONFIG REQUIRED) 13 | endif() 14 | 15 | if(USE_EMOJI) 16 | set(USE_EMOJI_BOOL "true") 17 | else() 18 | set(USE_EMOJI_BOOL "false") 19 | endif() 20 | 21 | file(READ "${CMAKE_CURRENT_SOURCE_DIR}/../LICENSE" LICENSE_TEXT) 22 | 23 | set(CODEGEN_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/${BUILD_TARGET_ID}") 24 | configure_file( 25 | "${CMAKE_CURRENT_SOURCE_DIR}/Config.in.hpp" 26 | "${CODEGEN_BUILD_DIR}/Config.hpp" 27 | @ONLY 28 | ) 29 | 30 | add_library( 31 | lib 32 | STATIC 33 | APILayerDetails.cpp 34 | GUI.cpp 35 | SaveReport.cpp 36 | std23/ranges.hpp 37 | ConstexprString.hpp 38 | StringTemplateParameter.hpp 39 | GetActiveRuntimePath.hpp 40 | windows/GetActiveRuntimePath.cpp 41 | ) 42 | 43 | # Using an OBJECT library instead of `STATIC`, because otherwise 44 | # Microsoft's linker gets rid of the static initializers used to register linters :( 45 | add_library( 46 | linters 47 | OBJECT 48 | Linter.cpp 49 | linters/BadInstallationLinter.cpp 50 | linters/DuplicatesLinter.cpp 51 | linters/OrderingLinter.cpp 52 | LayerRules.cpp 53 | ) 54 | target_link_libraries(linters PUBLIC lib) 55 | target_include_directories( 56 | lib 57 | PUBLIC 58 | "${CODEGEN_BUILD_DIR}" 59 | "${CMAKE_CURRENT_SOURCE_DIR}" 60 | ) 61 | 62 | target_link_libraries( 63 | lib 64 | PUBLIC 65 | ImGui-SFML::ImGui-SFML 66 | nlohmann_json::nlohmann_json 67 | fmt::fmt-header-only 68 | ) 69 | 70 | add_executable( 71 | gui 72 | WIN32 73 | "${CMAKE_SOURCE_DIR}/icon/icon.rc" 74 | ) 75 | install( 76 | TARGETS gui 77 | DESTINATION "." 78 | ) 79 | sign_target(gui) 80 | # Used by version.in.rc 81 | set(OUTPUT_NAME "OpenXR-API-Layers-GUI") 82 | set_target_properties( 83 | gui 84 | PROPERTIES 85 | OUTPUT_NAME "${OUTPUT_NAME}" 86 | ) 87 | target_link_libraries(gui PRIVATE lib linters) 88 | 89 | set(PLATFORM_APILAYERS_CPP "stubs/APILayerStore.cpp") 90 | set(PLATFORM_MAIN_CPP "main.cpp") 91 | set(PLATFORM_GUI_CPP "stubs/PlatformGUI.cpp") 92 | set(PLATFORM_REPORT_MAIN_CPP "SaveReport_main.cpp") 93 | 94 | if(WIN32) 95 | set(PLATFORM_APILAYERS_CPP "windows/WindowsAPILayerStore.cpp") 96 | set(PLATFORM_GUI_CPP "windows/PlatformGUI.cpp") 97 | set(PLATFORM_MAIN_CPP "windows/wWinMain.cpp") 98 | set(PLATFORM_REPORT_MAIN_CPP "windows/SaveReport_wWinMain.cpp") 99 | 100 | configure_file( 101 | "${CMAKE_CURRENT_SOURCE_DIR}/version.in.rc" 102 | "${CODEGEN_BUILD_DIR}/gui-version.rc" 103 | @ONLY 104 | ) 105 | 106 | target_sources( 107 | lib 108 | PRIVATE 109 | windows/CheckForUpdates.cpp 110 | windows/GetActiveRuntimePath.cpp 111 | ) 112 | 113 | target_sources( 114 | linters 115 | PRIVATE 116 | linters/windows/NotADWORDLinter.cpp 117 | linters/windows/OpenXRToolkitLinter.cpp 118 | linters/windows/OutdatedOpenKneeboardLinter.cpp 119 | linters/windows/ProgramFilesLinter.cpp 120 | linters/windows/UnsignedDllLinter.cpp 121 | linters/windows/XRNeckSaferLinter.cpp 122 | ) 123 | 124 | target_sources( 125 | gui 126 | PRIVATE 127 | "${CODEGEN_BUILD_DIR}/gui-version.rc" 128 | manifest.xml 129 | ) 130 | 131 | target_compile_definitions( 132 | lib 133 | PUBLIC 134 | "WIN32_LEAN_AND_MEAN" 135 | "NOMINMAX" 136 | ) 137 | 138 | target_compile_options( 139 | lib 140 | PUBLIC 141 | "/EHsc" 142 | "/diagnostics:caret" 143 | "/utf-8" 144 | "/await:strict" 145 | ) 146 | 147 | target_link_libraries( 148 | lib 149 | PUBLIC 150 | Microsoft::CppWinRT 151 | WIL::WIL 152 | Comctl32 153 | WinTrust 154 | Dwmapi 155 | # Missing dependency for CppWinRT vcpkg 156 | # https://github.com/microsoft/vcpkg/issues/15339 157 | RuntimeObject 158 | ) 159 | 160 | target_link_options( 161 | gui 162 | PRIVATE 163 | "/MANIFESTUAC:level='requireAdministrator'" 164 | ) 165 | 166 | endif() 167 | 168 | target_sources( 169 | lib 170 | PRIVATE 171 | "${PLATFORM_APILAYERS_CPP}" 172 | "${PLATFORM_GUI_CPP}" 173 | ) 174 | target_sources( 175 | gui 176 | PRIVATE 177 | "${PLATFORM_MAIN_CPP}" 178 | ) 179 | 180 | set(OUTPUT_NAME "OpenXR-API-Layers-Create-Report") 181 | configure_file( 182 | "${CMAKE_CURRENT_SOURCE_DIR}/version.in.rc" 183 | "${CODEGEN_BUILD_DIR}/create-report-version.rc" 184 | @ONLY 185 | ) 186 | add_executable( 187 | create-report 188 | WIN32 189 | "${PLATFORM_REPORT_MAIN_CPP}" 190 | "${CMAKE_SOURCE_DIR}/icon/icon.rc" 191 | "${CODEGEN_BUILD_DIR}/create-report-version.rc" 192 | ) 193 | target_link_libraries( 194 | create-report 195 | PRIVATE 196 | lib 197 | linters 198 | ) 199 | set_target_properties( 200 | create-report 201 | PROPERTIES 202 | OUTPUT_NAME "${OUTPUT_NAME}" 203 | ) 204 | sign_target(create-report) 205 | install( 206 | TARGETS create-report 207 | DESTINATION "." 208 | ) 209 | 210 | if (WIN32) 211 | set(UPDATER_FILENAME "fredemmott_OpenXR-API-Layers-GUI_Updater.exe") 212 | add_custom_command( 213 | TARGET gui POST_BUILD 214 | COMMAND 215 | "${CMAKE_COMMAND}" -E copy_if_different 216 | "${UPDATER_EXE}" 217 | "$/${UPDATER_FILENAME}" 218 | VERBATIM 219 | ) 220 | install( 221 | FILES 222 | "$/${UPDATER_FILENAME}" 223 | DESTINATION "." 224 | ) 225 | 226 | install( 227 | FILES 228 | "$" 229 | "$" 230 | DESTINATION "." 231 | COMPONENT "DebugSymbols" 232 | EXCLUDE_FROM_ALL 233 | ) 234 | endif() 235 | -------------------------------------------------------------------------------- /src/Config.in.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | namespace FredEmmott::OpenXRLayers::Config { 9 | 10 | constexpr auto BUILD_VERSION {"@BUILD_VERSION_STRING@"}; 11 | constexpr auto BUILD_VERSION_W {L"@BUILD_VERSION_STRING@"}; 12 | 13 | // clang-format off 14 | constexpr bool USE_EMOJI { @USE_EMOJI_BOOL@ }; 15 | // clang-format on 16 | 17 | constexpr auto GLYPH_ENABLED {USE_EMOJI ? "\u2705" : "Y"}; 18 | constexpr auto GLYPH_DISABLED {USE_EMOJI ? "\u274c" : "N"}; 19 | constexpr auto GLYPH_ERROR {USE_EMOJI ? "\u26a0" : "!"}; 20 | 21 | constexpr auto LICENSE_TEXT {R"---LICENSE---(@LICENSE_TEXT@)---LICENSE---"}; 22 | 23 | }// namespace FredEmmott::OpenXRLayers::Config -------------------------------------------------------------------------------- /src/ConstexprString.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | #pragma once 4 | 5 | #include 6 | 7 | namespace FredEmmott::OpenXRLayers { 8 | 9 | /** A compile-time or runtime string. 10 | * 11 | * If it is initialized at compile-time, it contains an `std::string_view` 12 | * reference to the compile-time constant. 13 | * 14 | * If it is initialized at runtime with a dynamic string, it stores a copy. 15 | * 16 | * This allows classes that contain strings to have both `constexpr` and safe 17 | * runtime instantiations. 18 | */ 19 | class ConstexprString final { 20 | public: 21 | ConstexprString() = delete; 22 | constexpr ConstexprString(std::string_view init) { 23 | if (std::is_constant_evaluated()) { 24 | mStorage = init; 25 | } else { 26 | mStorage = std::string {init}; 27 | } 28 | } 29 | 30 | [[nodiscard]] constexpr std::string_view Get() const noexcept { 31 | if (std::holds_alternative(mStorage)) { 32 | return std::get(mStorage); 33 | } 34 | return std::get(mStorage); 35 | } 36 | 37 | constexpr bool operator==(const ConstexprString& other) const noexcept 38 | = default; 39 | 40 | private: 41 | std::variant mStorage; 42 | }; 43 | 44 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/GUI.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #include "GUI.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include 14 | 15 | #include "APILayer.hpp" 16 | #include "APILayerStore.hpp" 17 | #include "Config.hpp" 18 | #include "Linter.hpp" 19 | #include "SaveReport.hpp" 20 | #include 21 | 22 | namespace FredEmmott::OpenXRLayers { 23 | 24 | namespace { 25 | constexpr ImVec2 MINIMUM_WINDOW_SIZE {1024, 768}; 26 | 27 | class MyWindow final : public sf::RenderWindow { 28 | public: 29 | using sf::RenderWindow::RenderWindow; 30 | ~MyWindow() override = default; 31 | 32 | void setMinimumSize(const ImVec2& value) { 33 | mMinimumSize = value; 34 | this->onResize(); 35 | } 36 | 37 | protected: 38 | void onResize() override { 39 | auto size = this->getSize(); 40 | auto newSize = size; 41 | if (size.x < mMinimumSize.x) { 42 | newSize.x = static_cast(mMinimumSize.x); 43 | } 44 | if (size.y < mMinimumSize.y) { 45 | newSize.y = static_cast(mMinimumSize.y); 46 | } 47 | 48 | if (size == newSize) { 49 | return; 50 | } 51 | 52 | this->setSize(newSize); 53 | } 54 | 55 | private: 56 | ImVec2 mMinimumSize {MINIMUM_WINDOW_SIZE}; 57 | }; 58 | }// namespace 59 | 60 | void GUI::Run() { 61 | auto& platform = PlatformGUI::Get(); 62 | 63 | MyWindow window { 64 | sf::VideoMode( 65 | static_cast(MINIMUM_WINDOW_SIZE.x), 66 | static_cast(MINIMUM_WINDOW_SIZE.y)), 67 | fmt::format("OpenXR API Layers v{}", Config::BUILD_VERSION)}; 68 | window.setFramerateLimit(60); 69 | if (!ImGui::SFML::Init(window)) { 70 | return; 71 | } 72 | mWindowHandle = window.getSystemHandle(); 73 | 74 | platform.SetWindow(mWindowHandle); 75 | 76 | // partial workaround for: 77 | // - https://github.com/SFML/imgui-sfml/issues/206 78 | // - https://github.com/SFML/imgui-sfml/issues/212 79 | // 80 | // remainder is in windows/PlatformGUI.cpp 81 | ImGui::SFML::ProcessEvent(window, {sf::Event::LostFocus}); 82 | ImGui::SFML::ProcessEvent(window, {sf::Event::GainedFocus}); 83 | 84 | auto dpiScaling = platform.GetDPIScaling(); 85 | window.setMinimumSize({ 86 | MINIMUM_WINDOW_SIZE.x * dpiScaling, 87 | MINIMUM_WINDOW_SIZE.y * dpiScaling, 88 | }); 89 | platform.SetupFonts(&ImGui::GetIO()); 90 | 91 | sf::Clock deltaClock {}; 92 | 93 | std::vector layerSets; 94 | for (auto&& store: ReadWriteAPILayerStore::Get()) { 95 | layerSets.push_back({store}); 96 | } 97 | while (window.isOpen()) { 98 | { 99 | if (const auto changeInfo = platform.GetDPIChangeInfo()) { 100 | window.setMinimumSize({0, 0}); 101 | if (changeInfo->mRecommendedSize) { 102 | window.setSize(*changeInfo->mRecommendedSize); 103 | } 104 | window.setMinimumSize({ 105 | MINIMUM_WINDOW_SIZE.x * changeInfo->mDPIScaling, 106 | MINIMUM_WINDOW_SIZE.y * changeInfo->mDPIScaling, 107 | }); 108 | platform.SetupFonts(&ImGui::GetIO()); 109 | } 110 | } 111 | 112 | sf::Event event {}; 113 | while (window.pollEvent(event)) { 114 | ImGui::SFML::ProcessEvent(window, event); 115 | if (event.type == sf::Event::Closed) { 116 | window.close(); 117 | } 118 | } 119 | 120 | ImGui::SFML::Update(window, deltaClock.restart()); 121 | 122 | auto viewport = ImGui::GetMainViewport(); 123 | ImGui::SetNextWindowPos(viewport->WorkPos); 124 | ImGui::SetNextWindowSize(viewport->WorkSize); 125 | 126 | platform.BeginFrame(); 127 | 128 | ImGui::Begin( 129 | "MainWindow", 130 | nullptr, 131 | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove 132 | | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings 133 | | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar 134 | | ImGuiWindowFlags_NoScrollWithMouse); 135 | 136 | if (ImGui::BeginTabBar("##LayerSetTabs", ImGuiTabBarFlags_None)) { 137 | for (auto& layerSet: layerSets) { 138 | const auto name = layerSet.mStore->GetDisplayName(); 139 | const auto label = layerSet.HasErrors() 140 | ? fmt::format("{} {}", Config::GLYPH_ERROR, name) 141 | : name; 142 | const auto labelWithID = fmt::format("{}###layerSet-{}", label, name); 143 | if (ImGui::BeginTabItem(labelWithID.c_str())) { 144 | layerSet.Draw(); 145 | ImGui::EndTabItem(); 146 | } 147 | } 148 | 149 | if (ImGui::BeginTabItem("About")) { 150 | ImGui::TextWrapped( 151 | "OpenXR API Layers GUI v%s\n\n---\n\n%s", 152 | Config::BUILD_VERSION, 153 | Config::LICENSE_TEXT); 154 | ImGui::EndTabItem(); 155 | } 156 | 157 | if (ImGui::TabItemButton("Save Report...", ImGuiTabItemFlags_Trailing)) { 158 | this->Export(); 159 | } 160 | 161 | ImGui::EndTabBar(); 162 | } 163 | 164 | ImGui::End(); 165 | 166 | ImGui::SFML::Render(window); 167 | window.display(); 168 | } 169 | ImGui::SFML::Shutdown(); 170 | } 171 | 172 | void GUI::LayerSet::GUILayersList() { 173 | auto viewport = ImGui::GetMainViewport(); 174 | const auto dpiScale = PlatformGUI::Get().GetDPIScaling(); 175 | ImGui::BeginListBox( 176 | "##Layers", 177 | {viewport->WorkSize.x - (256 * dpiScale), viewport->WorkSize.y / 2}); 178 | ImGuiListClipper clipper {}; 179 | clipper.Begin(static_cast(mLayers.size())); 180 | 181 | while (clipper.Step()) { 182 | for (auto i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) { 183 | auto& layer = mLayers.at(i); 184 | auto layerErrors 185 | = std::ranges::filter_view(mLintErrors, [&layer](const auto& error) { 186 | return error->GetAffectedLayers().contains(layer.mJSONPath); 187 | }); 188 | 189 | ImGui::PushID(i); 190 | 191 | bool layerIsEnabled = layer.IsEnabled(); 192 | if (ImGui::Checkbox("##Enabled", &layerIsEnabled)) { 193 | using Value = APILayer::Value; 194 | layer.mValue = layerIsEnabled ? Value::Enabled : Value::Disabled; 195 | mLintErrorsAreStale = true; 196 | mStore->SetAPILayers(mLayers); 197 | } 198 | 199 | auto label = layer.mJSONPath.string(); 200 | 201 | if (!layerErrors.empty()) { 202 | label = fmt::format("{} {}", Config::GLYPH_ERROR, label); 203 | } 204 | 205 | ImGui::SameLine(); 206 | 207 | if (ImGui::Selectable(label.c_str(), mSelectedLayer == &layer)) { 208 | mSelectedLayer = &layer; 209 | } 210 | 211 | if (ImGui::BeginDragDropSource()) { 212 | const size_t index = i; 213 | ImGui::SetDragDropPayload("APILayerIndex", &index, sizeof(index)); 214 | ImGui::EndDragDropSource(); 215 | } 216 | 217 | if (ImGui::BeginDragDropTarget()) { 218 | if ( 219 | const auto payload = ImGui::AcceptDragDropPayload("APILayerIndex")) { 220 | const auto sourceIndex = *static_cast(payload->Data); 221 | const auto& source = mLayers.at(sourceIndex); 222 | DragDropReorder(source, layer); 223 | } 224 | ImGui::EndDragDropTarget(); 225 | } 226 | 227 | ImGui::PopID(); 228 | } 229 | } 230 | ImGui::EndListBox(); 231 | } 232 | 233 | void GUI::LayerSet::GUIButtons() { 234 | ImGui::BeginGroup(); 235 | if (ImGui::Button("Reload List", {-FLT_MIN, 0})) { 236 | mLayerDataIsStale = true; 237 | } 238 | 239 | if (ImGui::Button("Add Layers...", {-FLT_MIN, 0})) { 240 | this->AddLayersClicked(); 241 | } 242 | 243 | ImGui::BeginDisabled(mSelectedLayer == nullptr); 244 | if (ImGui::Button("Remove Layer...", {-FLT_MIN, 0})) { 245 | ImGui::OpenPopup("Remove Layer"); 246 | } 247 | ImGui::EndDisabled(); 248 | this->GUIRemoveLayerPopup(); 249 | 250 | ImGui::Separator(); 251 | 252 | ImGui::BeginDisabled(!(mSelectedLayer && !mSelectedLayer->IsEnabled())); 253 | using Value = APILayer::Value; 254 | if (ImGui::Button("Enable Layer", {-FLT_MIN, 0})) { 255 | mSelectedLayer->mValue = Value::Enabled; 256 | mLintErrorsAreStale = true; 257 | mStore->SetAPILayers(mLayers); 258 | } 259 | ImGui::EndDisabled(); 260 | 261 | ImGui::BeginDisabled(!(mSelectedLayer && mSelectedLayer->IsEnabled())); 262 | if (ImGui::Button("Disable Layer", {-FLT_MIN, 0})) { 263 | mSelectedLayer->mValue = Value::Disabled; 264 | mLintErrorsAreStale = true; 265 | mStore->SetAPILayers(mLayers); 266 | } 267 | ImGui::EndDisabled(); 268 | 269 | ImGui::Separator(); 270 | 271 | ImGui::BeginDisabled(!(mSelectedLayer && *mSelectedLayer != mLayers.front())); 272 | if (ImGui::Button("Move Up", {-FLT_MIN, 0})) { 273 | auto newLayers = mLayers; 274 | auto it = std::ranges::find(newLayers, *mSelectedLayer); 275 | if (it != newLayers.begin() && it != newLayers.end()) { 276 | std::iter_swap((it - 1), it); 277 | mStore->SetAPILayers(newLayers); 278 | mLayerDataIsStale = true; 279 | } 280 | } 281 | ImGui::EndDisabled(); 282 | ImGui::BeginDisabled(!(mSelectedLayer && *mSelectedLayer != mLayers.back())); 283 | if (ImGui::Button("Move Down", {-FLT_MIN, 0})) { 284 | auto newLayers = mLayers; 285 | auto it = std::ranges::find(newLayers, *mSelectedLayer); 286 | if (it != newLayers.end() && (it + 1) != newLayers.end()) { 287 | std::iter_swap(it, it + 1); 288 | mStore->SetAPILayers(newLayers); 289 | mLayerDataIsStale = true; 290 | } 291 | } 292 | ImGui::EndDisabled(); 293 | 294 | ImGui::EndGroup(); 295 | } 296 | 297 | void GUI::LayerSet::GUITabs() { 298 | if (ImGui::BeginTabBar("##ErrorDetailsTabs", ImGuiTabBarFlags_None)) { 299 | this->GUIErrorsTab(); 300 | this->GUIDetailsTab(); 301 | 302 | ImGui::EndTabBar(); 303 | } 304 | } 305 | 306 | void GUI::LayerSet::GUIErrorsTab() { 307 | if (ImGui::BeginTabItem("Warnings")) { 308 | ImGui::BeginChild("##ScrollArea", {-FLT_MIN, -FLT_MIN}); 309 | if (mSelectedLayer) { 310 | ImGui::Text("For %s:", mSelectedLayer->mJSONPath.string().c_str()); 311 | } else { 312 | ImGui::Text("All layers:"); 313 | } 314 | 315 | LintErrors selectedErrors {}; 316 | if (mSelectedLayer) { 317 | auto view = std::ranges::filter_view( 318 | mLintErrors, [layer = mSelectedLayer](const auto& error) { 319 | return error->GetAffectedLayers().contains(layer->mJSONPath); 320 | }); 321 | selectedErrors = {view.begin(), view.end()}; 322 | } else { 323 | selectedErrors = mLintErrors; 324 | } 325 | 326 | ImGui::Indent(); 327 | 328 | if (selectedErrors.empty()) { 329 | ImGui::Separator(); 330 | ImGui::BeginDisabled(); 331 | if (mSelectedLayer) { 332 | if (mSelectedLayer->IsEnabled()) { 333 | ImGui::Text("No warnings."); 334 | } else { 335 | ImGui::Text( 336 | "No warnings, however most checks were skipped because the layer " 337 | "is disabled."); 338 | } 339 | } else { 340 | // No layers selected 341 | if (std::ranges::any_of(mLayers, &APILayer::IsEnabled)) { 342 | ImGui::Text("No warnings in enabled layers."); 343 | } else { 344 | ImGui::Text( 345 | "No warnings, however most checks were skipped because there are " 346 | "no enabled layers."); 347 | } 348 | } 349 | ImGui::EndDisabled(); 350 | } else { 351 | std::vector> fixableErrors; 352 | for (const auto& error: selectedErrors) { 353 | auto fixable = std::dynamic_pointer_cast(error); 354 | if (fixable) { 355 | fixableErrors.push_back(fixable); 356 | } 357 | } 358 | 359 | if (fixableErrors.size() > 1) { 360 | ImGui::AlignTextToFramePadding(); 361 | if (fixableErrors.size() == selectedErrors.size()) { 362 | ImGui::Text( 363 | "%s", 364 | fmt::format( 365 | "All {} warnings are automatically fixable:", 366 | fixableErrors.size()) 367 | .c_str()); 368 | } else { 369 | ImGui::Text( 370 | "%s", 371 | fmt::format( 372 | "{} out of {} warnings are automatically " 373 | "fixable:", 374 | fixableErrors.size(), 375 | selectedErrors.size()) 376 | .c_str()); 377 | } 378 | ImGui::SameLine(); 379 | if (ImGui::Button("Fix Them!")) { 380 | auto nextLayers = mLayers; 381 | for (auto&& fixable: fixableErrors) { 382 | nextLayers = fixable->Fix(nextLayers); 383 | } 384 | mStore->SetAPILayers(nextLayers); 385 | mLayerDataIsStale = true; 386 | } 387 | } 388 | 389 | ImGui::BeginTable( 390 | "##Errors", 3, ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_RowBg); 391 | ImGui::TableSetupColumn("RowNumber", ImGuiTableColumnFlags_WidthFixed); 392 | ImGui::TableSetupColumn("Description"); 393 | ImGui::TableSetupColumn("Buttons", ImGuiTableColumnFlags_WidthFixed); 394 | for (int i = 0; i < selectedErrors.size(); ++i) { 395 | const auto& error = selectedErrors.at(i); 396 | const auto desc = error->GetDescription(); 397 | 398 | ImGui::PushID(i); 399 | ImGui::TableNextRow(); 400 | ImGui::TableNextColumn(); 401 | ImGui::Text("%s", fmt::to_string(i + 1).c_str()); 402 | ImGui::TableNextColumn(); 403 | ImGui::TextWrapped("%s", desc.c_str()); 404 | ImGui::TableNextColumn(); 405 | { 406 | auto fixer = std::dynamic_pointer_cast(error); 407 | ImGui::BeginDisabled(!fixer); 408 | if (ImGui::Button("Fix It!")) { 409 | mStore->SetAPILayers(fixer->Fix(mLayers)); 410 | mLayerDataIsStale = true; 411 | } 412 | ImGui::EndDisabled(); 413 | } 414 | ImGui::SameLine(); 415 | if (ImGui::Button("Copy")) { 416 | ImGui::SetClipboardText(desc.c_str()); 417 | } 418 | 419 | ImGui::PopID(); 420 | } 421 | ImGui::EndTable(); 422 | } 423 | ImGui::Unindent(); 424 | 425 | ImGui::EndChild(); 426 | ImGui::EndTabItem(); 427 | } 428 | } 429 | 430 | void GUI::LayerSet::GUIDetailsTab() { 431 | if (ImGui::BeginTabItem("Details")) { 432 | ImGui::BeginChild("##ScrollArea", {-FLT_MIN, -FLT_MIN}); 433 | if (mSelectedLayer) { 434 | ImGui::BeginTable( 435 | "##DetailsTable", 436 | 2, 437 | ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit); 438 | 439 | ImGui::TableNextRow(); 440 | ImGui::TableNextColumn(); 441 | ImGui::Text("JSON File"); 442 | ImGui::TableNextColumn(); 443 | if (ImGui::Button("Copy##CopyJSONFile")) { 444 | ImGui::SetClipboardText(mSelectedLayer->mJSONPath.string().c_str()); 445 | } 446 | ImGui::SameLine(); 447 | ImGui::Text("%s", mSelectedLayer->mJSONPath.string().c_str()); 448 | 449 | const APILayerDetails details {mSelectedLayer->mJSONPath}; 450 | if (details.mState != APILayerDetails::State::Loaded) { 451 | const auto error = details.StateAsString(); 452 | ImGui::TableNextRow(); 453 | ImGui::TableNextColumn(); 454 | ImGui::Text("%s", Config::GLYPH_ERROR); 455 | ImGui::TableNextColumn(); 456 | ImGui::Text("%s", error.c_str()); 457 | } else /* have details */ { 458 | { 459 | ImGui::TableNextRow(); 460 | ImGui::TableNextColumn(); 461 | ImGui::Text("Library Path"); 462 | ImGui::TableNextColumn(); 463 | if (details.mLibraryPath.empty()) { 464 | ImGui::Text( 465 | "%s", fmt::format("{} [none]", Config::GLYPH_ERROR).c_str()); 466 | } else { 467 | auto text = details.mLibraryPath.string(); 468 | if (!std::filesystem::exists(details.mLibraryPath)) { 469 | text = fmt::format("{} {}", Config::GLYPH_ERROR, text); 470 | } 471 | if (ImGui::Button("Copy##LibraryPath")) { 472 | ImGui::SetClipboardText(details.mLibraryPath.string().c_str()); 473 | } 474 | ImGui::SameLine(); 475 | ImGui::Text("%s", text.c_str()); 476 | } 477 | } 478 | 479 | if (!details.mName.empty()) { 480 | ImGui::TableNextRow(); 481 | ImGui::TableNextColumn(); 482 | ImGui::Text("Name"); 483 | ImGui::TableNextColumn(); 484 | if (ImGui::Button("Copy##Name")) { 485 | ImGui::SetClipboardText(details.mName.c_str()); 486 | } 487 | ImGui::SameLine(); 488 | ImGui::Text("%s", details.mName.c_str()); 489 | } 490 | 491 | if (!details.mImplementationVersion.empty()) { 492 | ImGui::TableNextRow(); 493 | ImGui::TableNextColumn(); 494 | ImGui::Text("Version"); 495 | ImGui::TableNextColumn(); 496 | if (ImGui::Button("Copy##ImplementationVersion")) { 497 | ImGui::SetClipboardText(details.mImplementationVersion.c_str()); 498 | } 499 | ImGui::SameLine(); 500 | ImGui::Text("v%s", details.mImplementationVersion.c_str()); 501 | } 502 | 503 | if (!details.mDescription.empty()) { 504 | ImGui::TableNextRow(); 505 | ImGui::TableNextColumn(); 506 | ImGui::Text("Description"); 507 | ImGui::TableNextColumn(); 508 | ImGui::Text("%s", details.mDescription.c_str()); 509 | } 510 | 511 | if (!details.mAPIVersion.empty()) { 512 | ImGui::TableNextRow(); 513 | ImGui::TableNextColumn(); 514 | ImGui::Text("OpenXR API Version"); 515 | ImGui::TableNextColumn(); 516 | ImGui::Text("%s", details.mAPIVersion.c_str()); 517 | } 518 | 519 | if (!details.mFileFormatVersion.empty()) { 520 | ImGui::TableNextRow(); 521 | ImGui::TableNextColumn(); 522 | ImGui::Text("File Format Version"); 523 | ImGui::TableNextColumn(); 524 | ImGui::Text("v%s", details.mFileFormatVersion.c_str()); 525 | } 526 | 527 | if (!details.mExtensions.empty()) { 528 | ImGui::TableNextRow(); 529 | ImGui::TableNextColumn(); 530 | ImGui::Text("Extensions"); 531 | ImGui::TableNextColumn(); 532 | ImGui::BeginTable( 533 | "##ExtensionsTable", 534 | 2, 535 | ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit); 536 | ImGui::TableSetupColumn("Name"); 537 | ImGui::TableSetupColumn("Version"); 538 | ImGui::TableHeadersRow(); 539 | for (const auto& ext: details.mExtensions) { 540 | ImGui::PushID(ext.mName.c_str()); 541 | ImGui::TableNextRow(); 542 | ImGui::TableNextColumn(); 543 | if (ImGui::Button("Copy")) { 544 | ImGui::SetClipboardText(ext.mName.c_str()); 545 | } 546 | ImGui::SameLine(); 547 | ImGui::AlignTextToFramePadding(); 548 | ImGui::Text("%s", ext.mName.c_str()); 549 | ImGui::TableNextColumn(); 550 | ImGui::Text("%s", ext.mVersion.c_str()); 551 | ImGui::PopID(); 552 | } 553 | ImGui::EndTable(); 554 | ImGui::TableNextColumn(); 555 | } 556 | } 557 | 558 | ImGui::EndTable(); 559 | } else { 560 | ImGui::BeginDisabled(); 561 | ImGui::Text("Select a layer above for details."); 562 | ImGui::EndDisabled(); 563 | } 564 | ImGui::EndChild(); 565 | ImGui::EndTabItem(); 566 | } 567 | } 568 | 569 | void GUI::LayerSet::ReloadLayerDataNow() { 570 | auto newLayers = mStore->GetAPILayers(); 571 | if (mSelectedLayer) { 572 | auto it = std::ranges::find(newLayers, *mSelectedLayer); 573 | if (it != newLayers.end()) { 574 | mSelectedLayer = &*it; 575 | } else { 576 | mSelectedLayer = nullptr; 577 | } 578 | } 579 | mLayers = std::move(newLayers); 580 | mLayerDataIsStale = false; 581 | mLintErrorsAreStale = true; 582 | } 583 | 584 | bool GUI::LayerSet::HasErrors() { 585 | if (mLayerDataIsStale) { 586 | this->ReloadLayerDataNow(); 587 | } 588 | if (mLintErrorsAreStale) { 589 | this->RunAllLintersNow(); 590 | } 591 | return !mLintErrors.empty(); 592 | } 593 | 594 | void GUI::LayerSet::RunAllLintersNow() { 595 | mLintErrors = RunAllLinters(mStore, mLayers); 596 | mLintErrorsAreStale = false; 597 | } 598 | 599 | void GUI::LayerSet::AddLayersClicked() { 600 | auto paths = PlatformGUI::Get().GetNewAPILayerJSONPaths(); 601 | for (auto it = paths.begin(); it != paths.end();) { 602 | auto existingLayer = std::ranges::find_if( 603 | mLayers, [it](const auto& layer) { return layer.mJSONPath == *it; }); 604 | if (existingLayer != mLayers.end()) { 605 | it = paths.erase(it); 606 | continue; 607 | } 608 | ++it; 609 | } 610 | 611 | if (paths.empty()) { 612 | return; 613 | } 614 | auto nextLayers = mLayers; 615 | for (const auto& path: paths) { 616 | nextLayers.push_back(APILayer { 617 | .mJSONPath = path, 618 | .mValue = APILayer::Value::Enabled, 619 | }); 620 | } 621 | 622 | bool changed = false; 623 | do { 624 | changed = false; 625 | auto errors = RunAllLinters(mStore, nextLayers); 626 | for (const auto& error: errors) { 627 | auto fixable = std::dynamic_pointer_cast(error); 628 | if (!fixable) { 629 | continue; 630 | } 631 | 632 | if (!std::ranges::any_of( 633 | fixable->GetAffectedLayers(), [&paths](const auto& it) { 634 | return std::ranges::find(paths, it) != paths.end(); 635 | })) { 636 | continue; 637 | } 638 | 639 | const auto fixed = fixable->Fix(nextLayers); 640 | if (fixed != nextLayers) { 641 | nextLayers = fixed; 642 | changed = true; 643 | } 644 | } 645 | } while (changed); 646 | 647 | mStore->SetAPILayers(nextLayers); 648 | mLayerDataIsStale = true; 649 | } 650 | 651 | void GUI::LayerSet::GUIRemoveLayerPopup() { 652 | auto viewport = ImGui::GetMainViewport(); 653 | ImVec2 center = viewport->GetCenter(); 654 | ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); 655 | ImGui::SetNextWindowSize({viewport->WorkSize.x / 2, 0}, ImGuiCond_Appearing); 656 | if (ImGui::BeginPopupModal( 657 | "Remove Layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { 658 | ImGui::TextWrapped( 659 | "Are you sure you want to completely remove '%s'?\n\nThis can not " 660 | "be " 661 | "undone.", 662 | mSelectedLayer->mJSONPath.string().c_str()); 663 | ImGui::Separator(); 664 | const auto dpiScaling = PlatformGUI::Get().GetDPIScaling(); 665 | ImGui::SetCursorPosX((256 + 128) * dpiScaling); 666 | if (ImGui::Button("Remove", {64 * dpiScaling, 0}) && mSelectedLayer) { 667 | auto nextLayers = mLayers; 668 | auto it = std::ranges::find(nextLayers, *mSelectedLayer); 669 | if (it != nextLayers.end()) { 670 | nextLayers.erase(it); 671 | mStore->SetAPILayers(nextLayers); 672 | mLayerDataIsStale = true; 673 | } 674 | ImGui::CloseCurrentPopup(); 675 | } 676 | ImGui::SameLine(); 677 | if (ImGui::Button("Cancel", {64 * dpiScaling, 0})) { 678 | ImGui::CloseCurrentPopup(); 679 | } 680 | ImGui::SetItemDefaultFocus(); 681 | 682 | ImGui::EndPopup(); 683 | } 684 | } 685 | 686 | void GUI::LayerSet::DragDropReorder( 687 | const APILayer& source, 688 | const APILayer& target) { 689 | auto newLayers = mLayers; 690 | 691 | auto sourceIt = std::ranges::find(newLayers, source); 692 | if (sourceIt == newLayers.end()) { 693 | return; 694 | } 695 | 696 | auto targetIt = std::ranges::find(newLayers, target); 697 | if (targetIt == newLayers.end() || sourceIt == targetIt) { 698 | return; 699 | } 700 | 701 | const auto insertBefore = sourceIt > targetIt; 702 | 703 | newLayers.erase(sourceIt); 704 | targetIt = std::ranges::find(newLayers, target); 705 | assert(targetIt != newLayers.end()); 706 | if (!insertBefore) { 707 | ++targetIt; 708 | } 709 | newLayers.insert(targetIt, source); 710 | 711 | assert(mLayers.size() == newLayers.size()); 712 | if (mStore->SetAPILayers(newLayers)) { 713 | mLayerDataIsStale = true; 714 | } 715 | } 716 | 717 | void GUI::LayerSet::Draw() { 718 | if (mStore->Poll()) { 719 | mLayerDataIsStale = true; 720 | } 721 | if (mLayerDataIsStale) { 722 | this->ReloadLayerDataNow(); 723 | } 724 | 725 | if (mLintErrorsAreStale) { 726 | this->RunAllLintersNow(); 727 | } 728 | 729 | this->GUILayersList(); 730 | ImGui::SameLine(); 731 | this->GUIButtons(); 732 | 733 | ImGui::SetNextItemWidth(-FLT_MIN); 734 | this->GUITabs(); 735 | } 736 | 737 | void GUI::Export() { 738 | const auto path = PlatformGUI::Get().GetExportFilePath(); 739 | if (!path) { 740 | return; 741 | } 742 | 743 | SaveReport(*path); 744 | 745 | if (std::filesystem::exists(*path)) { 746 | PlatformGUI::Get().ShowFolderContainingFile(*path); 747 | } 748 | } 749 | 750 | }// namespace FredEmmott::OpenXRLayers 751 | -------------------------------------------------------------------------------- /src/GUI.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #include "APILayer.hpp" 16 | #include "Linter.hpp" 17 | 18 | namespace FredEmmott::OpenXRLayers { 19 | 20 | class APILayerStore; 21 | class ReadWriteAPILayerStore; 22 | 23 | struct DPIChangeInfo { 24 | float mDPIScaling {}; 25 | std::optional mRecommendedSize; 26 | }; 27 | 28 | // Platform-specific functions implemented in PlatformGUI_P*.cpp 29 | class PlatformGUI { 30 | public: 31 | static PlatformGUI& Get(); 32 | virtual ~PlatformGUI() = default; 33 | 34 | virtual void BeginFrame() = 0; 35 | 36 | virtual void SetWindow(sf::WindowHandle) = 0; 37 | 38 | virtual std::vector GetNewAPILayerJSONPaths() = 0; 39 | virtual std::optional GetExportFilePath() = 0; 40 | virtual void SetupFonts(ImGuiIO*) = 0; 41 | virtual float GetDPIScaling() = 0; 42 | virtual std::optional GetDPIChangeInfo() = 0; 43 | 44 | // Use OS/environment equivalent to Explorer 45 | virtual void ShowFolderContainingFile(const std::filesystem::path&) = 0; 46 | 47 | PlatformGUI(const PlatformGUI&) = delete; 48 | PlatformGUI(PlatformGUI&&) = delete; 49 | PlatformGUI& operator=(const PlatformGUI&) = delete; 50 | PlatformGUI& operator=(PlatformGUI&&) = delete; 51 | 52 | protected: 53 | PlatformGUI() = default; 54 | }; 55 | 56 | // The actual app GUI 57 | class GUI final { 58 | public: 59 | void Run(); 60 | 61 | private: 62 | using LintErrors = std::vector>; 63 | 64 | class LayerSet { 65 | public: 66 | std::type_identity_t* mStore {nullptr}; 67 | 68 | std::vector mLayers; 69 | APILayer* mSelectedLayer {nullptr}; 70 | LintErrors mLintErrors; 71 | bool mLayerDataIsStale {true}; 72 | bool mLintErrorsAreStale {true}; 73 | 74 | bool HasErrors(); 75 | 76 | void Draw(); 77 | 78 | void GUILayersList(); 79 | 80 | void GUIButtons(); 81 | void GUIRemoveLayerPopup(); 82 | 83 | void GUITabs(); 84 | void GUIErrorsTab(); 85 | void GUIDetailsTab(); 86 | 87 | // This should only be called at the top of the frame loop; set 88 | // mLayerDataIsStale instead. 89 | void ReloadLayerDataNow(); 90 | // Set mLayerDataIsStale or mLintErrorsAreStale instead 91 | void RunAllLintersNow(); 92 | 93 | void AddLayersClicked(); 94 | void DragDropReorder(const APILayer& source, const APILayer& target); 95 | }; 96 | 97 | void Export(); 98 | 99 | sf::WindowHandle mWindowHandle {}; 100 | }; 101 | 102 | }// namespace FredEmmott::OpenXRLayers 103 | -------------------------------------------------------------------------------- /src/GetActiveRuntimePath.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | namespace FredEmmott::OpenXRLayers { 9 | 10 | /** Return the path of the active runtime. 11 | * 12 | * Will return an empty path if the active runtime can not be determined. 13 | */ 14 | std::filesystem::path GetActiveRuntimePath(); 15 | 16 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/LayerRules.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #include "LayerRules.hpp" 5 | 6 | namespace FredEmmott::OpenXRLayers { 7 | 8 | inline namespace LayerIDs { 9 | #define DEFINE_LAYER_ID(x) static constexpr LayerID x {#x}; 10 | DEFINE_LAYER_ID(XR_APILAYER_FREDEMMOTT_HandTrackedCockpitClicking) 11 | DEFINE_LAYER_ID(XR_APILAYER_FREDEMMOTT_OpenKneeboard) 12 | DEFINE_LAYER_ID(XR_APILAYER_MBUCCHIA_quad_views_foveated) 13 | DEFINE_LAYER_ID(XR_APILAYER_MBUCCHIA_toolkit) 14 | DEFINE_LAYER_ID(XR_APILAYER_MBUCCHIA_varjo_foveated) 15 | DEFINE_LAYER_ID(XR_APILAYER_MBUCCHIA_vulkan_d3d12_interop) 16 | DEFINE_LAYER_ID(XR_APILAYER_NOVENDOR_motion_compensation) 17 | DEFINE_LAYER_ID(XR_APILAYER_NOVENDOR_OBSMirror) 18 | DEFINE_LAYER_ID(XR_APILAYER_NOVENDOR_XRNeckSafer) 19 | DEFINE_LAYER_ID(XR_APILAYER_app_racelab_Overlay) 20 | #undef DEFINE_LAYER_ID 21 | }// namespace LayerIDs 22 | 23 | inline namespace ExtensionIDs { 24 | #define DEFINE_EXTENSION_ID(x) static constexpr ExtensionID x {#x}; 25 | DEFINE_EXTENSION_ID(XR_EXT_eye_gaze_interaction) 26 | DEFINE_EXTENSION_ID(XR_EXT_hand_tracking) 27 | DEFINE_EXTENSION_ID(XR_VARJO_foveated_rendering) 28 | #undef DEFINE_EXTENSION_ID 29 | }// namespace ExtensionIDs 30 | 31 | namespace Facets { 32 | #define DEFINE_FACET(name, description) \ 33 | static constexpr Facet name {"#" #name, description}; 34 | DEFINE_FACET(CompositionLayers, "provides an overlay") 35 | DEFINE_FACET(TransformsPoses, "modifies poses") 36 | DEFINE_FACET(UsesGameWorldPoses, "uses poses") 37 | #undef DEFINE_FACET 38 | 39 | }// namespace Facets 40 | 41 | namespace { 42 | struct Literals { 43 | Literals() = delete; 44 | template 45 | explicit Literals(Args&&... args) noexcept { 46 | mRet = {{Facet {args}, {}}...}; 47 | } 48 | 49 | operator FacetMap() const {// NOLINT(*-explicit-constructor) 50 | return mRet; 51 | } 52 | 53 | private: 54 | FacetMap mRet; 55 | }; 56 | }// namespace 57 | 58 | std::vector GetLayerRules() { 59 | return { 60 | { 61 | .mID = Facets::TransformsPoses, 62 | .mBelow = Literals { 63 | Facets::UsesGameWorldPoses, 64 | }, 65 | }, 66 | { 67 | .mID = XR_APILAYER_FREDEMMOTT_HandTrackedCockpitClicking, 68 | .mAbove = Literals { 69 | XR_EXT_hand_tracking, 70 | }, 71 | }, 72 | { 73 | .mID = XR_APILAYER_FREDEMMOTT_OpenKneeboard, 74 | .mFacets = Literals { 75 | Facets::CompositionLayers, 76 | Facets::UsesGameWorldPoses, 77 | }, 78 | }, 79 | { 80 | .mID = XR_APILAYER_app_racelab_Overlay, 81 | .mFacets = Literals { 82 | Facets::CompositionLayers, 83 | Facets::UsesGameWorldPoses, 84 | }, 85 | }, 86 | { 87 | .mID = XR_APILAYER_MBUCCHIA_quad_views_foveated, 88 | .mAbove = Literals { 89 | XR_EXT_eye_gaze_interaction, 90 | }, 91 | }, 92 | { 93 | .mID = XR_APILAYER_MBUCCHIA_toolkit, 94 | .mAbove = Literals { 95 | XR_EXT_eye_gaze_interaction, 96 | XR_EXT_hand_tracking, 97 | }, 98 | .mBelow = Literals { 99 | XR_VARJO_foveated_rendering, 100 | }, 101 | .mFacets = Literals { 102 | Facets::CompositionLayers, 103 | }, 104 | .mConflictsPerApp = Literals { 105 | XR_APILAYER_MBUCCHIA_varjo_foveated, 106 | }, 107 | }, 108 | { 109 | .mID = XR_APILAYER_NOVENDOR_motion_compensation, 110 | .mAbove = Literals { 111 | // Unknown incompatibility issue: 112 | XR_APILAYER_FREDEMMOTT_HandTrackedCockpitClicking, 113 | }, 114 | .mFacets = Literals { 115 | Facets::TransformsPoses, 116 | }, 117 | }, 118 | { 119 | .mID = XR_APILAYER_MBUCCHIA_vulkan_d3d12_interop, 120 | .mAbove = Literals { 121 | // Incompatible with Vulkan: 122 | XR_APILAYER_MBUCCHIA_toolkit, 123 | XR_APILAYER_NOVENDOR_OBSMirror, 124 | }, 125 | }, 126 | { 127 | .mID = XR_APILAYER_NOVENDOR_OBSMirror, 128 | .mBelow = Literals { 129 | Facets::CompositionLayers, 130 | XR_VARJO_foveated_rendering, 131 | }, 132 | }, 133 | { 134 | .mID = XR_APILAYER_NOVENDOR_XRNeckSafer, 135 | .mAbove = Literals { 136 | // Unknown incompatibility issue: 137 | XR_APILAYER_FREDEMMOTT_HandTrackedCockpitClicking, 138 | // - https://gitlab.com/NobiWan/xrnecksafer/-/issues/15 139 | // - https://gitlab.com/NobiWan/xrnecksafer/-/issues/16 140 | // - Other developers have mentioned thread safety issues in XRNS that can cause crashes; I've not confirmed these 141 | Facets::CompositionLayers, 142 | }, 143 | }, 144 | }; 145 | } 146 | 147 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/LayerRules.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "ConstexprString.hpp" 12 | #include "StringTemplateParameter.hpp" 13 | 14 | namespace FredEmmott::OpenXRLayers { 15 | 16 | class Facet { 17 | public: 18 | enum class Kind { 19 | Explicit, 20 | Layer, 21 | Extension, 22 | }; 23 | 24 | Facet() = delete; 25 | constexpr Facet( 26 | const std::string_view& id, 27 | const std::string_view& description) 28 | : Facet(Kind::Explicit, id, description) { 29 | } 30 | 31 | constexpr Facet( 32 | const Kind kind, 33 | const std::string_view& id, 34 | const std::string_view& description) 35 | : mKind(kind), mID(id), mDescription(description) { 36 | } 37 | 38 | [[nodiscard]] constexpr auto GetKind() const noexcept { 39 | return mKind; 40 | } 41 | 42 | [[nodiscard]] constexpr auto GetID() const noexcept { 43 | return mID.Get(); 44 | } 45 | 46 | [[nodiscard]] constexpr std::string_view GetDescription() const noexcept { 47 | return mDescription.Get(); 48 | } 49 | 50 | [[nodiscard]] constexpr bool operator==(const Facet&) const noexcept 51 | = default; 52 | 53 | struct Hash { 54 | auto operator()(const Facet& facet) const noexcept { 55 | return std::hash {}(facet.GetID()); 56 | } 57 | }; 58 | 59 | private: 60 | Kind mKind; 61 | ConstexprString mID; 62 | ConstexprString mDescription; 63 | }; 64 | 65 | template 66 | class BasicFacetID { 67 | public: 68 | BasicFacetID() = delete; 69 | explicit constexpr BasicFacetID(const std::string_view id) : mID(id) { 70 | } 71 | 72 | explicit constexpr BasicFacetID(const Facet& facet) : mID(facet.GetID()) { 73 | assert(facet.GetKind() == TKind); 74 | } 75 | 76 | [[nodiscard]] constexpr auto GetID() const noexcept { 77 | return mID.Get(); 78 | } 79 | 80 | // ReSharper disable once CppNonExplicitConversionOperator 81 | constexpr operator Facet() const noexcept {// NOLINT(*-explicit-constructor) 82 | return Facet { 83 | TKind, mID.Get(), std::format(TDescriptionFormat.value, mID.Get())}; 84 | } 85 | 86 | [[nodiscard]] constexpr bool operator==(const BasicFacetID&) const noexcept 87 | = default; 88 | 89 | [[nodiscard]] constexpr bool operator==(const Facet& facet) const noexcept { 90 | return facet.GetKind() == TKind && facet.GetID() == mID.Get(); 91 | } 92 | 93 | private: 94 | ConstexprString mID; 95 | }; 96 | 97 | using LayerID = BasicFacetID; 98 | using ExtensionID = BasicFacetID; 99 | 100 | struct FacetTraceEntry { 101 | Facet mWhat; 102 | Facet mWhy; 103 | constexpr bool operator==(const FacetTraceEntry&) const noexcept = default; 104 | }; 105 | 106 | // Really want a stack, but std::stack isn't iterable 107 | using FacetTrace = std::deque; 108 | using FacetMap = std::unordered_map; 109 | 110 | struct LayerRules { 111 | const Facet mID; 112 | /* Features that should be below this layer. 113 | * A 'feature' can include: 114 | * - an API layer name 115 | * - an extension name 116 | * - an explicit feature ID 117 | */ 118 | FacetMap mAbove; 119 | 120 | /* Features that should be above this layer. 121 | * 122 | * @see mAbove 123 | */ 124 | FacetMap mBelow; 125 | 126 | /* Features that this layer provides, in addition to its name and extensions. 127 | * 128 | * This should only include constants from the Features namespace; extensions 129 | * should be specified in the OpenXR JSON manifest file, not here. 130 | */ 131 | FacetMap mFacets; 132 | 133 | /* Features (usually other layers) that this layer is completely 134 | * incompatible with. */ 135 | FacetMap mConflicts; 136 | 137 | /* Features (usually other layers) that this layer is completely 138 | * incompatible with, but one or both support enabling/disabling per game. */ 139 | FacetMap mConflictsPerApp; 140 | }; 141 | 142 | std::vector GetLayerRules(); 143 | 144 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/Linter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #include "Linter.hpp" 5 | 6 | #include 7 | #include 8 | 9 | namespace FredEmmott::OpenXRLayers { 10 | 11 | static std::set gLinters; 12 | 13 | static void RegisterLinter(Linter* linter) { 14 | gLinters.emplace(linter); 15 | } 16 | static void UnregisterLinter(Linter* linter) { 17 | gLinters.erase(linter); 18 | } 19 | 20 | LintError::LintError( 21 | const std::string& description, 22 | const std::set& affectedLayers) 23 | : mDescription(description), mAffectedLayers(affectedLayers) { 24 | } 25 | 26 | std::string LintError::GetDescription() const { 27 | return mDescription; 28 | } 29 | 30 | PathSet LintError::GetAffectedLayers() const { 31 | return mAffectedLayers; 32 | } 33 | 34 | Linter::Linter() { 35 | RegisterLinter(this); 36 | } 37 | 38 | Linter::~Linter() { 39 | UnregisterLinter(this); 40 | } 41 | 42 | std::vector> RunAllLinters( 43 | const APILayerStore* store, 44 | const std::vector& layers) { 45 | std::vector> errors; 46 | 47 | std::vector> layersWithDetails; 48 | for (const auto& layer: layers) { 49 | layersWithDetails.push_back({layer, {layer.mJSONPath}}); 50 | } 51 | 52 | auto it = std::back_inserter(errors); 53 | for (const auto linter: gLinters) { 54 | std::ranges::move(linter->Lint(store, layersWithDetails), it); 55 | } 56 | 57 | return errors; 58 | } 59 | 60 | OrderingLintError::OrderingLintError( 61 | const std::string& description, 62 | const std::filesystem::path& layerToMove, 63 | Position position, 64 | const std::filesystem::path& relativeTo, 65 | const PathSet& allAffectedLayers) 66 | : FixableLintError( 67 | description, 68 | allAffectedLayers.empty() ? PathSet {layerToMove, relativeTo} 69 | : allAffectedLayers), 70 | mLayerToMove(layerToMove), 71 | mPosition(position), 72 | mRelativeTo(relativeTo) { 73 | } 74 | 75 | std::vector OrderingLintError::Fix( 76 | const std::vector& oldLayers) { 77 | auto newLayers = oldLayers; 78 | 79 | auto moveIt = std::ranges::find_if( 80 | newLayers, [this](const auto& it) { return it.mJSONPath == mLayerToMove; }); 81 | 82 | if (moveIt == newLayers.end()) { 83 | return oldLayers; 84 | } 85 | const auto movedLayer = *moveIt; 86 | newLayers.erase(moveIt); 87 | 88 | auto anchorIt = std::ranges::find_if( 89 | newLayers, [this](const auto& it) { return it.mJSONPath == mRelativeTo; }); 90 | 91 | if (anchorIt == newLayers.end()) { 92 | return oldLayers; 93 | } 94 | 95 | switch (mPosition) { 96 | case Position::Above: 97 | newLayers.insert(anchorIt, movedLayer); 98 | break; 99 | case Position::Below: 100 | newLayers.insert(anchorIt + 1, movedLayer); 101 | break; 102 | } 103 | 104 | return newLayers; 105 | } 106 | 107 | KnownBadLayerLintError::KnownBadLayerLintError( 108 | const std::string& description, 109 | const std::filesystem::path& layer) 110 | : FixableLintError(description, {layer}) { 111 | } 112 | 113 | std::vector KnownBadLayerLintError::Fix( 114 | const std::vector& allLayers) { 115 | const auto affected = this->GetAffectedLayers(); 116 | assert(affected.size() == 1); 117 | const auto& path = *affected.begin(); 118 | 119 | auto newLayers = allLayers; 120 | auto it = std::ranges::find_if( 121 | newLayers, [&path](const auto& layer) { return layer.mJSONPath == path; }); 122 | 123 | if (it != newLayers.end()) { 124 | it->mValue = APILayer::Value::Disabled; 125 | } 126 | return newLayers; 127 | } 128 | 129 | InvalidLayerLintError::InvalidLayerLintError( 130 | const std::string& description, 131 | const std::filesystem::path& layer) 132 | : FixableLintError(description, {layer}) { 133 | } 134 | 135 | std::vector InvalidLayerLintError::Fix( 136 | const std::vector& allLayers) { 137 | const auto affected = this->GetAffectedLayers(); 138 | assert(affected.size() == 1); 139 | const auto& path = *affected.begin(); 140 | 141 | auto newLayers = allLayers; 142 | auto it = std::ranges::find_if( 143 | newLayers, [&path](const auto& layer) { return layer.mJSONPath == path; }); 144 | 145 | if (it != newLayers.end()) { 146 | newLayers.erase(it); 147 | } 148 | return newLayers; 149 | } 150 | 151 | InvalidLayerStateLintError::InvalidLayerStateLintError( 152 | const std::string& description, 153 | const std::filesystem::path& layer) 154 | : FixableLintError(description, {layer}) { 155 | } 156 | 157 | std::vector InvalidLayerStateLintError::Fix( 158 | const std::vector& allLayers) { 159 | const auto affected = this->GetAffectedLayers(); 160 | assert(affected.size() == 1); 161 | const auto& path = *affected.begin(); 162 | 163 | auto newLayers = allLayers; 164 | auto it = std::ranges::find_if( 165 | newLayers, [&path](const auto& layer) { return layer.mJSONPath == path; }); 166 | 167 | if (it != newLayers.end()) { 168 | it->mValue = APILayer::Value::Disabled; 169 | } 170 | 171 | return newLayers; 172 | } 173 | 174 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/Linter.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "APILayer.hpp" 13 | 14 | namespace FredEmmott::OpenXRLayers { 15 | 16 | using PathSet = std::set; 17 | 18 | class APILayerStore; 19 | 20 | class LintError { 21 | public: 22 | LintError() = delete; 23 | LintError(const std::string& description, const PathSet& affectedLayers); 24 | virtual ~LintError() = default; 25 | 26 | [[nodiscard]] std::string GetDescription() const; 27 | [[nodiscard]] PathSet GetAffectedLayers() const; 28 | 29 | private: 30 | std::string mDescription; 31 | PathSet mAffectedLayers; 32 | }; 33 | 34 | // A lint error that can be automatically fixed 35 | class FixableLintError : public LintError { 36 | public: 37 | using LintError::LintError; 38 | ~FixableLintError() override = default; 39 | 40 | virtual std::vector Fix(const std::vector&) = 0; 41 | }; 42 | 43 | // A lint error that can be fixed by reordering layers 44 | class OrderingLintError final : public FixableLintError { 45 | public: 46 | enum class Position { 47 | Above, 48 | Below, 49 | }; 50 | 51 | OrderingLintError( 52 | const std::string& description, 53 | const std::filesystem::path& layerToMove, 54 | Position position, 55 | const std::filesystem::path& relativeTo, 56 | const PathSet& allAffectedLayers = {}); 57 | virtual ~OrderingLintError() = default; 58 | 59 | virtual std::vector Fix(const std::vector&) override; 60 | 61 | private: 62 | std::filesystem::path mLayerToMove; 63 | Position mPosition; 64 | std::filesystem::path mRelativeTo; 65 | }; 66 | 67 | /// A lint error that is fixed by disabling the layer 68 | class KnownBadLayerLintError final : public FixableLintError { 69 | public: 70 | KnownBadLayerLintError( 71 | const std::string& description, 72 | const std::filesystem::path& layer); 73 | 74 | ~KnownBadLayerLintError() override = default; 75 | std::vector Fix(const std::vector&) override; 76 | }; 77 | 78 | /// A lint error that is fixed by removing the layer 79 | class InvalidLayerLintError final : public FixableLintError { 80 | public: 81 | InvalidLayerLintError( 82 | const std::string& description, 83 | const std::filesystem::path& layer); 84 | virtual ~InvalidLayerLintError() = default; 85 | 86 | virtual std::vector Fix(const std::vector&) override; 87 | }; 88 | 89 | // A lint error that is fixed by disabling the layer 90 | class InvalidLayerStateLintError final : public FixableLintError { 91 | public: 92 | InvalidLayerStateLintError( 93 | const std::string& description, 94 | const std::filesystem::path& layer); 95 | virtual ~InvalidLayerStateLintError() = default; 96 | 97 | virtual std::vector Fix(const std::vector&) override; 98 | }; 99 | 100 | class Linter { 101 | protected: 102 | Linter(); 103 | 104 | public: 105 | virtual ~Linter(); 106 | 107 | virtual std::vector> Lint( 108 | const APILayerStore*, 109 | const std::vector>&) 110 | = 0; 111 | }; 112 | 113 | std::vector> RunAllLinters( 114 | const APILayerStore*, 115 | const std::vector&); 116 | 117 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/Runtime.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #pragma once 5 | 6 | #include "APILayer.hpp" 7 | 8 | namespace FredEmmott::OpenXRLayers { 9 | 10 | struct Runtime : public APILayer {}; 11 | 12 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/SaveReport.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #include "SaveReport.hpp" 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "APILayerStore.hpp" 14 | #include "Config.hpp" 15 | #include "GetActiveRuntimePath.hpp" 16 | #include "Linter.hpp" 17 | 18 | namespace FredEmmott::OpenXRLayers { 19 | 20 | static std::string GenerateReportText(const APILayerStore* store) { 21 | auto ret = std::format( 22 | "\n--------------------------------\n" 23 | "{}\n" 24 | "--------------------------------", 25 | store->GetDisplayName()); 26 | const auto layers = store->GetAPILayers(); 27 | if (layers.empty()) { 28 | ret += "\nNo layers."; 29 | return ret; 30 | } 31 | 32 | const auto errors = RunAllLinters(store, layers); 33 | 34 | for (const auto& layer: layers) { 35 | using Value = APILayer::Value; 36 | std::string_view value; 37 | switch (layer.mValue) { 38 | case Value::Enabled: 39 | value = Config::GLYPH_ENABLED; 40 | break; 41 | case Value::Disabled: 42 | value = Config::GLYPH_DISABLED; 43 | break; 44 | default: 45 | value = Config::GLYPH_ERROR; 46 | break; 47 | } 48 | ret += std::format("\n{} {}", value, layer.mJSONPath.string()); 49 | 50 | const APILayerDetails details {layer.mJSONPath}; 51 | if (details.mState != APILayerDetails::State::Loaded) { 52 | ret += fmt::format( 53 | "\n\t- {} {}", Config::GLYPH_ERROR, details.StateAsString()); 54 | } else { 55 | if (!details.mName.empty()) { 56 | ret += fmt::format("\n\tName: {}", details.mName); 57 | } 58 | 59 | if (details.mLibraryPath.empty()) { 60 | ret += fmt::format( 61 | "\n\tLibrary path: {} No library path in JSON file", 62 | Config::GLYPH_ERROR); 63 | } else { 64 | ret 65 | += fmt::format("\n\tLibrary path: {}", details.mLibraryPath.string()); 66 | } 67 | 68 | if (!details.mImplementationVersion.empty()) { 69 | ret += fmt::format( 70 | "\n\tImplementation version: {}", details.mImplementationVersion); 71 | } 72 | 73 | if (!details.mAPIVersion.empty()) { 74 | ret += fmt::format("\n\tOpenXR API version: {}", details.mAPIVersion); 75 | } 76 | 77 | if (!details.mDescription.empty()) { 78 | ret += fmt::format("\n\tDescription: {}", details.mDescription); 79 | } 80 | 81 | if (!details.mFileFormatVersion.empty()) { 82 | ret += fmt::format( 83 | "\n\tFile format version: {}", details.mFileFormatVersion); 84 | } 85 | 86 | if (!details.mExtensions.empty()) { 87 | ret += "\n\tExtensions:"; 88 | for (const auto& ext: details.mExtensions) { 89 | ret 90 | += fmt::format("\n\t\t- {} (version {})", ext.mName, ext.mVersion); 91 | } 92 | } 93 | } 94 | 95 | auto layerErrors 96 | = std::ranges::filter_view(errors, [layer](const auto& error) { 97 | return error->GetAffectedLayers().contains(layer.mJSONPath); 98 | }); 99 | 100 | if (layerErrors.empty()) { 101 | if (layer.IsEnabled()) { 102 | ret += "\n\tNo errors."; 103 | } else { 104 | ret += "\n\tNo errors, however most linters were skipped because the layer is disabled."; 105 | } 106 | } else { 107 | ret += "\n\tErrors:"; 108 | for (const auto& error: layerErrors) { 109 | ret += fmt::format( 110 | "\n\t\t- {} {}", Config::GLYPH_ERROR, error->GetDescription()); 111 | } 112 | } 113 | } 114 | return ret; 115 | } 116 | 117 | void SaveReport(const std::filesystem::path& path) { 118 | auto text = std::format( 119 | "OpenXR API Layers GUI v{}\n" 120 | "Reported generated at {:%Y-%m-%d %H:%M:%S}", 121 | Config::BUILD_VERSION, 122 | std::chrono::zoned_time( 123 | std::chrono::current_zone(), std::chrono::system_clock::now())); 124 | 125 | const auto runtime = GetActiveRuntimePath(); 126 | if (runtime.empty()) { 127 | text 128 | += std::format("\n\n{} NO ACTIVE RUNTIME FOUND\n", Config::GLYPH_ERROR); 129 | } else { 130 | text += std::format("\n\nActive runtime: {}\n", runtime.string()); 131 | } 132 | 133 | for (const auto store: APILayerStore::Get()) { 134 | text += GenerateReportText(store); 135 | } 136 | 137 | std::ofstream(path, std::ios::binary | std::ios::out | std::ios::trunc) 138 | .write(text.data(), text.size()); 139 | } 140 | 141 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/SaveReport.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | namespace FredEmmott::OpenXRLayers { 9 | 10 | void SaveReport(const std::filesystem::path&); 11 | 12 | } -------------------------------------------------------------------------------- /src/StringTemplateParameter.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | #pragma once 4 | 5 | #include 6 | 7 | namespace FredEmmott::OpenXRLayers { 8 | 9 | /** Class containing a compile-time string literal, which is usable as a 10 | * template parameter. 11 | * 12 | * C++ doesn't allow string literals or `std::string_view`'s as template 13 | * parameters, so we need a helper. 14 | * 15 | * Usage: 16 | * 17 | * "foo"_tp 18 | */ 19 | template 20 | struct StringTemplateParameter { 21 | StringTemplateParameter() = delete; 22 | // ReSharper disable once CppNonExplicitConvertingConstructor 23 | consteval StringTemplateParameter(char const (&init)[N]) { 24 | std::ranges::copy(init, value); 25 | } 26 | 27 | char value[N] {}; 28 | }; 29 | 30 | template <> 31 | struct StringTemplateParameter<0> {}; 32 | 33 | /// Make a string literal usable as a template parameter 34 | template 35 | consteval auto operator""_tp() { 36 | return T; 37 | } 38 | 39 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/linters/BadInstallationLinter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "Linter.hpp" 9 | 10 | namespace FredEmmott::OpenXRLayers { 11 | 12 | // Detect API layers with missing files or invalid JSON 13 | class BadInstallationLinter final : public Linter { 14 | virtual std::vector> Lint( 15 | const APILayerStore*, 16 | const std::vector>& layers) { 17 | std::vector> errors; 18 | for (const auto& [layer, details]: layers) { 19 | if (!std::filesystem::exists(layer.mJSONPath)) { 20 | errors.push_back(std::make_shared( 21 | fmt::format( 22 | "JSON file `{}` does not exist", layer.mJSONPath.string()), 23 | layer.mJSONPath)); 24 | continue; 25 | } 26 | 27 | if (details.mState != APILayerDetails::State::Loaded) { 28 | errors.push_back(std::make_shared( 29 | fmt::format( 30 | "Unable to load details from the JSON file `{}`", 31 | layer.mJSONPath.string()), 32 | layer.mJSONPath)); 33 | continue; 34 | } 35 | 36 | if (details.mLibraryPath.empty()) { 37 | errors.push_back(std::make_shared( 38 | fmt::format( 39 | "Layer does not specify an implementation in `{}`", 40 | layer.mJSONPath.string()), 41 | layer.mJSONPath)); 42 | continue; 43 | } 44 | 45 | if (!std::filesystem::exists(details.mLibraryPath)) { 46 | errors.push_back(std::make_shared( 47 | fmt::format( 48 | "Implementation file `{}` does not exist", 49 | details.mLibraryPath.string()), 50 | layer.mJSONPath)); 51 | continue; 52 | } 53 | } 54 | 55 | return errors; 56 | } 57 | }; 58 | 59 | static BadInstallationLinter gInstance; 60 | 61 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/linters/DuplicatesLinter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include "Linter.hpp" 10 | 11 | namespace FredEmmott::OpenXRLayers { 12 | 13 | // Detect multiple enabled versions of the same layer 14 | class DuplicatesLinter final : public Linter { 15 | public: 16 | virtual std::vector> Lint( 17 | const APILayerStore*, 18 | const std::vector>& layers) { 19 | std::unordered_map byName; 20 | for (const auto& [layer, details]: layers) { 21 | if (!layer.IsEnabled()) { 22 | continue; 23 | } 24 | 25 | if (details.mState != APILayerDetails::State::Loaded) { 26 | continue; 27 | } 28 | 29 | if (byName.contains(details.mName)) { 30 | byName.at(details.mName).emplace(layer.mJSONPath); 31 | } else { 32 | byName[details.mName] = {layer.mJSONPath}; 33 | } 34 | } 35 | 36 | std::vector> errors; 37 | for (const auto& [name, paths]: byName) { 38 | if (paths.size() == 1) { 39 | continue; 40 | } 41 | 42 | auto text = fmt::format("Multiple copies of {} are enabled:", name); 43 | for (const auto& path: paths) { 44 | text += fmt::format("\n- {}", path.string()); 45 | } 46 | 47 | errors.push_back(std::make_shared(text, paths)); 48 | } 49 | return errors; 50 | } 51 | }; 52 | 53 | static DuplicatesLinter gInstance; 54 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/linters/OrderingLinter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "LayerRules.hpp" 12 | #include "Linter.hpp" 13 | #include "std23/ranges.hpp" 14 | 15 | namespace FredEmmott::OpenXRLayers { 16 | 17 | using LayerExtensions = std::unordered_map< 18 | LayerID, 19 | std::unordered_set, 20 | Facet::Hash>; 21 | 22 | static FacetMap ExpandFacets( 23 | const FacetMap& facets, 24 | const LayerExtensions& layers, 25 | const std::vector& rules) { 26 | FacetMap next; 27 | for (auto&& [facet, trace]: facets) { 28 | switch (facet.GetKind()) { 29 | case Facet::Kind::Layer: 30 | next.emplace(facet, trace); 31 | break; 32 | case Facet::Kind::Extension: 33 | for (auto&& [layer, extensions]: layers) { 34 | if (std23::ranges::contains(extensions, ExtensionID {facet})) { 35 | auto nextTrace = trace; 36 | nextTrace.push_front({layer, facet}); 37 | next.emplace(layer, nextTrace); 38 | } 39 | } 40 | break; 41 | case Facet::Kind::Explicit: 42 | for (auto&& rule: rules) { 43 | if (std23::ranges::contains(rule.mFacets | std::views::keys, facet)) { 44 | auto nextTrace = trace; 45 | nextTrace.push_front({rule.mID, facet}); 46 | next.emplace(rule.mID, nextTrace); 47 | } 48 | } 49 | break; 50 | } 51 | } 52 | 53 | if (next == facets) { 54 | for (const auto& it: facets | std::views::keys) { 55 | assert(it.GetKind() == Facet::Kind::Layer); 56 | } 57 | return next; 58 | } 59 | 60 | return ExpandFacets(next, layers, rules); 61 | } 62 | 63 | static FacetMap ExpandFacets( 64 | const LayerRules& rule, 65 | auto proj, 66 | const LayerExtensions& layers, 67 | const std::vector& rules) { 68 | FacetMap toExpand = std::invoke(proj, rule); 69 | 70 | for (auto&& mixin: rule.mFacets | std::views::keys) { 71 | auto mixinIt = std::ranges::find(rules, mixin, &LayerRules::mID); 72 | if (mixinIt == rules.end()) { 73 | continue; 74 | } 75 | const FacetMap& mixinValues = std::invoke(proj, *mixinIt); 76 | if (mixinValues.empty()) { 77 | continue; 78 | } 79 | 80 | for (auto& value: mixinValues | std::views::keys) { 81 | toExpand.emplace( 82 | value, 83 | FacetTrace { 84 | {rule.mID, mixin}, 85 | }); 86 | } 87 | } 88 | 89 | return ExpandFacets(toExpand, layers, rules); 90 | } 91 | 92 | /** Replace Extension and Explicit facets with the Layers. 93 | * 94 | * The original Facets are retained in the trace. 95 | */ 96 | static std::vector ExpandRules( 97 | const std::vector& rules, 98 | const std::vector>& layers) { 99 | LayerExtensions layerExtensions; 100 | for (auto&& [_, details]: layers) { 101 | layerExtensions.emplace( 102 | LayerID {details.mName}, 103 | details.mExtensions | std::views::transform([](auto& ext) { 104 | return ExtensionID {ext.mName}; 105 | }) | std23::ranges::to()); 106 | } 107 | 108 | auto ret = rules | std::views::filter([](auto& it) { 109 | return it.mID.GetKind() == Facet::Kind::Layer; 110 | }) 111 | | std23::ranges::to(); 112 | auto expandFacets = [&](LayerRules& it, auto proj) { 113 | FacetMap& facets = std::invoke(proj, it); 114 | facets = ExpandFacets(it, proj, layerExtensions, rules); 115 | }; 116 | for (auto& it: ret) { 117 | expandFacets(it, &LayerRules::mAbove); 118 | expandFacets(it, &LayerRules::mBelow); 119 | expandFacets(it, &LayerRules::mConflicts); 120 | expandFacets(it, &LayerRules::mConflictsPerApp); 121 | } 122 | return ret; 123 | } 124 | 125 | static std::string ExplainTrace(const FacetTrace& trace) { 126 | if (trace.empty()) { 127 | return {}; 128 | } 129 | 130 | if (trace.size() == 1) { 131 | return std::format("because it {}", trace.front().mWhy.GetDescription()); 132 | } else { 133 | std::string traceStr; 134 | const auto reverseTrace = trace | std::views::reverse; 135 | for (auto it = reverseTrace.begin(); it != reverseTrace.end(); ++it) { 136 | if (it != reverseTrace.begin()) { 137 | traceStr 138 | += (std::ranges::next(it) == reverseTrace.end()) ? ", and " : ", "; 139 | } 140 | 141 | const auto& [what, why] = *it; 142 | traceStr 143 | += std::format("{} {}", what.GetDescription(), why.GetDescription()); 144 | } 145 | return std::format("because {}", traceStr); 146 | } 147 | } 148 | 149 | static auto MakeOrderingLintError( 150 | const std::tuple& layerToMove, 151 | OrderingLintError::Position position, 152 | const std::tuple& relativeTo, 153 | const FacetTrace& trace) { 154 | const auto toMoveName = std::get<1>(layerToMove).mName; 155 | const auto toMovePath = std::get<0>(layerToMove).mJSONPath; 156 | const auto relativeToName = std::get<1>(relativeTo).mName; 157 | const auto relativeToPath = std::get<0>(relativeTo).mJSONPath; 158 | 159 | auto msg = std::format( 160 | "{} ({}) must be {} {} ({})", 161 | toMoveName, 162 | toMovePath.string(), 163 | position == OrderingLintError::Position::Above ? "above" : "below", 164 | relativeToName, 165 | relativeToPath.string()); 166 | if (!trace.empty()) { 167 | msg += std::format(" {}.", ExplainTrace(trace)); 168 | } else { 169 | msg += "."; 170 | } 171 | 172 | return std::make_shared( 173 | msg, toMovePath, position, relativeToPath); 174 | } 175 | 176 | // Detect dependencies 177 | class OrderingLinter final : public Linter { 178 | public: 179 | std::vector> Lint( 180 | const APILayerStore*, 181 | const std::vector>& allLayers) 182 | override { 183 | const auto layers 184 | = allLayers | std::views::filter([](const auto& it) { 185 | const auto& [layer, details] = it; 186 | if (!layer.IsEnabled()) { 187 | return false; 188 | } 189 | if (details.mState != APILayerDetails::State::Loaded) { 190 | return false; 191 | } 192 | return true; 193 | }) 194 | | std23::ranges::to(); 195 | 196 | std::vector> errors; 197 | 198 | const auto rules = ExpandRules(GetLayerRules(), layers); 199 | 200 | std::unordered_map layerIndices; 201 | for (auto&& [_, details]: layers) { 202 | layerIndices.emplace(LayerID {details.mName}, layerIndices.size()); 203 | } 204 | 205 | for (auto&& [layer, details]: layers) { 206 | const LayerID layerID {details.mName}; 207 | const auto layerIndex = layerIndices.at(layerID); 208 | 209 | const auto rule 210 | = std::ranges::find(rules, Facet {layerID}, &LayerRules::mID); 211 | if (rule == rules.end()) { 212 | continue; 213 | } 214 | using Position = OrderingLintError::Position; 215 | 216 | // LINT RULE: Above 217 | for (auto&& [other, trace]: rule->mAbove) { 218 | const auto it = layerIndices.find(LayerID {other}); 219 | if (it == layerIndices.end()) { 220 | continue; 221 | } 222 | 223 | if (it->second > layerIndex) { 224 | continue; 225 | } 226 | errors.push_back(MakeOrderingLintError( 227 | {layer, details}, Position::Above, layers.at(it->second), trace)); 228 | } 229 | 230 | // LINT RULE: Below 231 | for (auto&& [facet, trace]: rule->mBelow) { 232 | const auto it = layerIndices.find(LayerID {facet}); 233 | if (it == layerIndices.end()) { 234 | continue; 235 | } 236 | 237 | if (it->second < layerIndex) { 238 | continue; 239 | } 240 | 241 | errors.push_back(MakeOrderingLintError( 242 | {layer, details}, Position::Below, layers.at(it->second), trace)); 243 | } 244 | 245 | // LINT RULE: Conflicts 246 | for (const auto& facet: rule->mConflicts | std::views::keys) { 247 | const auto otherIt 248 | = std::ranges::find(layers, facet.GetID(), [](const auto& it) { 249 | return std::get<1>(it).mName; 250 | }); 251 | if (otherIt == layers.end()) { 252 | continue; 253 | } 254 | 255 | const auto& [other, otherDetails] = *otherIt; 256 | errors.push_back(std::make_shared( 257 | fmt::format( 258 | "{} ({}) and {} ({}) are incompatible; you must remove or " 259 | "disable one.", 260 | details.mName, 261 | layer.mJSONPath.string(), 262 | otherDetails.mName, 263 | other.mJSONPath.string()), 264 | PathSet {layer.mJSONPath, other.mJSONPath})); 265 | } 266 | 267 | // LINT RULE: ConflictsPerApp 268 | for (const auto& facet: rule->mConflictsPerApp | std::views::keys) { 269 | const auto otherIt 270 | = std::ranges::find(layers, facet.GetID(), [](const auto& it) { 271 | return std::get<1>(it).mName; 272 | }); 273 | if (otherIt == layers.end()) { 274 | continue; 275 | } 276 | 277 | const auto& [other, otherDetails] = *otherIt; 278 | errors.push_back(std::make_shared( 279 | fmt::format( 280 | "{} ({}) and {} ({}) are incompatible; make sure that games using " 281 | "{} are disabled in {}.", 282 | details.mName, 283 | layer.mJSONPath.string(), 284 | otherDetails.mName, 285 | other.mJSONPath.string(), 286 | details.mName, 287 | otherDetails.mName), 288 | PathSet {layer.mJSONPath, other.mJSONPath})); 289 | } 290 | } 291 | 292 | return errors; 293 | } 294 | }; 295 | 296 | // Unused, but we care about the constructor 297 | [[maybe_unused]] static OrderingLinter gInstance; 298 | 299 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/linters/windows/NotADWORDLinter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #include 5 | 6 | #include "Linter.hpp" 7 | 8 | // Warn about a registry value that is not a DWORD 9 | namespace FredEmmott::OpenXRLayers { 10 | 11 | class NotADWORDLinter final : public Linter { 12 | virtual std::vector> Lint( 13 | const APILayerStore*, 14 | const std::vector>& layers) { 15 | std::vector> ret; 16 | for (const auto& [layer, details]: layers) { 17 | if (layer.mValue != APILayer::Value::Win32_NotDWORD) { 18 | continue; 19 | } 20 | ret.push_back(std::make_shared( 21 | fmt::format( 22 | "OpenXR requires that layer registry values are DWORDs; `{}` has a " 23 | "different type. This can cause various issues with other layers " 24 | "or games.", 25 | layer.mJSONPath.string()), 26 | layer.mJSONPath)); 27 | } 28 | return ret; 29 | } 30 | }; 31 | 32 | static NotADWORDLinter gInstance; 33 | 34 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/linters/windows/OpenXRToolkitLinter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "Linter.hpp" 9 | #include "windows/WindowsAPILayerStore.hpp" 10 | 11 | namespace FredEmmott::OpenXRLayers { 12 | 13 | class OpenXRToolkitLinter final : public Linter { 14 | virtual std::vector> Lint( 15 | const APILayerStore* store, 16 | const std::vector>& layers) { 17 | auto winStore = dynamic_cast(store); 18 | if ( 19 | winStore->GetRegistryBitness() 20 | != WindowsAPILayerStore::RegistryBitness::Wow64_64) { 21 | return {}; 22 | } 23 | 24 | std::vector> errors; 25 | for (const auto& [layer, details]: layers) { 26 | if (!layer.IsEnabled()) { 27 | continue; 28 | } 29 | if (details.mName != "XR_APILAYER_MBUCCHIA_toolkit") { 30 | continue; 31 | } 32 | errors.push_back(std::make_shared( 33 | "OpenXR Toolkit is unsupported, and is known to cause crashes and " 34 | "other issues in modern games; you should disable it if you encounter " 35 | "problems.", 36 | layer.mJSONPath)); 37 | } 38 | return errors; 39 | } 40 | }; 41 | 42 | static OpenXRToolkitLinter gInstance; 43 | 44 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/linters/windows/OutdatedOpenKneeboardLinter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "Linter.hpp" 9 | #include "windows/WindowsAPILayerStore.hpp" 10 | 11 | namespace FredEmmott::OpenXRLayers { 12 | 13 | // Warn about legacy versions which may indicate corrupted or outdaed 14 | // installations. 15 | // 16 | // These old versions used MSIX, which can lead to ACL issues. While 17 | // installing a new version will automatically clean these up, 18 | // it's then still possible to co-install an old msix afterwards. 19 | class OutdatedOpenKneeboardLinter final : public Linter { 20 | virtual std::vector> Lint( 21 | const APILayerStore* store, 22 | const std::vector>& layers) { 23 | auto winStore = dynamic_cast(store); 24 | if ( 25 | winStore->GetRegistryBitness() 26 | != WindowsAPILayerStore::RegistryBitness::Wow64_64) { 27 | return {}; 28 | } 29 | 30 | std::vector> errors; 31 | for (const auto& [layer, details]: layers) { 32 | bool outdated = false; 33 | if (details.mName == "XR_APILAYER_NOVENDOR_OpenKneeboard") { 34 | outdated = true; 35 | } else if (details.mName != "XR_APILAYER_FREDEMMOTT_OpenKneeboard") { 36 | continue; 37 | } 38 | 39 | if (winStore->GetRootKey() == HKEY_CURRENT_USER) { 40 | outdated = true; 41 | } 42 | 43 | if (!outdated) { 44 | const std::string path = details.mLibraryPath.string(); 45 | if (path.find("ProgramData") != std::string::npos) { 46 | outdated = true; 47 | } else if (path.find("WindowsApps") != std::string::npos) { 48 | outdated = true; 49 | } 50 | } 51 | 52 | if (!outdated) { 53 | continue; 54 | } 55 | 56 | errors.push_back(std::make_shared( 57 | fmt::format( 58 | "{} is from an extremely outdated version of OpenKneeboard, which " 59 | "may cause issues. Remove this API layer, install updates, and " 60 | "remove any left over old versions from 'Add or Remove Programs'.", 61 | layer.mJSONPath.string()), 62 | layer.mJSONPath)); 63 | } 64 | return errors; 65 | } 66 | }; 67 | 68 | static OutdatedOpenKneeboardLinter gInstance; 69 | 70 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/linters/windows/ProgramFilesLinter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include "Linter.hpp" 11 | #include "windows/GetKnownFolderPath.hpp" 12 | #include "windows/WindowsAPILayerStore.hpp" 13 | 14 | namespace FredEmmott::OpenXRLayers { 15 | 16 | // Warn about installations outside of program files 17 | class ProgramFilesLinter final : public Linter { 18 | std::vector> Lint( 19 | const APILayerStore* store, 20 | const std::vector>& layers) override { 21 | auto winStore = dynamic_cast(store); 22 | if (!winStore) { 23 | #ifndef NDEBUG 24 | __debugbreak(); 25 | #endif 26 | return {}; 27 | } 28 | 29 | const auto programFiles = std::array { 30 | GetKnownFolderPath(), 31 | GetKnownFolderPath(), 32 | }; 33 | 34 | std::vector> errors; 35 | for (const auto& [layer, details]: layers) { 36 | if (!layer.IsEnabled()) { 37 | continue; 38 | } 39 | 40 | if (details.mLibraryPath.empty()) { 41 | continue; 42 | } 43 | 44 | bool isProgramFiles = false; 45 | for (auto&& base: programFiles) { 46 | const auto mismatchAt 47 | = std::ranges::mismatch(details.mLibraryPath, base); 48 | if (mismatchAt.in2 == base.end()) { 49 | isProgramFiles = true; 50 | break; 51 | } 52 | } 53 | 54 | if (isProgramFiles) { 55 | continue; 56 | } 57 | 58 | errors.push_back(std::make_shared( 59 | fmt::format( 60 | "{} is outside of Program Files; this can cause issue with sandboxed " 61 | "MS Store games or apps, such as OpenXR Tools for Windows Mixed " 62 | "Reality.", 63 | details.mLibraryPath.string()), 64 | PathSet {layer.mJSONPath})); 65 | } 66 | return errors; 67 | } 68 | }; 69 | 70 | static ProgramFilesLinter gInstance; 71 | 72 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/linters/windows/UnsignedDllLinter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "Linter.hpp" 9 | 10 | // clang-format off 11 | #include 12 | #include 13 | #include 14 | // clang-format on 15 | 16 | namespace FredEmmott::OpenXRLayers { 17 | 18 | // Warn about DLLs without valid authenticode signatures 19 | class UnsignedDllLinter final : public Linter { 20 | virtual std::vector> Lint( 21 | const APILayerStore*, 22 | const std::vector>& layers) { 23 | std::vector> errors; 24 | for (const auto& [layer, details]: layers) { 25 | if (!layer.IsEnabled()) { 26 | continue; 27 | } 28 | const auto dllPath = details.mLibraryPath.native(); 29 | if (!std::filesystem::exists(dllPath)) { 30 | continue; 31 | } 32 | 33 | WINTRUST_FILE_INFO fileInfo { 34 | .cbStruct = sizeof(WINTRUST_FILE_INFO), 35 | .pcwszFilePath = dllPath.c_str(), 36 | }; 37 | WINTRUST_DATA wintrustData { 38 | .cbStruct = sizeof(WINTRUST_DATA), 39 | .dwUIChoice = WTD_UI_NONE, 40 | .fdwRevocationChecks = WTD_REVOCATION_CHECK_NONE, 41 | .dwUnionChoice = WTD_CHOICE_FILE, 42 | .pFile = &fileInfo, 43 | .dwStateAction = WTD_STATEACTION_VERIFY, 44 | }; 45 | 46 | GUID policyGuid = WINTRUST_ACTION_GENERIC_VERIFY_V2; 47 | const auto result = WinVerifyTrust( 48 | static_cast(INVALID_HANDLE_VALUE), &policyGuid, &wintrustData); 49 | switch (result) { 50 | case 0: 51 | continue; 52 | case TRUST_E_SUBJECT_NOT_TRUSTED: 53 | case TRUST_E_NOSIGNATURE: 54 | errors.push_back(std::make_shared( 55 | fmt::format( 56 | "{} does not have a trusted signature; this is very likely to " 57 | "cause issues with games that use anti-cheat software.", 58 | details.mLibraryPath.string()), 59 | PathSet {layer.mJSONPath})); 60 | break; 61 | case CERT_E_EXPIRED: 62 | // Not seen reports of this so far; don't know if anti-cheats are 63 | // generally OK with this, or if they recognize the most popular 64 | // layers now 65 | errors.push_back(std::make_shared( 66 | fmt::format( 67 | "{} has a signature without a timestamp, from an expired " 68 | "certificate; This may cause issues with games that use " 69 | "anti-cheat software.", 70 | details.mLibraryPath.string()), 71 | PathSet {layer.mJSONPath})); 72 | break; 73 | default: 74 | errors.push_back(std::make_shared( 75 | fmt::format( 76 | "Unable to verify a signature for {} - error {:#0x}; this is " 77 | "very likely to cause issues with games that use anti-cheat " 78 | "software.", 79 | details.mLibraryPath.string(), 80 | std::bit_cast(result)), 81 | PathSet {layer.mJSONPath})); 82 | break; 83 | } 84 | } 85 | return errors; 86 | } 87 | }; 88 | 89 | static UnsignedDllLinter gInstance; 90 | 91 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/linters/windows/XRNeckSaferLinter.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "Linter.hpp" 9 | #include "windows/WindowsAPILayerStore.hpp" 10 | 11 | namespace FredEmmott::OpenXRLayers { 12 | 13 | class XRNeckSaferLinter final : public Linter { 14 | virtual std::vector> Lint( 15 | const APILayerStore* store, 16 | const std::vector>& layers) { 17 | auto winStore = dynamic_cast(store); 18 | if ( 19 | winStore->GetRegistryBitness() 20 | != WindowsAPILayerStore::RegistryBitness::Wow64_64) { 21 | return {}; 22 | } 23 | 24 | std::vector> errors; 25 | for (const auto& [layer, details]: layers) { 26 | if (!layer.IsEnabled()) { 27 | continue; 28 | } 29 | if (details.mName != "XR_APILAYER_NOVENDOR_XRNeckSafer") { 30 | continue; 31 | } 32 | if (details.mImplementationVersion != "1") { 33 | continue; 34 | } 35 | errors.push_back( 36 | std::make_shared( 37 | "XRNeckSafer has bugs that can cause issues include game crashes, and " 38 | "crashes in other API layers. Disable or uninstall it if you have any " 39 | "issues.", 40 | layer.mJSONPath)); 41 | } 42 | return errors; 43 | } 44 | }; 45 | 46 | static XRNeckSaferLinter gInstance; 47 | 48 | }// namespace FredEmmott::OpenXRLayers -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #include "GUI.hpp" 5 | 6 | // Entrypoint for Linux and MacOS 7 | // 8 | // See wwinmain.cpp for Windows 9 | int main() { 10 | FredEmmott::OpenXRLayers::GUI().Run(); 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /src/manifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | UTF-8 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/portability/filesystem.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | namespace FredEmmott::PortabilityHacks { 10 | template 11 | concept Hashable = requires(T a) { 12 | { std::hash {}(a) } -> std::convertible_to; 13 | }; 14 | 15 | }// namespace FredEmmott::PortabilityHacks 16 | 17 | // std::hash is available on MSVC, but not MacOS 18 | template 19 | requires( 20 | std::same_as 21 | && !FredEmmott::PortabilityHacks::Hashable) 22 | struct std::hash { 23 | std::size_t operator()(const T& p) const { 24 | return hash_value(p); 25 | } 26 | }; -------------------------------------------------------------------------------- /src/std23/ranges.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Fred Emmott 2 | // SPDX-License-Identifier: ISC 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | /** Basic functionality from C++23 9 | * 10 | * This doesn't aim to be a complete or conformant polyfill, just enough for 11 | * basic usage 12 | */ 13 | namespace FredEmmott::OpenXRLayers::std23::ranges { 14 | template