diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..17aa5f2a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Shell scripts must stay LF even on Windows checkouts (they run on Linux). +*.sh text eol=lf diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml index bc035af8..b6a09a34 100644 --- a/.github/workflows/Build.yml +++ b/.github/workflows/Build.yml @@ -14,8 +14,14 @@ env: STEAM_PATH: temp jobs: + Linux64: + uses: ./.github/workflows/BuildLinux.yml + secrets: inherit + Win64: runs-on: windows-2022 + # The release step below attaches the Linux tarball, so wait for it. + needs: Linux64 steps: - name: Checkout repo and submodules @@ -23,6 +29,7 @@ jobs: with: submodules: recursive fetch-depth: 0 + token: ${{ secrets.SHARED_PAT }} - name: Get current date, commit hash and count run: | @@ -86,12 +93,31 @@ jobs: - name: Zip vrto3d_profiles folder run: Compress-Archive -Path vrto3d_profiles/* -DestinationPath vrto3d_profiles.zip + - name: Install Inno Setup + run: choco install innosetup --yes --no-progress + shell: pwsh + + - name: Compile installer + run: iscc installer\VRto3D-Installer.iss + shell: pwsh + - name: Upload zip artifact uses: actions/upload-artifact@v4 with: name: "VRto3D-${{env.CommitHashShort}}" path: output/${{github.event.repository.name}}.zip + - name: Upload installer artifact + uses: actions/upload-artifact@v4 + with: + name: "VRto3D-Installer-${{env.CommitHashShort}}" + path: installer/Output/VRto3D-Installer.exe + + - name: Download Linux tarball + uses: actions/download-artifact@v4 + with: + name: vrto3d-linux64 + - name: GitHub pre-release uses: "marvinpinto/action-automatic-releases@latest" with: @@ -101,7 +127,9 @@ jobs: title: "[${{env.CurrentDate}}] ${{github.event.repository.name}} r${{env.CommitCount}}@${{env.CommitHashShort}}" files: | output/${{github.event.repository.name}}.zip + vrto3d-linux64.tar.gz vrto3d_profiles.zip + installer/Output/VRto3D-Installer.exe README_combined.jpg changelog_combined.jpg \ No newline at end of file diff --git a/.github/workflows/BuildLinux.yml b/.github/workflows/BuildLinux.yml new file mode 100644 index 00000000..b1612879 --- /dev/null +++ b/.github/workflows/BuildLinux.yml @@ -0,0 +1,53 @@ +name: Build Linux + +on: + push: + branches: + - linux_port + pull_request: + branches: + - main + workflow_dispatch: + # Called by Build.yml on main so the release ships the Linux tarball. + workflow_call: + +jobs: + Linux64: + # Oldest supported LTS: binaries link the runner's glibc, so building on + # 22.04 (glibc 2.35) keeps the tarball running on older distros and SteamOS. + runs-on: ubuntu-22.04 + steps: + - name: Checkout repo and submodules + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + token: ${{ secrets.SHARED_PAT }} + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y ninja-build libvulkan-dev libx11-dev \ + libxrandr-dev libwayland-dev wayland-protocols libxkbcommon-dev \ + libdrm-dev + + - name: Configure + run: cmake -S vrto3d -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo + + - name: Build + run: cmake --build build + + - name: Package driver + installer + run: | + mkdir -p package/vrto3d-linux64 + cp -a build/output/drivers package/vrto3d-linux64/ + cp installer/install.sh package/vrto3d-linux64/ + cp LICENSE README.md package/vrto3d-linux64/ + chmod +x package/vrto3d-linux64/install.sh + tar czf vrto3d-linux64.tar.gz -C package vrto3d-linux64 + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: vrto3d-linux64 + path: vrto3d-linux64.tar.gz diff --git a/.gitignore b/.gitignore index c572fa6e..c7597eed 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ bld/ [Ll]og/ [Ll]ogs/ output +vrto3d/build # Visual Studio 2015/2017 cache/options directory .vs/ @@ -240,6 +241,7 @@ ClientBin/ *.pfx *.publishsettings orleans.codegen.cs +.claude # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) diff --git a/.gitmodules b/.gitmodules index 970039ee..26ff1b33 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,20 @@ [submodule "external/VRto3DLib"] path = external/VRto3DLib url = https://github.com/oneup03/VRto3DLib.git +[submodule "external/nvapi"] + path = external/nvapi + url = https://github.com/NVIDIA/nvapi.git +[submodule "external/imgui"] + path = external/imgui + url = https://github.com/ocornut/imgui.git + branch = master +[submodule "external/adl_sdk"] + path = external/adl_sdk + url = https://github.com/GPUOpen-LibrariesAndSDKs/display-library +[submodule "external/SR-lib"] + path = external/SR-lib + url = https://github.com/bo3b/SR-lib.git +[submodule "external/NV3D-Lib"] + path = external/NV3D-Lib + url = https://github.com/oneup03/NV3D-Lib.git + branch = nv3d-glass diff --git a/.vscode/build-driver.ps1 b/.vscode/build-driver.ps1 index 5dfe93de..91f4cfff 100644 --- a/.vscode/build-driver.ps1 +++ b/.vscode/build-driver.ps1 @@ -14,7 +14,13 @@ $ErrorActionPreference = 'Stop' $repoRoot = Split-Path -Parent $PSScriptRoot Set-Location $repoRoot -$steamPath = if ($env:STEAM_PATH) { $env:STEAM_PATH } else { 'C:\Program Files (x86)\Steam' } +$steamPath = if ($env:STEAM_PATH) { + $env:STEAM_PATH +} else { + # Fall back to the registry (Steam isn't always in Program Files). + $reg = (Get-ItemProperty 'HKCU:\Software\Valve\Steam' -ErrorAction SilentlyContinue).SteamPath + if ($reg) { $reg -replace '/', '\' } else { 'C:\Program Files (x86)\Steam' } +} $msbuildCmd = Get-Command msbuild -ErrorAction SilentlyContinue | Select-Object -First 1 $msbuildExe = if ($msbuildCmd) { $msbuildCmd.Source } else { $null } @@ -38,12 +44,14 @@ if ($LASTEXITCODE -ne 0) { } if ($Target -in @('Build', 'Rebuild')) { - cmd /c "xcopy \"vrto3d\\vrto3d\" \"output\\drivers\\vrto3d\" /S /Y /I" + # Call xcopy directly (no cmd /c) — nested cmd quoting mangles paths + # ("Invalid path"), and trailing backslashes must be avoided. + & xcopy 'vrto3d\vrto3d' 'output\drivers\vrto3d' /S /Y /I if ($LASTEXITCODE -gt 1) { exit $LASTEXITCODE } - cmd /c "xcopy \"output\\drivers\\\" \"$steamPath\\steamapps\\common\\SteamVR\\drivers\\\" /S /Y /I" + & xcopy 'output\drivers' (Join-Path $steamPath 'steamapps\common\SteamVR\drivers') /S /Y /I if ($LASTEXITCODE -gt 1) { exit $LASTEXITCODE } diff --git a/README.md b/README.md index 9747a29c..d58da98a 100644 --- a/README.md +++ b/README.md @@ -1,334 +1,452 @@ -# VRto3D - -- OpenVR Driver that can render in SbS or TaB 3D with other formats converted to through ReShade -- Compatible games play great with a XInput controller. No motion controls required! -- VRto3D itself does not "fix" games for 3D, but it allows you to run VR modded (fixed) games on a 3D Display -- Supports User Profiles for individual games -- Provides HMD Pitch and Yaw emulation for games that require it -- Currently targeting OpenVR 2.5.1. -- Windows-only solution currently, but there are other solutions on Linux like Monado XR. - - -## Compatible 3D Displays -- 3D TVs & Projectors - work great, use [Base Installation](#base-installation) in SbS/TaB mode or potentially [Frame Packing](#framepacking-hdmi-3d-only-if-you-need-this-output-format) instructions -- Passive/Interlaced 3D displays - work great, use [Interlaced](#interlaced-checkerboard-and-anaglyph-installation-only-if-you-need-this-output-format) instructions -- AR Glasses (Rokid, Xreal, Viture, RayNeo) - work great, use [AR Glasses](#ar-glasses-only-if-you-need-this-output-format) instructions. If you don't have a USBC port with DP-Alt mode on your PC, they require a compatible adapter - choose one with SBS and audio support. A USBC extension is also recommended. VertoXR can be used for 6DoF or 3DoF Head Tracking -- Lume Pad - works great, use [Base Installation](#base-installation) instructions, requires Sunshine/Gamestream + Moonlight -- SR Displays (Acer Spatial Labs / Asus Spatial Vision / Samsung Odyssey 3D) - work great, use [SR Displays](#sr-simulated-reality-displays-only-if-you-need-this-output-format) instructions. SR-OpenTrack-Bridge can be used for 6DoF or 3DoF Head Tracking -- 3D Vision/Frame Sequential - use [WibbleWobbleVR](https://oneup03.github.io/VRto3D/wiki/WibbleWobbleVR3.0) instead -- Virtual Desktop with a VR headset - works with [additional setup](https://oneup03.github.io/VRto3D/wiki/VirtualDesktop) - - -## Compatible VR Games & Mods -Checkout the [Compatibility List](https://oneup03.github.io/VRto3D/wiki/Compatibility-List) to see if a game has been tested - - -## Hotkeys -- Adjust Depth (Separation) with `Ctrl + F3` and `Ctrl + F4` - - Synchronize the convergence setting by also holding `Shift` - this often has issues in VR mods -- Adjust Convergence with `Ctrl + F5` and `Ctrl + F6` - this often has issues in VR mods -- Save all current settings (ones indicated with a `"+"` under [Configuration](#configuration)) as a profile for the currently running game with `Ctrl + F7` A beep will indicate success -- Reload the profile settings (ones with a `"+"`) from the current game's `game.exe_config.json` with `Ctrl + F10` A beep will indicate success -- Reload the profile settings (ones with a `"+"`) from `default_config.json` with `Ctrl + Shift + F10` A beep will indicate success -- Toggle locking the SteamVR Headset Window to the foreground and focusing the game window with `Ctrl + F8` -- Adjust HMD position and yaw origin with `Ctrl + Home/End` for Y, `Ctrl + Delete/PageDown` for X, `Ctrl + Insert/PageUp` for Yaw, and `Ctrl + Shift + PageUp/PageDown` for Height - - This is useful if you want to align the HMD to a lighthouse tracked position -- Check the [Controls](#controls) section and the Configuration table below to setup HMD camera controls for VR games (check the compatibility list to see if they are needed) -- Check the [User Presets](#user-presets) section for instructions on setting up your own Depth/Separation and Convergence presets and also reference the Configuration table below -- When Pitch/Yaw emulation is enabled (and use_track_filter is disabled), you can adjust the ctrl_sensitivity with `Ctrl -` and `Ctrl +` and the pitch_radius with `Ctrl [` and `Ctrl ]` -- When `use_track_filter` is `true`: - - `Ctrl -` / `Ctrl +` and `Ctrl [` / `Ctrl ]` adjust track filter rotation/translation sensitivity, and also hold `Shift` to adjust track filter rotation/translation deadzones - - `Ctrl + ;` / `Ctrl + '` adjust track filter zoom smoothing, and also hold `Shift` to adjust track filter max zoom range -- Save `hmd_height, hmd_x, hmd_y, hmd_yaw`, and all 6 Track Filter parameters using `Ctrl + F9` -- Attempt to take a SbS Screenshot with `Ctrl + F12` (doesn't always work) - - -## Configuration - -- VRto3D has to be installed and SteamVR launched once for this config file to show up -- Modify the `Steam\config\vrto3d\default_config.json` for your setup -- Some changes made to this configuration require a restart of SteamVR to take effect -- Fields with a `"+"` next to them will be saved to a game's profile when you press `Ctrl + F7` and can be reloaded from either the game's profile using `Ctrl + F10` or the `default_config.json` using `Ctrl + Shift + F10` -- Reference Virtual-Key Code strings for user hotkeys -- Reference [Profile Creation Steps](#profile-creation-steps) for creating a game-specific profile - -| Field Name | Type | Description | Default Value | -|---------------------|---------|---------------------------------------------------------------------------------------------|----------------| -| `display_index` | `int` | 3D display selection by display order (`0` = auto primary, `1` = first display, `2` = second, etc.) | `0` | -| `multi_display` | `bool` | Span 3D window across two displays, starting from display_index and moving to the right | `false` | -| `render_width` | `int` | The width to render per eye | `1920` | -| `render_height` | `int` | The height to render per eye | `1080` | -| `hmd_height` + | `float` | The height/Z position origin of the simulated HMD | `1.0` | -| `hmd_x` | `float` | The X position origin of the simulated HMD | `0.0` | -| `hmd_y` | `float` | The y position origin of the simulated HMD | `0.0` | -| `hmd_yaw` | `float` | The yaw attitude of the simulated HMD | `0.0` | -| `aspect_ratio` | `float` | The aspect ratio used to calculate vertical FoV | `1.77778` | -| `fov` + | `float` | The horizontal field of view (FoV) for the VR rendering | `90.0` | -| `depth` + | `float` | The max separation. Overrides VR's IPD field | `0.1` | -| `convergence` + | `float` | Where the left and right images converge. Adjusts frustum | `1.0` | -| `async_enable` + | `bool` | Whether or not to use Asynchronous Reprojection. May improve or worsen smoothness | `false` | -| `disable_hotkeys` | `bool` | Disable Depth & Convergence adjustment hotkeys to avoid conflict with other 3D mods | `false` | -| `tab_enable` | `bool` | Enable or disable top-and-bottom (TaB/OU) 3D output (Side by Side is default) | `false` | -| `framepack_offset` | `int` | Pixel gap between left and right views in TaB mode. Use for framepacking/HDMI 3D | `0` | -| `reverse_enable` | `bool` | Enable or disable reversed 3D output | `false` | -| `vd_fsbs_hack` | `bool` | Enable or disable half height Full-SbS for Virtual Desktop | `false` | -| `dash_enable` | `bool` | Enable or disable SteamVR Dashboard and Home | `false` | -| `auto_focus` | `bool` | Enable or disable automatic focusing/bringing VRto3D to foreground | `true` | -| `display_latency` | `float` | The display latency in seconds | `0.011` | -| `display_frequency` | `float` | The display refresh rate per-eye, in Hz | `60.0` | -| `pitch_enable` + | `bool` | Enables or disables Controller right stick y-axis mapped to HMD Pitch | `false` | -| `yaw_enable` + | `bool` | Enables or disables Controller right stick x-axis mapped to HMD Yaw | `false` | -| `use_open_track` | `bool` | Enables or disables OpenTrack 6DoF HMD Control | `false` | -| `open_track_port` | `int` | UDP Port for OpenTrack | `4242` | -| `use_track_filter` | `bool` | Enables or disables Accela-Hamilton style pose filtering for tracking rotation and position | `false` | -| `trk_flt_rot_sens` | `float` | Rotation smoothing threshold for track filter (lower = more smoothing) | `0.5` | -| `trk_flt_pos_sens` | `float` | Position smoothing threshold for track filter (lower = more smoothing) | `0.25` | -| `trk_flt_rot_dz` | `float` | Rotation deadzone used by track filter | `0.03` | -| `trk_flt_pos_dz` | `float` | Position deadzone used by track filter | `0.02` | -| `trk_flt_zoom_smooth`| `float` | Additional rotation smoothing when moving toward the display | `0.0` | -| `trk_flt_max_zoom` | `float` | Max Z distance used for scaling zoom smoothing | `10.0` | -| `launch_script` | `string`| Command executed once when VRto3D driver activates (`"start vertoxr://steamvr"`) | `""` | -| `pose_reset_key` + | `string`| The Virtual-Key Code to reset the HMD position and orientation | `"VK_NUMPAD7"` | -| `ctrl_toggle_key` + | `string`| The Virtual-Key Code to toggle Pitch and Yaw emulation on/off when they are enabled | `"XINPUT_GAMEPAD_RIGHT_THUMB"` | -| `ctrl_toggle_type` +| `string`| The ctrl_toggle_key's behavior ("toggle" "hold") | `"toggle"` | -| `pitch_radius` + | `float` | Radius of curvature for the HMD to pitch along. Useful in 3rd person VR games | `0.0` | -| `ctrl_deadzone` + | `float` | Controller Deadzone when using pitch or yaw emulation | `0.05` | -| `ctrl_sensitivity` +| `float` | Controller Sensitivity when using pitch or yaw emulation | `1.0` | -| `user_load_key` + | `string`| The Virtual-Key Code to load user preset | `"VK_NUMPAD1"` | -| `user_store_key` + | `string`| The Virtual-Key Code to store user preset temporarily | `"VK_NUMPAD4"` | -| `user_key_type` + | `string`| The store key's behavior ("switch" "toggle" "hold") | `"switch"` | -| `user_depth` + | `float` | The separation value for a user preset | `0.1` | -| `user_convergence` +| `float` | The convergence value for a user preset | `1.0` | -| `user_fov` + | `float` | The fov value for a user preset (optional, will default to global fov) | `90.0` | - - -## Base Installation - -- Install SteamVR -- Download the [latest VRto3D release](https://github.com/oneup03/VRto3D/releases/latest/download/vrto3d.zip) and copy the `vrto3d` folder from inside the VRto3D.zip to your `Steam\steamapps\common\SteamVR\drivers` folder -- Launch SteamVR once to generate the `default_config.json` and you should see a 1080p SbS `Headset Window` upscaled to fullscreen -- Close SteamVR -- Edit the `Steam\config\vrto3d\default_config.json` as needed - [see what each setting does](#configuration) - - Set `display_index` to your 3D display using display enumeration order (`1` = first enumerated display, `2` = second, etc). Leave it as `0` to auto-use the current primary display. Sometimes this glitches back to the wrong screen and you may have to restart SteamVR - - Set your render resolution per eye to what you want - can save some performance by reducing this. If your display is half-SbS or half-TaB, then you can try setting this to that half-resolution - - Configure any `Virtual-Key Code` settings to use keys that you want (especially `user_load_keys` settings as these load a defined depth+convergence preset) -- Download the latest [VRto3D profiles](https://github.com/oneup03/VRto3D/releases/download/latest/vrto3d_profiles.zip) for games and extract them to your `Steam\config\vrto3d\` folder -- Run SteamVR to verify that you see the Headset window covering your entire display. This is usually not needed before running games. - - The Headset window should appear on the configured `display_index` display - - Dismiss Headset Notice about `Enable Direct Display Mode` as this does nothing -- Try launching a VR game -- Keyboard and Mouse are usable, but you may run into issues with accidentally clicking the wrong window or the cursor escaping the game window if the game's mouse control is coded poorly - - Can try using AutoCursorLock if the mouse keeps escaping -- Make the game run in windowed mode either in-game settings or with `Alt + Enter` This will alleviate controller input and fullscreen issues. (Borderless fullscreen/windowed sometimes also work) -- If needed, press `Ctrl + F8` to lock the 3D window to the foreground and focus the game window - - This is automated by default with the `auto_focus` setting when a VRto3D profile exists for the game -- If game controls & audio aren't working, use `Alt + Tab` to switch to the game window -- To quit, exit the game and try to `Alt + Tab` out - - If the 3D window remains in the foreground, press `Ctrl + F8` to toggle the foregrounding off, and then `Alt + Tab` out - - -## Interlaced, Checkerboard, and Anaglyph Installation (only if you need this output format) - -- Complete the [Base Installation](#base-installation) section -- Optionally set `tab_enable` to true in `Steam\config\vrto3d\default_config.json` if you prefer to lose half vertical resolution instead of half horizontal resolution - - If using interlaced mode, you want SbS for Column Interlaced and TaB for Row/Line Interlaced. Most interlaced displays should use TaB -- Download the latest ReShade with full add-on support -- Run the ReShade installer - - Browse to to your `Steam\steamapps\common\SteamVR\bin\win64` folder - - Select `vrserver.exe` and click Next - - Select `DirectX 11` and click Next - - Click `Uncheck All` and click Next, Next, Finish -- Download 3DToElse.fx and save it to `Steam\steamapps\common\SteamVR\bin\win64\reshade-shaders\Shaders` -- Run SteamVR -- Press `Home` to open ReShade and click `Skip Tutorial` -- Select `To_Else` in the menu to enable 3DToElse -- Disable ReShade's `Performance Mode` checkbox -- Change 3DToElse settings: - - Set `Stereoscopic Mode Input` to `Side by Side` (or `Top and Bottom` if you set `tab_enable` above) - - Set `3D Display Mode` to the type needed for your display (even anaglyph) - - `Eye Swap` can be toggled if needed - - Don't touch `Perspective Slider` -- Enable ReShade's `Performance Mode` checkbox -- Once configuration is complete, you can run everything the same way as the Base Installation -- If ReShade settings don't save and you keep getting prompted for the tutorial, you may have to manually edit `Steam\steamapps\common\SteamVR\bin\win64\ReShade.ini` and disable Tutorial with `TutorialProgress=4` - - -## FramePacking, HDMI 3D (only if you need this output format) - -- Complete the [Base Installation](#base-installation) section -- In `Steam\config\vrto3d\default_config.json` set these settings: - - Set `display_index` to the 3D display where frame-packed output should appear - - `tab_enable` to true - - `framepack_offset` to `45` for 1920x2205 or `30` for 1280x1470 (this may vary by display) -- More instructions and discussion are in this forum -- Create one of these Custom Resolutions in Nvidia Control Panel or CRU: -- frame_packed_720p : resolution 1280x1470, 60Hz - - horizontal: 1280 active; 110 front, 40 sync, 220 back (1650 total) - - vertical: 1470 active; 5 front, 5 sync, 20 back (1500 total) -- frame_packed_1080p : resolution 1920x2205, 24Hz/60Hz - - horizontal: 1920 active; 638 front, 44 sync, 148 back (2750 total) - - vertical: 2205 active; 4 front, 5 sync, 36 back (2250 total) -- May need to use CVT reduced blank specs for success with 1080 60Hz, but 60Hz is not standard and may not work at all - - horizontal: 1920 active; 48 front, 32 sync, 80 back (2080 total) - - vertical: 2205 active; 4 front, 5 sync, 36 back (2250 total) -- It may be necessary to remove other resolutions with CRU to avoid games changing the resolution. Hopefully running them in windowed mode (required for VRto3D) will prevent issues though - - -## SR (Simulated Reality) Displays (only if you need this output format) - -- Complete the [Base Installation](#base-installation) section -- If your display supports higher refresh rates than 60hz, you can optionally set `display_frequency` to match in `Steam\config\vrto3d\default_config.json` -- Install the software package provided with your SR display (Samsung Odyssey 3D Hub or Acer TrueGame) -- Download the latest ReShade with full add-on support -- Run the ReShade installer - - Browse to to your `Steam\steamapps\common\SteamVR\bin\win64` folder - - Select `vrserver.exe` and click Next - - Select `DirectX 11` and click Next - - Click `Uncheck All` and click Next - - Select `3DGameBridge by Janthony & DinnerBram` and click Next - - Click Finish -- Run SteamVR -- Press `Home` to open ReShade and click `Skip Tutorial` -- Click on the `Add-Ons` tab -- Select `srReshade` in the menu to enable it - - Expand the srReshade dropdown and if you get a `Status: Inactive - Unable to load all SR DLLs` then you may need to do these additional steps: - - Open Windows Run with `Win + R` - - Paste this command: `cmd /k setx PATH "C:\Program Files\LeiaSR\Platform\bin;%PATH%"` - - Exit the terminal and reboot - - 3D can be toggled on and off by using srReshade's `Ctrl + 2` hotkey -- Click on the `Home` tab - - Enable ReShade's `Performance Mode` checkbox -- Once configuration is complete, you can run everything the same way as the Base Installation -- If ReShade settings don't save and you keep getting prompted for the tutorial, you may have to manually edit `Steam\steamapps\common\SteamVR\bin\win64\ReShade.ini` and disable Tutorial with `TutorialProgress=4` -- If you experience a super dark screen, try enabling a random ReShade shader -- Optional: Setup 6DoF or 3DoF Head Tracking - - Download SR-OpenTrack-Bridge for 6DoF or 3DoF Head Tracking - - Save it to somewhere with a short path without spaces like `C:/Apps/SRTrack/` - - In `Steam\config\vrto3d\default_config.json` set these settings - - Set `use_open_track` to `true` and ensure `open_track_port` is set to `4242` - - Set `use_track_filter` to `true` - - Set `launch_script` to `"start C:/Apps/SRTrack/Simulated_Reality_OpenTrack_Bridge.exe"` and tracking will be auto started every time you start SteamVR - - Start SteamVR and SR-OpenTrack-Bridge should start also - - Configure the settings in the SR-OpenTrack-Bridge window to tune the tracker alongside adjusting the VRto3D track filter using the [Hotkeys](#hotkeys) - - Either the `Yaw/Pitch only` or `XYZ + Yaw/Pitch` modes are recommended - - Lock the SR-OpenTrack-Bridge settings with `Ctrl + L` to avoid unwanted changes later - - Alt + Tab to the SR-OpenTrack-Bridge window and press `Ctrl + X` to recenter tracking - - -## AR Glasses (only if you need this output format) -- Connect AR glasses, switch to Full-SbS mode -- Complete the [Base Installation](#base-installation) section -- Optional: install VertoXR to provide 6DoF or 3DoF Head Tracking for popular AR glasses - - Open VertoXR, connect to the AR glasses - - Select `Game Mode`, place glasses on a flat surface looking straight ahead, and click `Start Calibrate`. Calibration may need to be redone if misalignment occurs - - Edit `OpenTrack Configuration` and disable `Enable Roll` if needed - - `Start` the OpenTrack VertoXR plugin - - In `Steam\config\vrto3d\default_config.json` set these settings: - - Set `use_open_track` to `true` and ensure `open_track_port` matches the VertoXR OpenTrack port - - Set `launch_script` to `"start vertoxr://steamvr"` and VertoXR will be auto started with Open Track active every time you start SteamVR - - Use the `Recenter` button in VertoXR as needed - - -## Frame Sequential (3DVision) -- Use [WibbleWobbleVR](https://oneup03.github.io/VRto3D/wiki/WibbleWobbleVR3.0) instead - it has support for VRto3D profiles - - -## Notes -- The game's main window has to be in focus for control input from your mouse/keyboard/controller to work -- SteamVR may still complain about Direct Display mode, but this can be safely dismissed -- Exiting SteamVR may "restart" Steam - this is normal -- Overlays generally won't work on this virtual HMD -- XInput controller is recommended -- SteamVR doesn't support HDR currently - - AutoHDR may work, but some games will be too dark or too bright -- DLSS, TAA, and other temporal based settings can create a halo around objects. Most VR mods have fixes for this, but some may not - -#### Controls -- If you want to use Steam Input - - Open Steam->Settings->Controller - - Toggle on `Enable Steam Input for Xbox Controllers` - - Click `Edit` on the `Desktop Layout` and then select `Disable Steam Input` - - On SteamVR's library page, click the `Controller Icon` and select `Disable Steam Input` - - Generally you need to start SteamVR first and separately from the game for Steam Input to work -- This project is primarily targeted for VR mods of flatscreen games, not full VR games. As such, there is only HMD pitch & yaw emulation and no VR controller emulation -- You can setup a Lighthouse + Vive Tracker + tracked controllers with VRto3D for a seated play area. See [this guide](https://oneup03.github.io/VRto3D/wiki/Motion-Controls-&-Tracking) for details -- Several VR controller only games can be made to work by using Driver4VR, a paid SteamVR Vive controller emulator. Games with mainly pointer controls work ok. Games with a lot of interaction/movement don't work well. -- Optional HMD `pitch_enable` and `yaw_enable` emulation can be turned on to help with games or mods that need it (maps to XInput right stick) - - Reference Virtual-Key Code to find the strings to use for these hotkeys - - The `ctrl_toggle_key` can be set and used to toggle these settings on/off in-game (only functions if `pitch_enable` and/or `yaw_enable` is set to true) - - The `ctrl_toggle_type` can be set to either `"toggle"` pitch/yaw on/off or `"hold"` that disables them while the button is held - - The `pose_reset_key` can be set to allow resetting the view to the original position and orientation - - Both of these keys can be set to XInput buttons & combinations or single keyboard/mouse keys as outlined in User Presets - Load Keys below - - The `pitch_radius` can be set to make the pitch emulation move along a semicircle instead of just tilting up/down in place. Use the [Hotkeys](#hotkeys) to adjust this in-game -- OpenTrack 6DoF or 3DoF support is available over UDP loopback at the configured `open_track_port` when `use_open_track` is true - - It can be used in combination with Pitch/Yaw emulation and HMD offsets - - Both AR glasses and SR displays are compatible via 3rd party apps mentioned above - - You can also use the OpenTrack app to do tracking with cameras, IMUs, phone apps, etc -- Track filtering of 6DoF/3DoF input can be enabled with `use_track_filter` - - This filter is useful for reducing jitter while preserving responsiveness through sensitivity/deadzone tuning - - See the [Hotkeys](#hotkeys) section for how to adjust the filter - -#### User Presets -- If you swap between different convergence settings in-game, sometimes you will end up with black bars on the sides of the screen or you may not see a change immediately. If you reload/restart/reinitialize the VR mod, you should see the change -- It is recommended to use a single convergence setting for all your presets given the above issue with some VR mods -- Create any number of user depth/separation & convergence hotkeys in the `user_settings` area of the `default_config.json` - - A user preset looks like this: - - ``` - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.1, - "user_convergence": 1.0, - "user_fov": 70.0 - }, - ``` -- A Load key and a Store key can be configured to load and save Depth/Separation and Convergence settings for a preset - - Load keys can use XInput buttons & combinations as well as single keyboard/mouse keys - - The Guide button can be used, but not in combinations - - XInput Combinations can be set like this `"XINPUT_GAMEPAD_A+XINPUT_GAMEPAD_B"` - - Store keys can only use single keyboard/mouse keys - - Reference Virtual-Key Code to find the strings to use for these hotkeys -- The Load key can be configured to `"switch"` to the user depth/separation & convergence setting, `"toggle"` between the preset and the previous setting every 1.5s, or `"hold"` the user setting until the key is released -- The Store key will update your user Depth/Separation and Convergence setting to the current value (this only saves while the game is running - you need to create a game profile as detailed below to store it permanently) -- It is recommended to have a single user preset of `"switch"` type that matches the default depth/separation & convergence so you can easily get back to the default - -#### Profile Creation Steps: -1. Modify or copy and create user preset(s) in `default_config.json` (or preferably in the `Game.exe_config.json` if one already exists) for the game you want to play -2. If applicable, modify `hmd_height, fov, pitch_enable, yaw_enable, pose_reset_key, ctrl_toggle_key, ctrl_toggle_type, pitch_radius, ctrl_deadzone, ctrl_sensitivity` for the game profile -3. If the game is already running, use `Ctrl + Shift + F10` to reload the `default_config.json` (or `Ctrl + F10` to reload the `Game.exe_config.json`) with your new settings and presets -4. Adjust depth/separation (`Ctrl + F3` and `Ctrl + F4` with `+ shift` if possible) & convergence (`Ctrl + F5` and `Ctrl + F6`) for a preset -5. Use the configured `user_store_key` to temporarily save the current depth/separation & convergence values to the preset -6. Repeat 4 & 5 for each preset you need -7. Adjust depth/separation & convergence back to what you want the default to be (if you have a default `"switch"` preset, you can use its configured `user_load_key`) -8. If applicable, adjust the `ctrl_sensitivity` with `Ctrl -` and `Ctrl +` and the `pitch_radius` with `Ctrl [` and `Ctrl ]` -9. Save the profile with `Ctrl + F7` -10. Open your new profile from `Steam\config\vrto3d\` in a text editor and make final adjustments like: making all the convergence values match to avoid rendering or performance issues, changing virtual-key mappings, or tweaking other values/settings -11. Close out of SteamVR and the game and restart the game. You should hear a loud beep to indicate the profile loaded. Test the profile and you can still make any adjustments per above instructions -12. Share your `Steam\config\vrto3d\Game.exe_config.json` with others - -#### Troubleshooting -- If SteamVR appears on the wrong display, set `display_index` to the correct display enumeration order and restart SteamVR -- If SteamVR crashes and disables add-ons, you will need to re-enable VRto3D in the SteamVR Status window -- The first thing to try is deleting your `Steam\config\steamvr.vrsettings` and `Steam\config\vrto3d\default_config.json` -- If you have used other SteamVR drivers that also create a virtual HMD, you will need to disable and/or uninstall them - - Run SteamVR - - On the SteamVR Status window, go to `Menu -> Settings` - - Change to the `Startup / Shutdown` tab - - Click `Manage Add-Ons` - - Turn `Off` any virtual HMD drivers (ALVR, VRidge, OpenTrack, VCR, iVRy, etc) - - if issues still arise, try a Clean SteamVR Install and delete your `Steam\steamapps\common\SteamVR` folder -- Conversely, to uninstall or disable VRto3D, you can disable it under the same `Manage Add-Ons` menu or delete the `vrto3d` folder from your `Steam\steamapps\common\SteamVR\drivers` folder -- If you have a VR headset and run into issues with this driver, here's some things to try: - - Disconnect VR headset from computer - - Clean SteamVR Install - - Set SteamVR as OpenXR Runtime - - -## Building - -- Clone the code and initialize submodules -- Define `STEAM_PATH` environment variable with the path to your main Steam folder -- Open Solution in Visual Studio 2022 or VSCode -- Use the solution to build this driver -- Build output is automatically copied to your `SteamVR\drivers` folder +# VRto3D + +- OpenVR Driver (OpenXR also supported) that can render in any stereo format natively +- Compatible games play great with a XInput controller. No motion controls required! +- VRto3D itself does not "fix" games for 3D, but it allows you to run VR modded (fixed) games on a 3D Display +- Supports User Profiles for individual games +- Provides HMD Pitch and Yaw emulation for games that require it +- Currently targeting OpenVR 2.15.6. +- Runs on Windows and natively on Linux — see the [Linux](#linux) section for platform-specific setup + + +## Compatible 3D Displays & Output Modes + +VRto3D renders one canonical 2W x H side-by-side frame internally and the active Output Mode repacks it for your display. Pick the matching `output_mode` either in `default_config.json` or from the OSD `System` tab (`Ctrl + Home`). Most modes only need the [Regular Installation](#installation); the few that need extra software or display setup are called out below. + +Find your display type and use the listed Output Mode(s): + +
+ 3D TVs & Projectors (HDMI SbS / TaB) + +- **Output Modes:** `SbS`, `TaB`, or one of the `FramePacked*` modes +- Pick `SbS` or `TaB` based on what your TV / projector accepts as a stereo input +- `FramePacked*` modes target HDMI 1.4 frame-packed 3D - VRto3D will attempt to engage the custom resolution, but it still relies on your display accepting it and allowing you to engage 3D: + - `FramePacked720p60` - 1280x1470 @60Hz, 30px gap + - `FramePacked1080p24` - 1920x2205 @24Hz, 45px gap (most universal) + - `FramePacked1080p60` - 1920x2205 @60Hz, 45px gap, 371.25 MHz pixel clock (HDMI 2.0+) + - `FramePacked1080p60CVT` - 1920x2205 @60Hz with CVT reduced blanking, 45px gap, 280.8 MHz pixel clock (HDMI 2.0) + - For any of these modes, it is recommended to start SteamVR before starting the game, as they change monitor modes, which might break games +- For FramePacked, you can also try manually creating one of these Custom Resolutions in Nvidia Control Panel or CRU first: + - **frame_packed_720p** - 1280x1470, 60Hz + - horizontal: 1280 active; 110 front, 40 sync, 220 back (1650 total) + - vertical: 1470 active; 5 front, 5 sync, 20 back (1500 total) + - **frame_packed_1080p** - 1920x2205, 24Hz / 60Hz + - horizontal: 1920 active; 638 front, 44 sync, 148 back (2750 total) + - vertical: 2205 active; 4 front, 5 sync, 36 back (2250 total) + - May need to use CVT reduced blank specs for success with 1080 60Hz, but 60Hz is not standard and may not work at all + - horizontal: 1920 active; 48 front, 32 sync, 80 back (2080 total) + - vertical: 2205 active; 4 front, 5 sync, 36 back (2250 total) + - It may be necessary to remove other resolutions with CRU to avoid games changing the resolution. Hopefully running them in windowed mode (required for VRto3D) will prevent issues though + - More instructions and discussion are in this forum thread +- **Linux:** `SbS`, `TaB`, and the `FramePacked*` modes all work. Frame-packed timing is applied differently than on Windows: + - **X11 session**: VRto3D applies the 1280x1470/1920x2205 custom modeline at runtime via XRandR — no EDID hacking (AMD/Intel; NVIDIA proprietary needs `ModeValidation` overrides in xorg.conf) + - **Wayland session**: runtime custom modelines aren't possible. Generate an EDID override with `tools/make_fp_edid.py` and load it via `drm.edid_firmware=:edid/.bin` on the kernel cmdline + - Either way the HDMI 3D InfoFrame is not emitted (kernel limitation) — most 3D TVs sync to the raw timing or let you force 3D mode manually, exactly like CRU-based setups on Windows + +
+ +
+ Passive / Interlaced 3D Displays + +- **Output Modes:** `RowInterlaced`, `ColInterlaced`, or `Checkerboard` +- `RowInterlaced` covers the vast majority of passive 3D TVs and monitors +- `ColInterlaced` is for column-interlaced passive panels +- `Checkerboard` is for DLP-link 3D projectors and the older Mitsubishi / Samsung DLP 3D TVs (`(x+y)%2` eye selection) +- **Linux:** supported + +
+ +
+ AR Glasses (Rokid, Xreal, Viture, RayNeo) + +- **Output Mode:** `SbS` (switch the glasses to Full-SbS mode first) +- If you don't have a USBC port with DP-Alt mode on your PC, you'll need a compatible adapter - choose one with SBS and audio support +- A USBC extension is also recommended +- Optional: install VertoXR for 6DoF or 3DoF Head Tracking + - Open VertoXR and connect to the AR glasses + - Select `Game Mode`, place the glasses on a flat surface looking straight ahead, and click `Start Calibrate`. Calibration may need to be redone if misalignment occurs + - Edit `OpenTrack Configuration` and disable `Enable Roll` if needed + - `Start` the OpenTrack VertoXR plugin + - Open the VRto3D OSD (`Ctrl + Home`) and on the `Tracking` tab tick `Enable OpenTrack` under `OpenTrack`, then set `UDP Port` to match VertoXR's OpenTrack port (restart SteamVR after a port change) + - On the OSD `System` tab, set `Launch Script` to `start vertoxr://steamvr` so VertoXR auto-starts with OpenTrack active every time you start SteamVR + - Click `Save Default Cfg` in the OSD footer to persist the changes + - Use the `Recenter` button in VertoXR as needed +- **Linux:** `SbS` output is supported; the optional VertoXR head-tracking app is Windows-only, so use another OpenTrack source for head tracking on Linux + +
+ +
+ Lume Pad, 3DS, Other Wireless 3D Displays + +- **Output Mode:** `SbS` +- Lume Pad Requires Sunshine/Gamestream + Moonlight to stream the SbS output to the device +- 3DS Requires Sunshine/Gamestream + Moonlight with a custom resolution +- **Linux:** supported + +
+ +
+ SR Displays (Acer Spatial Labs / Asus Spatial Vision / Samsung Odyssey 3D) + +- **Output Mode:** `LeiaSR` (SR Display Weaver) +- Install Samsung Odyssey 3D Hub or Acer TrueGame first +- Check the `Fix LeiaSR library loading (requires restart)` option in the VRto3D installer + - Or Manually: Open Windows Run with `Win + R`, Paste this command: `cmd /k setx PATH "C:\Program Files\LeiaSR\Platform\bin;%PATH%"` Exit the terminal and reboot +- 6DoF head tracking is built in - just enable Open Track in the OSD `Tracking` tab. Tune the LeiaSR head-tracking and track filter sliders on the same tab +- **Linux:** not available (compiled out) - there is no Linux SR SDK in-tree + +
+ +
+ 3D Vision / Frame Sequential (Shutter Glasses) + +- **Output Modes:** `WibbleWobble` (preferred) or `NvidiaDX9` (legacy / unstable) +- `WibbleWobble` requires the WibbleWobbleClient - select `Install WibbleWobble for Frame Sequential 3D` in the VRto3D Installer. Follow the [WibbleWobble SteamVR Setup Instructions](https://oneup03.github.io/VRto3D/wiki/WibbleWobbleVR3.0#steamvr-setup) (skip the `VR Config` step, but do the rest). When framerate is low, you will have eye flickering +- `NvidiaDX9` requires the [3DVision driver installed](https://oneup03.github.io/3DVision4All/docs/Native) and 3D Enabled. May freeze or crash, requiring a hard reset. Should be more stable on single-display setups. If it crashes, just try again +- For both of these modes, it is recommended to start SteamVR before starting the game, as they change monitor modes, which might break games +- Either one may require `Swap Eyes` to be set depending on how your display initializes +- **Linux:** not available (compiled out) - `NvidiaDX9` has no Linux driver stack, and `WibbleWobble` is a Windows-only client (a port may come later) + +
+ +
+ VR Headset (via Virtual Desktop) + +- **Output Mode:** `VirtualDesktop` +- Half-height Full-SbS in a 2W x 2H window with black bars. See the [Virtual Desktop setup wiki](https://oneup03.github.io/VRto3D/wiki/VirtualDesktop) for required additional configuration +- **Linux:** the `VirtualDesktop` output mode works + +
+ +
+ Dual Display Setups (Dual Projector / Dual Monitor) + +- **Output Modes:** `DualDisplay` or `DualDisplayFlip` +- This assumes you have two displays with the same resolution that are aligned vertically. You can also use Nvidia Surround +- `DualDisplay` puts the left eye on the `display_index` and the right eye on the display to the right of it +- `DualDisplayFlip` is the same as `DualDisplay` but the left eye is flipped vertically - useful for mirror-based dual-monitor 3D rigs +- **Linux:** supported + +
+ +
+ Anaglyph 3D (Color-Filter Glasses) + +- **Output Modes:** any of the `Anaglyph*` modes - works on any 2D display +- Red / Cyan glasses: `AnaglyphRedCyan` (simple R \| GB split), `AnaglyphRedCyanDubois` (Dubois - best general-purpose), `AnaglyphRedCyanDeghosted`, `AnaglyphRedCyanCompromise` +- Green / Magenta glasses: `AnaglyphGreenMagenta` (simple G \| RB split), `AnaglyphGreenMagentaDubois`, `AnaglyphGreenMagentaDeghosted` +- Blue / Amber (ColorCode 3D) glasses: `AnaglyphBlueAmber` +- **Linux:** supported + +
+ +
+ 2D / Mono Display + +- **Output Mode:** `Mono` +- Single-eye view on any normal 2D display. Toggle `eye_swap` to render the right eye instead of the left +- **Linux:** supported + +
+ + +## Compatible VR Games & Mods +Checkout the [Compatibility List](https://oneup03.github.io/VRto3D/wiki/Compatibility-List) to see if a game has been tested + + +## Hotkeys +Most adjustments now happen in the [On-Screen Menu](#user-presets-via-osd) — open it with `Ctrl + Home` (or `Start + D-Pad Down` on a connected XInput controller). The remaining global hotkeys are intentionally minimal: + +- Open / close the OSD menu with `Ctrl + Home` or the `Start + D-Pad Down` gamepad chord + - Once open, the menu accepts mouse, keyboard, and gamepad navigation (D-pad / left stick to move, `A` to activate, `B` to cancel the current widget, `LB`/`RB` to page through tabs, right stick to scroll) + - Tap `X` (Xbox) / `Square` (PS) to jump nav up to the title bar (where the `X` close button lives — press `A` to dismiss); hold the same button to enter ImGui's *windowing mode* (left stick moves the window, D-pad resizes it, `LB`/`RB` cycle windows) +- Adjust Depth (Separation) with `Ctrl + F3` and `Ctrl + F4` - re-syncs the projection so the change is visible immediately (some VR mods otherwise need a reload to pick it up) + - Hold `Shift` to skip the projection re-sync (bare depth nudge) +- Adjust Convergence with `Ctrl + F5` and `Ctrl + F6` - this often has issues in VR mods +- Save current Depth / Convergence / FoV (and other profile fields marked with `"+"` under [Configuration](#configuration)) to the running game's `Game.exe_config.json` with `Ctrl + F7` - a beep indicates success +- Reload the running game's `Game.exe_config.json` with `Ctrl + F10`, or reload `default_config.json` with `Ctrl + Shift + F10` - a beep indicates success +- Toggle Auto-Depth on / off with `Ctrl + F11` +- Toggle locking the SteamVR Headset Window to the foreground (and focusing the game window) with `Ctrl + F8` +- Take a SbS (and Crossview) Screenshot with `Ctrl + F12` +- All of the above (except `Ctrl + Home`) can be disabled with `disable_hotkeys` if they conflict with another 3D mod - the OSD menu remains accessible + +User-defined preset hotkeys (configured under [User Presets](#user-presets-via-osd)) work alongside the built-in keys above. + + +## Installation + +- Install SteamVR +- **Recommended**: Download [`VRto3D-Installer.exe`](https://github.com/oneup03/VRto3D/releases/download/latest/VRto3D-Installer.exe) from the latest release and run it. The installer auto-detects your Steam library, installs/updates the VRto3D driver, and offers optional cleanup of legacy ReShade and third-party SteamVR drivers. It can also install WibbleWobble and launch SteamVR for you. + - **Manual alternative**: Download the [latest VRto3D release](https://github.com/oneup03/VRto3D/releases/latest/download/vrto3d.zip) and copy the `vrto3d` folder from inside the VRto3D.zip to your `Steam\steamapps\common\SteamVR\drivers` folder +- Launch SteamVR once to generate `default_config.json` and you should see a 1080p SbS `VRto3D` window upscaled to fullscreen +- Open the OSD menu with `Ctrl + Home` and use the `System` tab to pick `Display Index`, `Output Mode`, and render resolution for your display - see the [Output Modes](#compatible-3d-displays--output-modes) table for which mode to choose + - Or edit `Steam\config\vrto3d\default_config.json` manually - [see what each setting does](#configuration) + - Sometimes the display selection glitches back to the wrong screen and you may have to restart SteamVR + - If your display is half-SbS or half-TaB, you can usually save some performance by reducing the per-eye render to half-resolution. (may break aspect ratio in some games) +- Download the latest [VRto3D profiles](https://github.com/oneup03/VRto3D/releases/download/latest/vrto3d_profiles.zip) for games and extract them to your `Steam\config\vrto3d\` folder (or click `Download Latest Profiles` on the OSD's `System` tab) +- Restart SteamVR to verify that you see the Headset window covering your entire display. This is usually not needed before running games. + - The Headset window should appear on the configured `display_index` display +- Try launching a VR game +- Keyboard and Mouse are usable, but you may run into issues with accidentally clicking the wrong window or the cursor escaping the game window if the game's mouse control is coded poorly + - Enable `Lock Cursor to Game` (`lock_cursor`) in the OSD's `System` tab to confine the mouse to the game window, and `Hide Cursor` (`hide_cursor`) to fully hide the OS cursor while the game is focused + - Enable `Stereo Cursor` (`stereo_cursor`) to replace the flat OS cursor with a per-eye 3D cursor drawn at an adjustable depth — recommended for SbS/TaB displays where the OS cursor only lines up with one eye +- Make the game run in windowed mode either in-game settings or with `Alt + Enter` This will alleviate controller input and fullscreen issues. (Borderless fullscreen/windowed sometimes also work) +- If needed, press `Ctrl + F8` to lock the 3D window to the foreground and focus the game window + - This is automated by default with the `auto_focus` setting +- If game controls & audio aren't working, use `Alt + Tab` to switch to the game window +- To quit, exit the game and try to `Alt + Tab` out + - If the 3D window remains in the foreground, press `Ctrl + F8` to toggle the foregrounding off, and then `Alt + Tab` out + + +## Linux + +Native Linux SteamVR driver — same direct-mode architecture as Windows, no virtual display or capture chain. The SteamVR compositor renders games into driver-allocated Vulkan images (shared via dmabuf), VRto3D repacks them into your chosen 3D format and presents fullscreen on the selected display. Configuration, profiles, the OSD, and hotkeys all work the same as on Windows; everything below is Linux-specific. + +#### Requirements + +- SteamVR for Linux (Steam → SteamVR → install; ~2.16+) +- A Vulkan-capable GPU (tested: AMD/RADV on Steam Deck) +- Desktop session (X11 or Wayland). KDE, GNOME, Hyprland/Sway all work; always-on-top is guaranteed on KDE/wlroots (layer-shell) and X11. +- For hotkeys/gamepad: your user in the `input` group: + ``` + sudo usermod -aG input $USER # then log out/in + ``` + +#### Install + +- Download [`vrto3d-linux64.tar.gz`](https://github.com/oneup03/VRto3D/releases/download/latest/vrto3d-linux64.tar.gz) from the latest release, extract it, and run the installer: + ``` + tar xzf vrto3d-linux64.tar.gz + cd vrto3d-linux64 + ./install.sh + ``` + The script finds your Steam installation (pass `--steam ` if it's somewhere unusual), copies the driver into `SteamVR/drivers/`, registers it in `steamvr.vrsettings`, and warns if your user is missing the `input` group. Run `./install.sh --uninstall` to remove it again. +- **Manual alternative**: copy the `drivers/vrto3d` folder from the tarball into `/steamapps/common/SteamVR/drivers/`, then in `~/.local/share/Steam/config/steamvr.vrsettings` set: + ```json + "steamvr": { "requireHmd": true, "forcedDriver": "vrto3d", "activateMultipleDrivers": true } + ``` + +Config lives in `/config/vrto3d/default_config.json`, same schema as Windows. Keybind names use the portable vocabulary (`Key_A`, `Numpad7`, `Pad_A`, `Pad_Start+Pad_DPadDown`); legacy `VK_*`/`XINPUT_*` names still load and are rewritten on the next profile save. + +#### Display selection & presenters + +`display_index` picks the output (0 = primary, 1..N = connected order). The presenter is chosen by session: Wayland (layer-shell overlay surface — always on top on KDE/Hyprland/Sway, plain fullscreen on GNOME) or X11 (borderless `_NET_WM_STATE_ABOVE` window). Override with env `VRTO3D_PRESENTER=x11|wayland` (e.g. force X11/XWayland when you need runtime frame-packed modelines). + +Per-output-mode Linux compatibility (including the LeiaSR / 3D Vision / WibbleWobble modes that are compiled out, and the runtime vs. EDID handling for frame-packed HDMI) is noted inline in the [Output Modes](#compatible-3d-displays--output-modes) table. + +#### Limitations + +- Cursor lock/hide/stereo (`hide_cursor`/`lock_cursor`/`stereo_cursor`) and focus stealing — no cross-client mechanism on Wayland; X11 cursor grab may come later + + +## Configuration + +- VRto3D has to be installed and SteamVR launched once for `default_config.json` to be created +- The easiest way to edit settings is the in-game OSD menu (`Ctrl + Home`); changes take effect immediately and the footer's `Save Default Cfg` / `Save Game Cfg` buttons persist them +- For manual edits, modify `Steam\config\vrto3d\default_config.json` - some changes require a SteamVR restart (display index, output mode, render size, OpenTrack port) +- Fields with a `"+"` next to them are saved to a game's profile when you press `Ctrl + F7` (or use the OSD `Save Game Cfg` footer button), and can be reloaded from the game's profile (`Ctrl + F10`) or `default_config.json` (`Ctrl + Shift + F10`) +- Reference Virtual-Key Code strings for hotkey fields +- Reference [Profile Creation Steps](#profile-creation-via-osd) for creating a game-specific profile + +| Field Name | Type | Description | Default Value | +|-------------------------------|---------|---------------------------------------------------------------------------------------------------|----------------| +| `display_index` | `int` | 3D display selection by display order (`0` = auto primary, `1` = first display, `2` = second, etc.) | `0` | +| `output_mode` | `string`| Stereo output format. See the [Output Modes](#compatible-3d-displays--output-modes) table for valid values | `"SbS"` | +| `eye_swap` | `bool` | Swap left and right eyes | `false` | +| `render_width` | `int` | The width to render per eye | `1920` | +| `render_height` | `int` | The height to render per eye | `1080` | +| `display_frequency` | `float` | Per-eye refresh rate in Hz. `0.0` auto-detects from the target monitor at activation | `0.0` | +| `hmd_height` + | `float` | The height/Z position origin of the simulated HMD | `1.0` | +| `hmd_x` | `float` | The X position origin of the simulated HMD | `0.0` | +| `hmd_y` | `float` | The y position origin of the simulated HMD | `0.0` | +| `hmd_yaw` | `float` | The yaw attitude of the simulated HMD | `0.0` | +| `aspect_ratio` | `float` | The aspect ratio used to calculate vertical FoV | `1.77778` | +| `fov` + | `float` | The horizontal field of view (FoV) for the VR rendering | `90.0` | +| `depth` + | `float` | The max separation. Overrides VR's IPD field | `0.1` | +| `convergence` + | `float` | Where the left and right images converge. Adjusts frustum | `1.0` | +| `async_enable` + | `bool` | Whether or not to use Asynchronous Reprojection. May improve or worsen smoothness | `false` | +| `auto_depth_enabled` + | `bool` | Enable Auto-Depth (GPU disparity analysis caps depth so the closest object stays comfortable) | `false` | +| `auto_depth_target_disparity` +| `float`| Target max on-screen disparity, as a fraction of one eye's width | `0.005` | +| `auto_depth_smoothing` + | `float` | Auto-Depth smoothing (higher = snappier; lower = smoother) | `0.08` | +| `shader_enabled` + | `bool` | Enable the display-correction shader pass (Lift/Gamma/Gain + S-Curve) - see [Display Correction](#display-correction-crosstalk-reduction) | `false` | +| `shader_lift` + | `[float]` | RGB Lift (shadows), 3-element array, each `0..2` | `[1.0, 1.0, 1.0]` | +| `shader_gamma` + | `[float]` | RGB Gamma (midtones), 3-element array, each `0..2` | `[1.0, 1.0, 1.0]` | +| `shader_gain` + | `[float]` | RGB Gain (highlights), 3-element array, each `0..2` | `[1.0, 1.0, 1.0]` | +| `shader_curve` + | `float` | Extended S-Curve strength (`0.33..3.0`; `1.0` = pass-through; `<1` reduces midtone contrast) | `1.0` | +| `shader_curve_offset_low` + | `float` | S-Curve low-color offset (`-1..1`) | `0.0` | +| `shader_curve_offset_high` + | `float` | S-Curve high-color offset (`-1..1`) | `0.0` | +| `shader_curve_offset_both` + | `float` | S-Curve both-color blend-point offset (`-1..1`) | `0.0` | +| `disable_hotkeys` | `bool` | Disable the global hotkeys (Depth/Convergence/profile/save) to avoid conflict with other 3D mods. `Ctrl + Home` and `Start + D-Pad Down` (OSD toggles) are unaffected | `false` | +| `dash_enable` | `bool` | Enable or disable SteamVR Dashboard and Home | `false` | +| `auto_focus` | `bool` | Enable or disable automatic focusing/bringing VRto3D to foreground | `true` | +| `hide_cursor` + | `bool` | Fully hide the OS cursor while the game is focused (blanks the system cursor set; restored on focus loss) | `false` | +| `lock_cursor` + | `bool` | Confine the OS cursor to the focused game window's client area (re-asserted continuously) | `false` | +| `stereo_cursor` + | `bool` | Hide the OS cursor and draw a per-eye 3D cursor in both eye views instead (also used for the OSD menu) | `false` | +| `cursor_depth` + | `float` | Stereo cursor per-eye shift in pixels: `0` = screen plane, positive = into the screen, negative = pop-out | `0.0` | +| `cursor_size` + | `int` | Stereo cursor arrow height in per-eye pixels | `32` | +| `pitch_enable` + | `bool` | Enables or disables Controller right stick y-axis mapped to HMD Pitch | `false` | +| `yaw_enable` + | `bool` | Enables or disables Controller right stick x-axis mapped to HMD Yaw | `false` | +| `use_open_track` | `bool` | Enables or disables OpenTrack 6DoF HMD Control | `false` | +| `open_track_port` | `int` | UDP Port for OpenTrack | `4242` | +| `use_track_filter` | `bool` | Enables or disables Accela-Hamilton style pose filtering for tracking rotation and position | `false` | +| `trk_flt_rot_sens` | `float` | Rotation smoothing threshold for track filter (lower = more smoothing) | `0.5` | +| `trk_flt_pos_sens` | `float` | Position smoothing threshold for track filter (lower = more smoothing) | `0.25` | +| `trk_flt_rot_dz` | `float` | Rotation deadzone used by track filter | `0.03` | +| `trk_flt_pos_dz` | `float` | Position deadzone used by track filter | `0.02` | +| `trk_flt_zoom_smooth` | `float` | Additional rotation smoothing when moving toward the display | `0.0` | +| `trk_flt_max_zoom` | `float` | Max Z distance used for scaling zoom smoothing | `10.0` | +| `sr_tracking_enabled` | `bool` | Enable the LeiaSR built-in head-tracking sender. Disable to feed OpenTrack from another source (e.g. the OpenTrack app) while in LeiaSR mode | `true` | +| `sr_filter_pos_mincutoff` | `float` | LeiaSR built-in head tracking: One-Euro position min cutoff | `0.08` | +| `sr_filter_pos_beta` | `float` | LeiaSR built-in head tracking: One-Euro position beta | `0.08` | +| `sr_filter_rot_mincutoff` | `float` | LeiaSR built-in head tracking: One-Euro rotation min cutoff | `0.12` | +| `sr_filter_rot_beta` | `float` | LeiaSR built-in head tracking: One-Euro rotation beta | `0.01` | +| `sr_angle_deadzone_deg` | `float` | LeiaSR built-in head tracking: angular deadzone in degrees | `0.2` | +| `sr_sens_yaw` | `float` | LeiaSR built-in head tracking: yaw sensitivity | `1.0` | +| `sr_sens_pitch` | `float` | LeiaSR built-in head tracking: pitch sensitivity | `1.0` | +| `sr_sens_roll` | `float` | LeiaSR built-in head tracking: roll sensitivity | `1.0` | +| `sr_max_yaw` | `float` | LeiaSR built-in head tracking: max yaw clamp (deg) | `70.0` | +| `sr_max_pitch` | `float` | LeiaSR built-in head tracking: max pitch clamp (deg) | `70.0` | +| `sr_max_roll` | `float` | LeiaSR built-in head tracking: max roll clamp (deg) | `70.0` | +| `sr_track_mode` | `string`| LeiaSR built-in head tracking mode (`"XYZ_YawPitch"`, `"XYZ"`, `"YawPitch"`, `"Full6DOF"`, `"YawPitchRoll"`) | `"XYZ_YawPitch"` | +| `launch_script` | `string`| Command executed once when VRto3D driver activates (e.g. `"start vertoxr://steamvr"`) | `""` | +| `pose_reset_key` + | `string`| The Virtual-Key Code to reset the HMD position and orientation | `"VK_NUMPAD7"` | +| `ctrl_toggle_key` + | `string`| The Virtual-Key Code to toggle Pitch and Yaw emulation on/off when they are enabled | `"VK_NUMPAD8"` | +| `ctrl_toggle_type` + | `string`| The ctrl_toggle_key's behavior (`"toggle"`, `"hold"`) | `"toggle"` | +| `pitch_radius` + | `float` | Radius of curvature for the HMD to pitch along. Useful in 3rd person VR games | `0.0` | +| `ctrl_deadzone` + | `float` | Controller Deadzone when using pitch or yaw emulation | `0.05` | +| `ctrl_sensitivity` + | `float` | Controller Sensitivity when using pitch or yaw emulation | `1.0` | +| `user_settings[].user_load_key` + | `string`| The Virtual-Key Code (or XInput chord) to load a user preset | `"VK_NUMPAD1"` | +| `user_settings[].user_key_type` + | `string`| The load key's behavior. Only `"toggle"` supports multi-preset cycles; `"switch"` and `"hold"` keep just the first value | `"switch"` | +| `user_settings[].user_depth` + | `float` or `[float]` | The separation value(s) for a user preset (array = cycle on each press, `toggle` mode only) | `0.1` | +| `user_settings[].user_convergence` + | `float` or `[float]` | The convergence value(s) for a user preset (array = cycle on each press, `toggle` mode only) | `1.0` | +| `user_settings[].user_fov` + | `float` or `[float]` | The fov value(s) for a user preset; `0` falls back to the global FoV (array = cycle on each press, `toggle` mode only) | `90.0` | + + +## Additional Settings +- The game's main window has to be in focus for control input from your mouse/keyboard/controller to work. Auto Focus should do this for you, but sometimes you may need to `Alt + Tab` to the game +- Overlays generally won't work on this virtual HMD +- XInput controller is recommended +- SteamVR doesn't support HDR currently + - AutoHDR may work, but some games will be too dark or too bright +- DLSS, TAA, and other temporal based settings can create a halo around objects. Most VR mods have fixes for this, but some may not + +#### Display Correction (Crosstalk Reduction) +- Some 3D displays show visible left/right crosstalk in high-contrast scenes, which can make some games hard to watch. The OSD `Shader` tab runs a configurable post-process pass on the final stereo image that trades some brightness/contrast for sharply less visible crosstalk - toggle it on/off instantly when needed +- Works with every output mode that produces a side-by-side internal frame (SbS, TaB, Interlaced, Checkerboard, Anaglyph, Mono, LeiaSR, NvidiaDX9, WibbleWobble, etc.) +- **Controls** on the `Shader` tab: + - `Enable Display Correction Shader` - master on/off (defaults off, no perf cost when off) + - **Lift / Gamma / Gain** - per-channel RGB controls (`0..2`, default `1.0`): `RGB Lift` raises/lowers shadows, `RGB Gamma` adjusts midtone gamma, `RGB Gain` scales highlights + - **S-Curve** - extended contrast curve. `Curve` (range `0.33..3.0`, default `1.0` = pass-through); below `1.0` *reduces* midtone contrast (the crosstalk-fix direction), above `1.0` *increases* it. `Curve Offset (Low / High / Both)` fine-tune the curve's low-end, high-end, and overall blend point (`-1..1`, default `0`) + - `Reset to Defaults` - snap everything back to pass-through +- **Example starting point** for a LeiaSR 3D monitor with heavy crosstalk (Acer/Samsung 27"): + + | Control | Value | + |---|---| + | `Enable Display Correction Shader` | on | + | `RGB Gamma` | `0.6, 0.6, 0.6` | + | `Curve` | `0.33` | + | `Curve Offset (Both)` | `-1.0` | + +- This combination strongly reduces midtone contrast (where most crosstalk is visible) while compressing the overall range. From there, tune `Curve` upward toward `1.0` until you find the smallest correction that hides the crosstalk in your worst-case scene - every step toward `1.0` recovers contrast +- Save with `Save Game Cfg` (per-game) or `Save Default Cfg` (global) in the OSD footer to persist the settings + +#### Controls +- If you want to use Steam Input + - Open Steam->Settings->Controller + - Toggle on `Enable Steam Input for Xbox Controllers` + - Click `Edit` on the `Desktop Layout` and then select `Disable Steam Input` + - On SteamVR's library page, click the `Controller Icon` and select `Disable Steam Input` + - Generally you need to start SteamVR first and separately from the game for Steam Input to work +- This project is primarily targeted for VR mods of flatscreen games, not full VR games. As such, there is currently no VR controller emulation +- You can setup a Lighthouse + Vive Tracker + tracked controllers with VRto3D for a seated play area. See [this guide](https://oneup03.github.io/VRto3D/wiki/Motion-Controls-&-Tracking) for details +- Several VR controller only games can be made to work by using Driver4VR, a paid SteamVR Vive controller emulator. Games with mainly pointer controls work ok. Games with a lot of interaction/movement don't work well. +- Optional XInput right-stick HMD Pitch / Yaw emulation can be turned on for games or mods that need it - configure it from the OSD `Tracking` tab (`Ctrl + Home` -> `XInput (Xbox) Controller`) + - Tick `Pitch (right stick)` and / or `Yaw (right stick)` to map them + - `Sensitivity`, `Stick Deadzone`, and `Pitch Radius` (curves the view along a semicircle - useful in 3rd-person games) all live in the same panel + - `Toggle Key` (default `VK_NUMPAD8`) toggles pitch/yaw on/off in-game; the `Mode` combo next to it switches between `toggle` and `hold` behavior. `Reset Key` (default `VK_NUMPAD7`) recenters. Both bindings accept any keyboard/mouse key or XInput button/chord - click `Set` to capture a single key/button or `Combo` to capture an XInput chord, or type a string from key_names.h directly +- OpenTrack 6DoF / 3DoF support is available over UDP loopback - tick `Enable OpenTrack` on the OSD `Tracking` tab (`OpenTrack` panel) and set the UDP port to match your sender (default `4242`; port changes need a SteamVR restart) + - Works alongside XInput Pitch/Yaw emulation and the HMD offsets + - SR Displays are supported natively (no bridge required) - enable OpenTrack with `output_mode: "LeiaSR"` and tune the One-Euro filter / sensitivities / clamps in the OSD `Tracking` tab's `LeiaSR Head Tracking` panel + - AR glasses are compatible via 3rd party apps like VertoXR - set the `Launch Script` field on the OSD `System` tab to `start vertoxr://steamvr` to auto-start it with SteamVR + - You can also use the OpenTrack app to do tracking with cameras, IMUs, phone apps, etc +- Track filtering of 6DoF/3DoF input can be enabled with `use_track_filter` + - This filter is useful for reducing jitter while preserving responsiveness through sensitivity/deadzone tuning + - Adjust filter sensitivities and deadzones from the OSD `Tracking` tab + +#### User Presets (via OSD) +- Press `Ctrl + Home` (or `Start + D-Pad Down` on a gamepad) to open the OSD menu and select the `User Hotkeys` tab +- Each row maps a Load key to a `(Depth, Convergence, FoV)` preset. Only `toggle` mode supports multi-preset cycles - `switch` and `hold` rows are trimmed to the first value on edit and on profile load + - Click `Set` to capture a single key, mouse button, or XInput button + - Click `Combo` to capture an XInput chord (e.g. `XINPUT_GAMEPAD_LEFT_SHOULDER+XINPUT_GAMEPAD_RIGHT_SHOULDER`); the chord commits when you release everything + - You can also type any Virtual-Key Code string into the Load field directly +- Choose the load behavior under `Mode`: + - `switch` - jump to the preset and stay there + - `toggle` - bounce between the preset and the previous setting every 1.5s + - `hold` - apply the preset only while the key is held +- Edit `Depth`, `Conv`, and `FoV` directly, or click `Copy Current` to fill them from the current Stereo tab values + - Comma-separated values create a cycle - each press of the Load key advances to the next entry (`toggle` mode only; `switch` and `hold` keep only the first value) + - `FoV = 0` falls back to the active profile's global FoV +- `+ Add Preset Row` adds a new empty row; the `X` button removes a row +- It is recommended to keep one `switch` preset that matches your default depth/convergence so you can easily get back to baseline +- If you swap between different convergence settings in-game, sometimes you will end up with black bars on the sides of the screen or you may not see a change immediately - reload/restart/reinitialize the VR mod to pick it up. Using a single convergence value across all presets in a game avoids the issue entirely +- Changes are live as soon as you edit them but only persist when you click `Save Game Cfg` or `Save Default Cfg` in the footer + +#### Profile Creation (via OSD) +1. Launch the game and let VRto3D load - the OSD title bar will show `Profile: (default)` if no per-game profile exists yet +2. Press `Ctrl + Home` (or `Start + D-Pad Down` on a gamepad) to open the OSD menu +3. **Stereo tab**: dial in `Depth`, `Convergence`, `FoV`, and toggle `Swap Eyes` / `Auto-Depth` as needed +4. **User Hotkeys tab**: add preset rows for the depth/convergence/fov values you want, bind their Load keys, and use `Copy Current` to capture the current values into a row +5. **Tracking tab** (only if the game needs it): set `Height` under `HMD Pose`, and enable `Pitch (right stick)` / `Yaw (right stick)` under `XInput (Xbox) Controller` for games that need camera control +6. **System tab**: toggle `Async Reprojection` under `Misc` if it helps smoothness for this game +7. In the footer click `Save Game Cfg` to write `Steam\config\vrto3d\Game.exe_config.json` (the button label includes the game name and is disabled until VRto3D detects a running game). `Save Default Cfg` writes the equivalent values to `default_config.json` for use as a global baseline +8. (Optional) Open the file in a text editor for final tweaks - making all convergence values match avoids rendering / performance issues, and you can hand-edit virtual-key strings or other fields the OSD doesn't expose +9. Restart the game; you should hear a beep when the profile loads. Continue tuning in-OSD and re-save with `Ctrl + F7` (or the footer button) at any time +10. Share your `Steam\config\vrto3d\Game.exe_config.json` with others + + +## Troubleshooting +- If SteamVR appears on the wrong display, set `display_index` to the correct display enumeration order (or pick it from the OSD `System` tab) and restart SteamVR +- If SteamVR crashes and disables add-ons, you will need to re-enable VRto3D in the SteamVR Status window +- The first thing to try is deleting your `Steam\config\steamvr.vrsettings` and `Steam\config\vrto3d\default_config.json` +- If you have used other SteamVR drivers that also create a virtual HMD, you will need to disable and/or uninstall them + - Run SteamVR + - On the SteamVR Status window, go to `Menu -> Settings` + - Change to the `Startup / Shutdown` tab + - Click `Manage Add-Ons` + - Turn `Off` any virtual HMD drivers (ALVR, VRidge, OpenTrack, VCR, iVRy, etc) + - if issues still arise, try a Clean SteamVR Install and delete your `Steam\steamapps\common\SteamVR` folder +- Conversely, to uninstall or disable VRto3D, you can disable it under the same `Manage Add-Ons` menu or delete the `vrto3d` folder from your `Steam\steamapps\common\SteamVR\drivers` folder +- If you have a VR headset and run into issues with this driver, here's some things to try: + - Disconnect VR headset from computer + - Clean SteamVR Install + - Set SteamVR as OpenXR Runtime + +#### Linux +- `vrto3d.txt` log: `/logs/vrto3d.txt` (plus stderr in vrserver.txt) +- No hotkeys → check `groups` includes `input`; the OSD shows a warning toast +- No output window → check `WAYLAND_DISPLAY`/`DISPLAY` reach vrserver (launch SteamVR from a desktop session, not a raw console) +- Screenshots land in `/steamapps/common/SteamVR/screenshots` + + +## Building + +#### Windows +- Clone the code and initialize submodules +- Define `STEAM_PATH` environment variable with the path to your main Steam folder +- Open Solution in Visual Studio 2022 or VSCode +- Use the solution to build this driver +- Build output is automatically copied to your `SteamVR\drivers` folder + +#### Linux +``` +cd vrto3d +cmake -B build -G Ninja +cmake --build build +``` +Dependencies: gcc/clang C++17, cmake, Vulkan headers, libX11 + libXrandr, wayland-client, libxkbcommon, libdrm headers. (Shaders ship pre-compiled as SPIR-V headers; regenerate with `shaders/compile_shaders.sh` if you edit them — needs glslc.) + +For quick iteration, register the build output in place instead of copying it: +``` +~/.local/share/Steam/steamapps/common/SteamVR/bin/vrpathreg.sh adddriver \ + /vrto3d/build/output/drivers/vrto3d +``` diff --git a/external/NV3D-Lib b/external/NV3D-Lib new file mode 160000 index 00000000..20db53d2 --- /dev/null +++ b/external/NV3D-Lib @@ -0,0 +1 @@ +Subproject commit 20db53d215052b4beb32e0684ced119d2e6510bf diff --git a/external/SR-lib b/external/SR-lib new file mode 160000 index 00000000..ba7f5c76 --- /dev/null +++ b/external/SR-lib @@ -0,0 +1 @@ +Subproject commit ba7f5c76049ef451c1ad266b4ecd99d14aba6bfc diff --git a/external/VRto3DLib b/external/VRto3DLib index 8d93d630..eccc3894 160000 --- a/external/VRto3DLib +++ b/external/VRto3DLib @@ -1 +1 @@ -Subproject commit 8d93d63057e856109e03c8b0df668c0d4f3a5202 +Subproject commit eccc38941d95b42e942325063d33d70170a41f3f diff --git a/external/adl_sdk b/external/adl_sdk new file mode 160000 index 00000000..84c881cb --- /dev/null +++ b/external/adl_sdk @@ -0,0 +1 @@ +Subproject commit 84c881cb2136f04287eb3af12de00a9e2867aa9e diff --git a/external/imgui b/external/imgui new file mode 160000 index 00000000..f5befd2d --- /dev/null +++ b/external/imgui @@ -0,0 +1 @@ +Subproject commit f5befd2d29e66809cd1110a152e375a7f1981f06 diff --git a/external/nvapi b/external/nvapi new file mode 160000 index 00000000..9b181ea5 --- /dev/null +++ b/external/nvapi @@ -0,0 +1 @@ +Subproject commit 9b181ea572f680327fe01a14a0f1f41c78034104 diff --git a/external/openvr b/external/openvr index ae46a8dd..09240643 160000 --- a/external/openvr +++ b/external/openvr @@ -1 +1 @@ -Subproject commit ae46a8dd0172580648c8922658a100439115d3eb +Subproject commit 0924064316de3effbcd1acf1e309182a2deb1c05 diff --git a/external/stb/stb_image_write.h b/external/stb/stb_image_write.h new file mode 100644 index 00000000..e4b32ed1 --- /dev/null +++ b/external/stb/stb_image_write.h @@ -0,0 +1,1724 @@ +/* stb_image_write - v1.16 - public domain - http://nothings.org/stb + writes out PNG/BMP/TGA/JPEG/HDR images to C stdio - Sean Barrett 2010-2015 + no warranty implied; use at your own risk + + Before #including, + + #define STB_IMAGE_WRITE_IMPLEMENTATION + + in the file that you want to have the implementation. + + Will probably not work correctly with strict-aliasing optimizations. + +ABOUT: + + This header file is a library for writing images to C stdio or a callback. + + The PNG output is not optimal; it is 20-50% larger than the file + written by a decent optimizing implementation; though providing a custom + zlib compress function (see STBIW_ZLIB_COMPRESS) can mitigate that. + This library is designed for source code compactness and simplicity, + not optimal image file size or run-time performance. + +BUILDING: + + You can #define STBIW_ASSERT(x) before the #include to avoid using assert.h. + You can #define STBIW_MALLOC(), STBIW_REALLOC(), and STBIW_FREE() to replace + malloc,realloc,free. + You can #define STBIW_MEMMOVE() to replace memmove() + You can #define STBIW_ZLIB_COMPRESS to use a custom zlib-style compress function + for PNG compression (instead of the builtin one), it must have the following signature: + unsigned char * my_compress(unsigned char *data, int data_len, int *out_len, int quality); + The returned data will be freed with STBIW_FREE() (free() by default), + so it must be heap allocated with STBIW_MALLOC() (malloc() by default), + +UNICODE: + + If compiling for Windows and you wish to use Unicode filenames, compile + with + #define STBIW_WINDOWS_UTF8 + and pass utf8-encoded filenames. Call stbiw_convert_wchar_to_utf8 to convert + Windows wchar_t filenames to utf8. + +USAGE: + + There are five functions, one for each image file format: + + int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); + int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); + int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); + int stbi_write_jpg(char const *filename, int w, int h, int comp, const void *data, int quality); + int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data); + + void stbi_flip_vertically_on_write(int flag); // flag is non-zero to flip data vertically + + There are also five equivalent functions that use an arbitrary write function. You are + expected to open/close your file-equivalent before and after calling these: + + int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes); + int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); + int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); + int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data); + int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality); + + where the callback is: + void stbi_write_func(void *context, void *data, int size); + + You can configure it with these global variables: + int stbi_write_tga_with_rle; // defaults to true; set to 0 to disable RLE + int stbi_write_png_compression_level; // defaults to 8; set to higher for more compression + int stbi_write_force_png_filter; // defaults to -1; set to 0..5 to force a filter mode + + + You can define STBI_WRITE_NO_STDIO to disable the file variant of these + functions, so the library will not use stdio.h at all. However, this will + also disable HDR writing, because it requires stdio for formatted output. + + Each function returns 0 on failure and non-0 on success. + + The functions create an image file defined by the parameters. The image + is a rectangle of pixels stored from left-to-right, top-to-bottom. + Each pixel contains 'comp' channels of data stored interleaved with 8-bits + per channel, in the following order: 1=Y, 2=YA, 3=RGB, 4=RGBA. (Y is + monochrome color.) The rectangle is 'w' pixels wide and 'h' pixels tall. + The *data pointer points to the first byte of the top-left-most pixel. + For PNG, "stride_in_bytes" is the distance in bytes from the first byte of + a row of pixels to the first byte of the next row of pixels. + + PNG creates output files with the same number of components as the input. + The BMP format expands Y to RGB in the file format and does not + output alpha. + + PNG supports writing rectangles of data even when the bytes storing rows of + data are not consecutive in memory (e.g. sub-rectangles of a larger image), + by supplying the stride between the beginning of adjacent rows. The other + formats do not. (Thus you cannot write a native-format BMP through the BMP + writer, both because it is in BGR order and because it may have padding + at the end of the line.) + + PNG allows you to set the deflate compression level by setting the global + variable 'stbi_write_png_compression_level' (it defaults to 8). + + HDR expects linear float data. Since the format is always 32-bit rgb(e) + data, alpha (if provided) is discarded, and for monochrome data it is + replicated across all three channels. + + TGA supports RLE or non-RLE compressed data. To use non-RLE-compressed + data, set the global variable 'stbi_write_tga_with_rle' to 0. + + JPEG does ignore alpha channels in input data; quality is between 1 and 100. + Higher quality looks better but results in a bigger image. + JPEG baseline (no JPEG progressive). + +CREDITS: + + + Sean Barrett - PNG/BMP/TGA + Baldur Karlsson - HDR + Jean-Sebastien Guay - TGA monochrome + Tim Kelsey - misc enhancements + Alan Hickman - TGA RLE + Emmanuel Julien - initial file IO callback implementation + Jon Olick - original jo_jpeg.cpp code + Daniel Gibson - integrate JPEG, allow external zlib + Aarni Koskela - allow choosing PNG filter + + bugfixes: + github:Chribba + Guillaume Chereau + github:jry2 + github:romigrou + Sergio Gonzalez + Jonas Karlsson + Filip Wasil + Thatcher Ulrich + github:poppolopoppo + Patrick Boettcher + github:xeekworx + Cap Petschulat + Simon Rodriguez + Ivan Tikhonov + github:ignotion + Adam Schackart + Andrew Kensler + +LICENSE + + See end of file for license information. + +*/ + +#ifndef INCLUDE_STB_IMAGE_WRITE_H +#define INCLUDE_STB_IMAGE_WRITE_H + +#include + +// if STB_IMAGE_WRITE_STATIC causes problems, try defining STBIWDEF to 'inline' or 'static inline' +#ifndef STBIWDEF +#ifdef STB_IMAGE_WRITE_STATIC +#define STBIWDEF static +#else +#ifdef __cplusplus +#define STBIWDEF extern "C" +#else +#define STBIWDEF extern +#endif +#endif +#endif + +#ifndef STB_IMAGE_WRITE_STATIC // C++ forbids static forward declarations +STBIWDEF int stbi_write_tga_with_rle; +STBIWDEF int stbi_write_png_compression_level; +STBIWDEF int stbi_write_force_png_filter; +#endif + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); +STBIWDEF int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data); +STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void *data, int quality); + +#ifdef STBIW_WINDOWS_UTF8 +STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input); +#endif +#endif + +typedef void stbi_write_func(void *context, void *data, int size); + +STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes); +STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data); +STBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality); + +STBIWDEF void stbi_flip_vertically_on_write(int flip_boolean); + +#endif//INCLUDE_STB_IMAGE_WRITE_H + +#ifdef STB_IMAGE_WRITE_IMPLEMENTATION + +#ifdef _WIN32 + #ifndef _CRT_SECURE_NO_WARNINGS + #define _CRT_SECURE_NO_WARNINGS + #endif + #ifndef _CRT_NONSTDC_NO_DEPRECATE + #define _CRT_NONSTDC_NO_DEPRECATE + #endif +#endif + +#ifndef STBI_WRITE_NO_STDIO +#include +#endif // STBI_WRITE_NO_STDIO + +#include +#include +#include +#include + +#if defined(STBIW_MALLOC) && defined(STBIW_FREE) && (defined(STBIW_REALLOC) || defined(STBIW_REALLOC_SIZED)) +// ok +#elif !defined(STBIW_MALLOC) && !defined(STBIW_FREE) && !defined(STBIW_REALLOC) && !defined(STBIW_REALLOC_SIZED) +// ok +#else +#error "Must define all or none of STBIW_MALLOC, STBIW_FREE, and STBIW_REALLOC (or STBIW_REALLOC_SIZED)." +#endif + +#ifndef STBIW_MALLOC +#define STBIW_MALLOC(sz) malloc(sz) +#define STBIW_REALLOC(p,newsz) realloc(p,newsz) +#define STBIW_FREE(p) free(p) +#endif + +#ifndef STBIW_REALLOC_SIZED +#define STBIW_REALLOC_SIZED(p,oldsz,newsz) STBIW_REALLOC(p,newsz) +#endif + + +#ifndef STBIW_MEMMOVE +#define STBIW_MEMMOVE(a,b,sz) memmove(a,b,sz) +#endif + + +#ifndef STBIW_ASSERT +#include +#define STBIW_ASSERT(x) assert(x) +#endif + +#define STBIW_UCHAR(x) (unsigned char) ((x) & 0xff) + +#ifdef STB_IMAGE_WRITE_STATIC +static int stbi_write_png_compression_level = 8; +static int stbi_write_tga_with_rle = 1; +static int stbi_write_force_png_filter = -1; +#else +int stbi_write_png_compression_level = 8; +int stbi_write_tga_with_rle = 1; +int stbi_write_force_png_filter = -1; +#endif + +static int stbi__flip_vertically_on_write = 0; + +STBIWDEF void stbi_flip_vertically_on_write(int flag) +{ + stbi__flip_vertically_on_write = flag; +} + +typedef struct +{ + stbi_write_func *func; + void *context; + unsigned char buffer[64]; + int buf_used; +} stbi__write_context; + +// initialize a callback-based context +static void stbi__start_write_callbacks(stbi__write_context *s, stbi_write_func *c, void *context) +{ + s->func = c; + s->context = context; +} + +#ifndef STBI_WRITE_NO_STDIO + +static void stbi__stdio_write(void *context, void *data, int size) +{ + fwrite(data,1,size,(FILE*) context); +} + +#if defined(_WIN32) && defined(STBIW_WINDOWS_UTF8) +#ifdef __cplusplus +#define STBIW_EXTERN extern "C" +#else +#define STBIW_EXTERN extern +#endif +STBIW_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, wchar_t *widestr, int cchwide); +STBIW_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const wchar_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default); + +STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input) +{ + return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL); +} +#endif + +static FILE *stbiw__fopen(char const *filename, char const *mode) +{ + FILE *f; +#if defined(_WIN32) && defined(STBIW_WINDOWS_UTF8) + wchar_t wMode[64]; + wchar_t wFilename[1024]; + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)/sizeof(*wFilename))) + return 0; + + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)/sizeof(*wMode))) + return 0; + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != _wfopen_s(&f, wFilename, wMode)) + f = 0; +#else + f = _wfopen(wFilename, wMode); +#endif + +#elif defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != fopen_s(&f, filename, mode)) + f=0; +#else + f = fopen(filename, mode); +#endif + return f; +} + +static int stbi__start_write_file(stbi__write_context *s, const char *filename) +{ + FILE *f = stbiw__fopen(filename, "wb"); + stbi__start_write_callbacks(s, stbi__stdio_write, (void *) f); + return f != NULL; +} + +static void stbi__end_write_file(stbi__write_context *s) +{ + fclose((FILE *)s->context); +} + +#endif // !STBI_WRITE_NO_STDIO + +typedef unsigned int stbiw_uint32; +typedef int stb_image_write_test[sizeof(stbiw_uint32)==4 ? 1 : -1]; + +static void stbiw__writefv(stbi__write_context *s, const char *fmt, va_list v) +{ + while (*fmt) { + switch (*fmt++) { + case ' ': break; + case '1': { unsigned char x = STBIW_UCHAR(va_arg(v, int)); + s->func(s->context,&x,1); + break; } + case '2': { int x = va_arg(v,int); + unsigned char b[2]; + b[0] = STBIW_UCHAR(x); + b[1] = STBIW_UCHAR(x>>8); + s->func(s->context,b,2); + break; } + case '4': { stbiw_uint32 x = va_arg(v,int); + unsigned char b[4]; + b[0]=STBIW_UCHAR(x); + b[1]=STBIW_UCHAR(x>>8); + b[2]=STBIW_UCHAR(x>>16); + b[3]=STBIW_UCHAR(x>>24); + s->func(s->context,b,4); + break; } + default: + STBIW_ASSERT(0); + return; + } + } +} + +static void stbiw__writef(stbi__write_context *s, const char *fmt, ...) +{ + va_list v; + va_start(v, fmt); + stbiw__writefv(s, fmt, v); + va_end(v); +} + +static void stbiw__write_flush(stbi__write_context *s) +{ + if (s->buf_used) { + s->func(s->context, &s->buffer, s->buf_used); + s->buf_used = 0; + } +} + +static void stbiw__putc(stbi__write_context *s, unsigned char c) +{ + s->func(s->context, &c, 1); +} + +static void stbiw__write1(stbi__write_context *s, unsigned char a) +{ + if ((size_t)s->buf_used + 1 > sizeof(s->buffer)) + stbiw__write_flush(s); + s->buffer[s->buf_used++] = a; +} + +static void stbiw__write3(stbi__write_context *s, unsigned char a, unsigned char b, unsigned char c) +{ + int n; + if ((size_t)s->buf_used + 3 > sizeof(s->buffer)) + stbiw__write_flush(s); + n = s->buf_used; + s->buf_used = n+3; + s->buffer[n+0] = a; + s->buffer[n+1] = b; + s->buffer[n+2] = c; +} + +static void stbiw__write_pixel(stbi__write_context *s, int rgb_dir, int comp, int write_alpha, int expand_mono, unsigned char *d) +{ + unsigned char bg[3] = { 255, 0, 255}, px[3]; + int k; + + if (write_alpha < 0) + stbiw__write1(s, d[comp - 1]); + + switch (comp) { + case 2: // 2 pixels = mono + alpha, alpha is written separately, so same as 1-channel case + case 1: + if (expand_mono) + stbiw__write3(s, d[0], d[0], d[0]); // monochrome bmp + else + stbiw__write1(s, d[0]); // monochrome TGA + break; + case 4: + if (!write_alpha) { + // composite against pink background + for (k = 0; k < 3; ++k) + px[k] = bg[k] + ((d[k] - bg[k]) * d[3]) / 255; + stbiw__write3(s, px[1 - rgb_dir], px[1], px[1 + rgb_dir]); + break; + } + /* FALLTHROUGH */ + case 3: + stbiw__write3(s, d[1 - rgb_dir], d[1], d[1 + rgb_dir]); + break; + } + if (write_alpha > 0) + stbiw__write1(s, d[comp - 1]); +} + +static void stbiw__write_pixels(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, void *data, int write_alpha, int scanline_pad, int expand_mono) +{ + stbiw_uint32 zero = 0; + int i,j, j_end; + + if (y <= 0) + return; + + if (stbi__flip_vertically_on_write) + vdir *= -1; + + if (vdir < 0) { + j_end = -1; j = y-1; + } else { + j_end = y; j = 0; + } + + for (; j != j_end; j += vdir) { + for (i=0; i < x; ++i) { + unsigned char *d = (unsigned char *) data + (j*x+i)*comp; + stbiw__write_pixel(s, rgb_dir, comp, write_alpha, expand_mono, d); + } + stbiw__write_flush(s); + s->func(s->context, &zero, scanline_pad); + } +} + +static int stbiw__outfile(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, int expand_mono, void *data, int alpha, int pad, const char *fmt, ...) +{ + if (y < 0 || x < 0) { + return 0; + } else { + va_list v; + va_start(v, fmt); + stbiw__writefv(s, fmt, v); + va_end(v); + stbiw__write_pixels(s,rgb_dir,vdir,x,y,comp,data,alpha,pad, expand_mono); + return 1; + } +} + +static int stbi_write_bmp_core(stbi__write_context *s, int x, int y, int comp, const void *data) +{ + if (comp != 4) { + // write RGB bitmap + int pad = (-x*3) & 3; + return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *) data,0,pad, + "11 4 22 4" "4 44 22 444444", + 'B', 'M', 14+40+(x*3+pad)*y, 0,0, 14+40, // file header + 40, x,y, 1,24, 0,0,0,0,0,0); // bitmap header + } else { + // RGBA bitmaps need a v4 header + // use BI_BITFIELDS mode with 32bpp and alpha mask + // (straight BI_RGB with alpha mask doesn't work in most readers) + return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *)data,1,0, + "11 4 22 4" "4 44 22 444444 4444 4 444 444 444 444", + 'B', 'M', 14+108+x*y*4, 0, 0, 14+108, // file header + 108, x,y, 1,32, 3,0,0,0,0,0, 0xff0000,0xff00,0xff,0xff000000u, 0, 0,0,0, 0,0,0, 0,0,0, 0,0,0); // bitmap V4 header + } +} + +STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_bmp_core(&s, x, y, comp, data); +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_bmp(char const *filename, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_bmp_core(&s, x, y, comp, data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif //!STBI_WRITE_NO_STDIO + +static int stbi_write_tga_core(stbi__write_context *s, int x, int y, int comp, void *data) +{ + int has_alpha = (comp == 2 || comp == 4); + int colorbytes = has_alpha ? comp-1 : comp; + int format = colorbytes < 2 ? 3 : 2; // 3 color channels (RGB/RGBA) = 2, 1 color channel (Y/YA) = 3 + + if (y < 0 || x < 0) + return 0; + + if (!stbi_write_tga_with_rle) { + return stbiw__outfile(s, -1, -1, x, y, comp, 0, (void *) data, has_alpha, 0, + "111 221 2222 11", 0, 0, format, 0, 0, 0, 0, 0, x, y, (colorbytes + has_alpha) * 8, has_alpha * 8); + } else { + int i,j,k; + int jend, jdir; + + stbiw__writef(s, "111 221 2222 11", 0,0,format+8, 0,0,0, 0,0,x,y, (colorbytes + has_alpha) * 8, has_alpha * 8); + + if (stbi__flip_vertically_on_write) { + j = 0; + jend = y; + jdir = 1; + } else { + j = y-1; + jend = -1; + jdir = -1; + } + for (; j != jend; j += jdir) { + unsigned char *row = (unsigned char *) data + j * x * comp; + int len; + + for (i = 0; i < x; i += len) { + unsigned char *begin = row + i * comp; + int diff = 1; + len = 1; + + if (i < x - 1) { + ++len; + diff = memcmp(begin, row + (i + 1) * comp, comp); + if (diff) { + const unsigned char *prev = begin; + for (k = i + 2; k < x && len < 128; ++k) { + if (memcmp(prev, row + k * comp, comp)) { + prev += comp; + ++len; + } else { + --len; + break; + } + } + } else { + for (k = i + 2; k < x && len < 128; ++k) { + if (!memcmp(begin, row + k * comp, comp)) { + ++len; + } else { + break; + } + } + } + } + + if (diff) { + unsigned char header = STBIW_UCHAR(len - 1); + stbiw__write1(s, header); + for (k = 0; k < len; ++k) { + stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin + k * comp); + } + } else { + unsigned char header = STBIW_UCHAR(len - 129); + stbiw__write1(s, header); + stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin); + } + } + } + stbiw__write_flush(s); + } + return 1; +} + +STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_tga_core(&s, x, y, comp, (void *) data); +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_tga(char const *filename, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_tga_core(&s, x, y, comp, (void *) data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif + +// ************************************************************************************************* +// Radiance RGBE HDR writer +// by Baldur Karlsson + +#define stbiw__max(a, b) ((a) > (b) ? (a) : (b)) + +#ifndef STBI_WRITE_NO_STDIO + +static void stbiw__linear_to_rgbe(unsigned char *rgbe, float *linear) +{ + int exponent; + float maxcomp = stbiw__max(linear[0], stbiw__max(linear[1], linear[2])); + + if (maxcomp < 1e-32f) { + rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0; + } else { + float normalize = (float) frexp(maxcomp, &exponent) * 256.0f/maxcomp; + + rgbe[0] = (unsigned char)(linear[0] * normalize); + rgbe[1] = (unsigned char)(linear[1] * normalize); + rgbe[2] = (unsigned char)(linear[2] * normalize); + rgbe[3] = (unsigned char)(exponent + 128); + } +} + +static void stbiw__write_run_data(stbi__write_context *s, int length, unsigned char databyte) +{ + unsigned char lengthbyte = STBIW_UCHAR(length+128); + STBIW_ASSERT(length+128 <= 255); + s->func(s->context, &lengthbyte, 1); + s->func(s->context, &databyte, 1); +} + +static void stbiw__write_dump_data(stbi__write_context *s, int length, unsigned char *data) +{ + unsigned char lengthbyte = STBIW_UCHAR(length); + STBIW_ASSERT(length <= 128); // inconsistent with spec but consistent with official code + s->func(s->context, &lengthbyte, 1); + s->func(s->context, data, length); +} + +static void stbiw__write_hdr_scanline(stbi__write_context *s, int width, int ncomp, unsigned char *scratch, float *scanline) +{ + unsigned char scanlineheader[4] = { 2, 2, 0, 0 }; + unsigned char rgbe[4]; + float linear[3]; + int x; + + scanlineheader[2] = (width&0xff00)>>8; + scanlineheader[3] = (width&0x00ff); + + /* skip RLE for images too small or large */ + if (width < 8 || width >= 32768) { + for (x=0; x < width; x++) { + switch (ncomp) { + case 4: /* fallthrough */ + case 3: linear[2] = scanline[x*ncomp + 2]; + linear[1] = scanline[x*ncomp + 1]; + linear[0] = scanline[x*ncomp + 0]; + break; + default: + linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0]; + break; + } + stbiw__linear_to_rgbe(rgbe, linear); + s->func(s->context, rgbe, 4); + } + } else { + int c,r; + /* encode into scratch buffer */ + for (x=0; x < width; x++) { + switch(ncomp) { + case 4: /* fallthrough */ + case 3: linear[2] = scanline[x*ncomp + 2]; + linear[1] = scanline[x*ncomp + 1]; + linear[0] = scanline[x*ncomp + 0]; + break; + default: + linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0]; + break; + } + stbiw__linear_to_rgbe(rgbe, linear); + scratch[x + width*0] = rgbe[0]; + scratch[x + width*1] = rgbe[1]; + scratch[x + width*2] = rgbe[2]; + scratch[x + width*3] = rgbe[3]; + } + + s->func(s->context, scanlineheader, 4); + + /* RLE each component separately */ + for (c=0; c < 4; c++) { + unsigned char *comp = &scratch[width*c]; + + x = 0; + while (x < width) { + // find first run + r = x; + while (r+2 < width) { + if (comp[r] == comp[r+1] && comp[r] == comp[r+2]) + break; + ++r; + } + if (r+2 >= width) + r = width; + // dump up to first run + while (x < r) { + int len = r-x; + if (len > 128) len = 128; + stbiw__write_dump_data(s, len, &comp[x]); + x += len; + } + // if there's a run, output it + if (r+2 < width) { // same test as what we break out of in search loop, so only true if we break'd + // find next byte after run + while (r < width && comp[r] == comp[x]) + ++r; + // output run up to r + while (x < r) { + int len = r-x; + if (len > 127) len = 127; + stbiw__write_run_data(s, len, comp[x]); + x += len; + } + } + } + } + } +} + +static int stbi_write_hdr_core(stbi__write_context *s, int x, int y, int comp, float *data) +{ + if (y <= 0 || x <= 0 || data == NULL) + return 0; + else { + // Each component is stored separately. Allocate scratch space for full output scanline. + unsigned char *scratch = (unsigned char *) STBIW_MALLOC(x*4); + int i, len; + char buffer[128]; + char header[] = "#?RADIANCE\n# Written by stb_image_write.h\nFORMAT=32-bit_rle_rgbe\n"; + s->func(s->context, header, sizeof(header)-1); + +#ifdef __STDC_LIB_EXT1__ + len = sprintf_s(buffer, sizeof(buffer), "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); +#else + len = sprintf(buffer, "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); +#endif + s->func(s->context, buffer, len); + + for(i=0; i < y; i++) + stbiw__write_hdr_scanline(s, x, comp, scratch, data + comp*x*(stbi__flip_vertically_on_write ? y-1-i : i)); + STBIW_FREE(scratch); + return 1; + } +} + +STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const float *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_hdr_core(&s, x, y, comp, (float *) data); +} + +STBIWDEF int stbi_write_hdr(char const *filename, int x, int y, int comp, const float *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_hdr_core(&s, x, y, comp, (float *) data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif // STBI_WRITE_NO_STDIO + + +////////////////////////////////////////////////////////////////////////////// +// +// PNG writer +// + +#ifndef STBIW_ZLIB_COMPRESS +// stretchy buffer; stbiw__sbpush() == vector<>::push_back() -- stbiw__sbcount() == vector<>::size() +#define stbiw__sbraw(a) ((int *) (void *) (a) - 2) +#define stbiw__sbm(a) stbiw__sbraw(a)[0] +#define stbiw__sbn(a) stbiw__sbraw(a)[1] + +#define stbiw__sbneedgrow(a,n) ((a)==0 || stbiw__sbn(a)+n >= stbiw__sbm(a)) +#define stbiw__sbmaybegrow(a,n) (stbiw__sbneedgrow(a,(n)) ? stbiw__sbgrow(a,n) : 0) +#define stbiw__sbgrow(a,n) stbiw__sbgrowf((void **) &(a), (n), sizeof(*(a))) + +#define stbiw__sbpush(a, v) (stbiw__sbmaybegrow(a,1), (a)[stbiw__sbn(a)++] = (v)) +#define stbiw__sbcount(a) ((a) ? stbiw__sbn(a) : 0) +#define stbiw__sbfree(a) ((a) ? STBIW_FREE(stbiw__sbraw(a)),0 : 0) + +static void *stbiw__sbgrowf(void **arr, int increment, int itemsize) +{ + int m = *arr ? 2*stbiw__sbm(*arr)+increment : increment+1; + void *p = STBIW_REALLOC_SIZED(*arr ? stbiw__sbraw(*arr) : 0, *arr ? (stbiw__sbm(*arr)*itemsize + sizeof(int)*2) : 0, itemsize * m + sizeof(int)*2); + STBIW_ASSERT(p); + if (p) { + if (!*arr) ((int *) p)[1] = 0; + *arr = (void *) ((int *) p + 2); + stbiw__sbm(*arr) = m; + } + return *arr; +} + +static unsigned char *stbiw__zlib_flushf(unsigned char *data, unsigned int *bitbuffer, int *bitcount) +{ + while (*bitcount >= 8) { + stbiw__sbpush(data, STBIW_UCHAR(*bitbuffer)); + *bitbuffer >>= 8; + *bitcount -= 8; + } + return data; +} + +static int stbiw__zlib_bitrev(int code, int codebits) +{ + int res=0; + while (codebits--) { + res = (res << 1) | (code & 1); + code >>= 1; + } + return res; +} + +static unsigned int stbiw__zlib_countm(unsigned char *a, unsigned char *b, int limit) +{ + int i; + for (i=0; i < limit && i < 258; ++i) + if (a[i] != b[i]) break; + return i; +} + +static unsigned int stbiw__zhash(unsigned char *data) +{ + stbiw_uint32 hash = data[0] + (data[1] << 8) + (data[2] << 16); + hash ^= hash << 3; + hash += hash >> 5; + hash ^= hash << 4; + hash += hash >> 17; + hash ^= hash << 25; + hash += hash >> 6; + return hash; +} + +#define stbiw__zlib_flush() (out = stbiw__zlib_flushf(out, &bitbuf, &bitcount)) +#define stbiw__zlib_add(code,codebits) \ + (bitbuf |= (code) << bitcount, bitcount += (codebits), stbiw__zlib_flush()) +#define stbiw__zlib_huffa(b,c) stbiw__zlib_add(stbiw__zlib_bitrev(b,c),c) +// default huffman tables +#define stbiw__zlib_huff1(n) stbiw__zlib_huffa(0x30 + (n), 8) +#define stbiw__zlib_huff2(n) stbiw__zlib_huffa(0x190 + (n)-144, 9) +#define stbiw__zlib_huff3(n) stbiw__zlib_huffa(0 + (n)-256,7) +#define stbiw__zlib_huff4(n) stbiw__zlib_huffa(0xc0 + (n)-280,8) +#define stbiw__zlib_huff(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : (n) <= 255 ? stbiw__zlib_huff2(n) : (n) <= 279 ? stbiw__zlib_huff3(n) : stbiw__zlib_huff4(n)) +#define stbiw__zlib_huffb(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : stbiw__zlib_huff2(n)) + +#define stbiw__ZHASH 16384 + +#endif // STBIW_ZLIB_COMPRESS + +STBIWDEF unsigned char * stbi_zlib_compress(unsigned char *data, int data_len, int *out_len, int quality) +{ +#ifdef STBIW_ZLIB_COMPRESS + // user provided a zlib compress implementation, use that + return STBIW_ZLIB_COMPRESS(data, data_len, out_len, quality); +#else // use builtin + static unsigned short lengthc[] = { 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258, 259 }; + static unsigned char lengtheb[]= { 0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 }; + static unsigned short distc[] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577, 32768 }; + static unsigned char disteb[] = { 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13 }; + unsigned int bitbuf=0; + int i,j, bitcount=0; + unsigned char *out = NULL; + unsigned char ***hash_table = (unsigned char***) STBIW_MALLOC(stbiw__ZHASH * sizeof(unsigned char**)); + if (hash_table == NULL) + return NULL; + if (quality < 5) quality = 5; + + stbiw__sbpush(out, 0x78); // DEFLATE 32K window + stbiw__sbpush(out, 0x5e); // FLEVEL = 1 + stbiw__zlib_add(1,1); // BFINAL = 1 + stbiw__zlib_add(1,2); // BTYPE = 1 -- fixed huffman + + for (i=0; i < stbiw__ZHASH; ++i) + hash_table[i] = NULL; + + i=0; + while (i < data_len-3) { + // hash next 3 bytes of data to be compressed + int h = stbiw__zhash(data+i)&(stbiw__ZHASH-1), best=3; + unsigned char *bestloc = 0; + unsigned char **hlist = hash_table[h]; + int n = stbiw__sbcount(hlist); + for (j=0; j < n; ++j) { + if (hlist[j]-data > i-32768) { // if entry lies within window + int d = stbiw__zlib_countm(hlist[j], data+i, data_len-i); + if (d >= best) { best=d; bestloc=hlist[j]; } + } + } + // when hash table entry is too long, delete half the entries + if (hash_table[h] && stbiw__sbn(hash_table[h]) == 2*quality) { + STBIW_MEMMOVE(hash_table[h], hash_table[h]+quality, sizeof(hash_table[h][0])*quality); + stbiw__sbn(hash_table[h]) = quality; + } + stbiw__sbpush(hash_table[h],data+i); + + if (bestloc) { + // "lazy matching" - check match at *next* byte, and if it's better, do cur byte as literal + h = stbiw__zhash(data+i+1)&(stbiw__ZHASH-1); + hlist = hash_table[h]; + n = stbiw__sbcount(hlist); + for (j=0; j < n; ++j) { + if (hlist[j]-data > i-32767) { + int e = stbiw__zlib_countm(hlist[j], data+i+1, data_len-i-1); + if (e > best) { // if next match is better, bail on current match + bestloc = NULL; + break; + } + } + } + } + + if (bestloc) { + int d = (int) (data+i - bestloc); // distance back + STBIW_ASSERT(d <= 32767 && best <= 258); + for (j=0; best > lengthc[j+1]-1; ++j); + stbiw__zlib_huff(j+257); + if (lengtheb[j]) stbiw__zlib_add(best - lengthc[j], lengtheb[j]); + for (j=0; d > distc[j+1]-1; ++j); + stbiw__zlib_add(stbiw__zlib_bitrev(j,5),5); + if (disteb[j]) stbiw__zlib_add(d - distc[j], disteb[j]); + i += best; + } else { + stbiw__zlib_huffb(data[i]); + ++i; + } + } + // write out final bytes + for (;i < data_len; ++i) + stbiw__zlib_huffb(data[i]); + stbiw__zlib_huff(256); // end of block + // pad with 0 bits to byte boundary + while (bitcount) + stbiw__zlib_add(0,1); + + for (i=0; i < stbiw__ZHASH; ++i) + (void) stbiw__sbfree(hash_table[i]); + STBIW_FREE(hash_table); + + // store uncompressed instead if compression was worse + if (stbiw__sbn(out) > data_len + 2 + ((data_len+32766)/32767)*5) { + stbiw__sbn(out) = 2; // truncate to DEFLATE 32K window and FLEVEL = 1 + for (j = 0; j < data_len;) { + int blocklen = data_len - j; + if (blocklen > 32767) blocklen = 32767; + stbiw__sbpush(out, data_len - j == blocklen); // BFINAL = ?, BTYPE = 0 -- no compression + stbiw__sbpush(out, STBIW_UCHAR(blocklen)); // LEN + stbiw__sbpush(out, STBIW_UCHAR(blocklen >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(~blocklen)); // NLEN + stbiw__sbpush(out, STBIW_UCHAR(~blocklen >> 8)); + memcpy(out+stbiw__sbn(out), data+j, blocklen); + stbiw__sbn(out) += blocklen; + j += blocklen; + } + } + + { + // compute adler32 on input + unsigned int s1=1, s2=0; + int blocklen = (int) (data_len % 5552); + j=0; + while (j < data_len) { + for (i=0; i < blocklen; ++i) { s1 += data[j+i]; s2 += s1; } + s1 %= 65521; s2 %= 65521; + j += blocklen; + blocklen = 5552; + } + stbiw__sbpush(out, STBIW_UCHAR(s2 >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(s2)); + stbiw__sbpush(out, STBIW_UCHAR(s1 >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(s1)); + } + *out_len = stbiw__sbn(out); + // make returned pointer freeable + STBIW_MEMMOVE(stbiw__sbraw(out), out, *out_len); + return (unsigned char *) stbiw__sbraw(out); +#endif // STBIW_ZLIB_COMPRESS +} + +static unsigned int stbiw__crc32(unsigned char *buffer, int len) +{ +#ifdef STBIW_CRC32 + return STBIW_CRC32(buffer, len); +#else + static unsigned int crc_table[256] = + { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, + 0x0eDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, + 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, + 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, + 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, + 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, + 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, + 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, + 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, + 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, + 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, + 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, + 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, + 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, + 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, + 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, + 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, + 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, + 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, + 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, + 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, + 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D + }; + + unsigned int crc = ~0u; + int i; + for (i=0; i < len; ++i) + crc = (crc >> 8) ^ crc_table[buffer[i] ^ (crc & 0xff)]; + return ~crc; +#endif +} + +#define stbiw__wpng4(o,a,b,c,d) ((o)[0]=STBIW_UCHAR(a),(o)[1]=STBIW_UCHAR(b),(o)[2]=STBIW_UCHAR(c),(o)[3]=STBIW_UCHAR(d),(o)+=4) +#define stbiw__wp32(data,v) stbiw__wpng4(data, (v)>>24,(v)>>16,(v)>>8,(v)); +#define stbiw__wptag(data,s) stbiw__wpng4(data, s[0],s[1],s[2],s[3]) + +static void stbiw__wpcrc(unsigned char **data, int len) +{ + unsigned int crc = stbiw__crc32(*data - len - 4, len+4); + stbiw__wp32(*data, crc); +} + +static unsigned char stbiw__paeth(int a, int b, int c) +{ + int p = a + b - c, pa = abs(p-a), pb = abs(p-b), pc = abs(p-c); + if (pa <= pb && pa <= pc) return STBIW_UCHAR(a); + if (pb <= pc) return STBIW_UCHAR(b); + return STBIW_UCHAR(c); +} + +// @OPTIMIZE: provide an option that always forces left-predict or paeth predict +static void stbiw__encode_png_line(unsigned char *pixels, int stride_bytes, int width, int height, int y, int n, int filter_type, signed char *line_buffer) +{ + static int mapping[] = { 0,1,2,3,4 }; + static int firstmap[] = { 0,1,0,5,6 }; + int *mymap = (y != 0) ? mapping : firstmap; + int i; + int type = mymap[filter_type]; + unsigned char *z = pixels + stride_bytes * (stbi__flip_vertically_on_write ? height-1-y : y); + int signed_stride = stbi__flip_vertically_on_write ? -stride_bytes : stride_bytes; + + if (type==0) { + memcpy(line_buffer, z, width*n); + return; + } + + // first loop isn't optimized since it's just one pixel + for (i = 0; i < n; ++i) { + switch (type) { + case 1: line_buffer[i] = z[i]; break; + case 2: line_buffer[i] = z[i] - z[i-signed_stride]; break; + case 3: line_buffer[i] = z[i] - (z[i-signed_stride]>>1); break; + case 4: line_buffer[i] = (signed char) (z[i] - stbiw__paeth(0,z[i-signed_stride],0)); break; + case 5: line_buffer[i] = z[i]; break; + case 6: line_buffer[i] = z[i]; break; + } + } + switch (type) { + case 1: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-n]; break; + case 2: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-signed_stride]; break; + case 3: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - ((z[i-n] + z[i-signed_stride])>>1); break; + case 4: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], z[i-signed_stride], z[i-signed_stride-n]); break; + case 5: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - (z[i-n]>>1); break; + case 6: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], 0,0); break; + } +} + +STBIWDEF unsigned char *stbi_write_png_to_mem(const unsigned char *pixels, int stride_bytes, int x, int y, int n, int *out_len) +{ + int force_filter = stbi_write_force_png_filter; + int ctype[5] = { -1, 0, 4, 2, 6 }; + unsigned char sig[8] = { 137,80,78,71,13,10,26,10 }; + unsigned char *out,*o, *filt, *zlib; + signed char *line_buffer; + int j,zlen; + + if (stride_bytes == 0) + stride_bytes = x * n; + + if (force_filter >= 5) { + force_filter = -1; + } + + filt = (unsigned char *) STBIW_MALLOC((x*n+1) * y); if (!filt) return 0; + line_buffer = (signed char *) STBIW_MALLOC(x * n); if (!line_buffer) { STBIW_FREE(filt); return 0; } + for (j=0; j < y; ++j) { + int filter_type; + if (force_filter > -1) { + filter_type = force_filter; + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, force_filter, line_buffer); + } else { // Estimate the best filter by running through all of them: + int best_filter = 0, best_filter_val = 0x7fffffff, est, i; + for (filter_type = 0; filter_type < 5; filter_type++) { + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, filter_type, line_buffer); + + // Estimate the entropy of the line using this filter; the less, the better. + est = 0; + for (i = 0; i < x*n; ++i) { + est += abs((signed char) line_buffer[i]); + } + if (est < best_filter_val) { + best_filter_val = est; + best_filter = filter_type; + } + } + if (filter_type != best_filter) { // If the last iteration already got us the best filter, don't redo it + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, best_filter, line_buffer); + filter_type = best_filter; + } + } + // when we get here, filter_type contains the filter type, and line_buffer contains the data + filt[j*(x*n+1)] = (unsigned char) filter_type; + STBIW_MEMMOVE(filt+j*(x*n+1)+1, line_buffer, x*n); + } + STBIW_FREE(line_buffer); + zlib = stbi_zlib_compress(filt, y*( x*n+1), &zlen, stbi_write_png_compression_level); + STBIW_FREE(filt); + if (!zlib) return 0; + + // each tag requires 12 bytes of overhead + out = (unsigned char *) STBIW_MALLOC(8 + 12+13 + 12+zlen + 12); + if (!out) return 0; + *out_len = 8 + 12+13 + 12+zlen + 12; + + o=out; + STBIW_MEMMOVE(o,sig,8); o+= 8; + stbiw__wp32(o, 13); // header length + stbiw__wptag(o, "IHDR"); + stbiw__wp32(o, x); + stbiw__wp32(o, y); + *o++ = 8; + *o++ = STBIW_UCHAR(ctype[n]); + *o++ = 0; + *o++ = 0; + *o++ = 0; + stbiw__wpcrc(&o,13); + + stbiw__wp32(o, zlen); + stbiw__wptag(o, "IDAT"); + STBIW_MEMMOVE(o, zlib, zlen); + o += zlen; + STBIW_FREE(zlib); + stbiw__wpcrc(&o, zlen); + + stbiw__wp32(o,0); + stbiw__wptag(o, "IEND"); + stbiw__wpcrc(&o,0); + + STBIW_ASSERT(o == out + *out_len); + + return out; +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_png(char const *filename, int x, int y, int comp, const void *data, int stride_bytes) +{ + FILE *f; + int len; + unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len); + if (png == NULL) return 0; + + f = stbiw__fopen(filename, "wb"); + if (!f) { STBIW_FREE(png); return 0; } + fwrite(png, 1, len, f); + fclose(f); + STBIW_FREE(png); + return 1; +} +#endif + +STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int stride_bytes) +{ + int len; + unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len); + if (png == NULL) return 0; + func(context, png, len); + STBIW_FREE(png); + return 1; +} + + +/* *************************************************************************** + * + * JPEG writer + * + * This is based on Jon Olick's jo_jpeg.cpp: + * public domain Simple, Minimalistic JPEG writer - http://www.jonolick.com/code.html + */ + +static const unsigned char stbiw__jpg_ZigZag[] = { 0,1,5,6,14,15,27,28,2,4,7,13,16,26,29,42,3,8,12,17,25,30,41,43,9,11,18, + 24,31,40,44,53,10,19,23,32,39,45,52,54,20,22,33,38,46,51,55,60,21,34,37,47,50,56,59,61,35,36,48,49,57,58,62,63 }; + +static void stbiw__jpg_writeBits(stbi__write_context *s, int *bitBufP, int *bitCntP, const unsigned short *bs) { + int bitBuf = *bitBufP, bitCnt = *bitCntP; + bitCnt += bs[1]; + bitBuf |= bs[0] << (24 - bitCnt); + while(bitCnt >= 8) { + unsigned char c = (bitBuf >> 16) & 255; + stbiw__putc(s, c); + if(c == 255) { + stbiw__putc(s, 0); + } + bitBuf <<= 8; + bitCnt -= 8; + } + *bitBufP = bitBuf; + *bitCntP = bitCnt; +} + +static void stbiw__jpg_DCT(float *d0p, float *d1p, float *d2p, float *d3p, float *d4p, float *d5p, float *d6p, float *d7p) { + float d0 = *d0p, d1 = *d1p, d2 = *d2p, d3 = *d3p, d4 = *d4p, d5 = *d5p, d6 = *d6p, d7 = *d7p; + float z1, z2, z3, z4, z5, z11, z13; + + float tmp0 = d0 + d7; + float tmp7 = d0 - d7; + float tmp1 = d1 + d6; + float tmp6 = d1 - d6; + float tmp2 = d2 + d5; + float tmp5 = d2 - d5; + float tmp3 = d3 + d4; + float tmp4 = d3 - d4; + + // Even part + float tmp10 = tmp0 + tmp3; // phase 2 + float tmp13 = tmp0 - tmp3; + float tmp11 = tmp1 + tmp2; + float tmp12 = tmp1 - tmp2; + + d0 = tmp10 + tmp11; // phase 3 + d4 = tmp10 - tmp11; + + z1 = (tmp12 + tmp13) * 0.707106781f; // c4 + d2 = tmp13 + z1; // phase 5 + d6 = tmp13 - z1; + + // Odd part + tmp10 = tmp4 + tmp5; // phase 2 + tmp11 = tmp5 + tmp6; + tmp12 = tmp6 + tmp7; + + // The rotator is modified from fig 4-8 to avoid extra negations. + z5 = (tmp10 - tmp12) * 0.382683433f; // c6 + z2 = tmp10 * 0.541196100f + z5; // c2-c6 + z4 = tmp12 * 1.306562965f + z5; // c2+c6 + z3 = tmp11 * 0.707106781f; // c4 + + z11 = tmp7 + z3; // phase 5 + z13 = tmp7 - z3; + + *d5p = z13 + z2; // phase 6 + *d3p = z13 - z2; + *d1p = z11 + z4; + *d7p = z11 - z4; + + *d0p = d0; *d2p = d2; *d4p = d4; *d6p = d6; +} + +static void stbiw__jpg_calcBits(int val, unsigned short bits[2]) { + int tmp1 = val < 0 ? -val : val; + val = val < 0 ? val-1 : val; + bits[1] = 1; + while(tmp1 >>= 1) { + ++bits[1]; + } + bits[0] = val & ((1<0)&&(DU[end0pos]==0); --end0pos) { + } + // end0pos = first element in reverse order !=0 + if(end0pos == 0) { + stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB); + return DU[0]; + } + for(i = 1; i <= end0pos; ++i) { + int startpos = i; + int nrzeroes; + unsigned short bits[2]; + for (; DU[i]==0 && i<=end0pos; ++i) { + } + nrzeroes = i-startpos; + if ( nrzeroes >= 16 ) { + int lng = nrzeroes>>4; + int nrmarker; + for (nrmarker=1; nrmarker <= lng; ++nrmarker) + stbiw__jpg_writeBits(s, bitBuf, bitCnt, M16zeroes); + nrzeroes &= 15; + } + stbiw__jpg_calcBits(DU[i], bits); + stbiw__jpg_writeBits(s, bitBuf, bitCnt, HTAC[(nrzeroes<<4)+bits[1]]); + stbiw__jpg_writeBits(s, bitBuf, bitCnt, bits); + } + if(end0pos != 63) { + stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB); + } + return DU[0]; +} + +static int stbi_write_jpg_core(stbi__write_context *s, int width, int height, int comp, const void* data, int quality) { + // Constants that don't pollute global namespace + static const unsigned char std_dc_luminance_nrcodes[] = {0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0}; + static const unsigned char std_dc_luminance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11}; + static const unsigned char std_ac_luminance_nrcodes[] = {0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d}; + static const unsigned char std_ac_luminance_values[] = { + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0,0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16,0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa + }; + static const unsigned char std_dc_chrominance_nrcodes[] = {0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0}; + static const unsigned char std_dc_chrominance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11}; + static const unsigned char std_ac_chrominance_nrcodes[] = {0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77}; + static const unsigned char std_ac_chrominance_values[] = { + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0,0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34,0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa + }; + // Huffman tables + static const unsigned short YDC_HT[256][2] = { {0,2},{2,3},{3,3},{4,3},{5,3},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9}}; + static const unsigned short UVDC_HT[256][2] = { {0,2},{1,2},{2,2},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9},{1022,10},{2046,11}}; + static const unsigned short YAC_HT[256][2] = { + {10,4},{0,2},{1,2},{4,3},{11,4},{26,5},{120,7},{248,8},{1014,10},{65410,16},{65411,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {12,4},{27,5},{121,7},{502,9},{2038,11},{65412,16},{65413,16},{65414,16},{65415,16},{65416,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {28,5},{249,8},{1015,10},{4084,12},{65417,16},{65418,16},{65419,16},{65420,16},{65421,16},{65422,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {58,6},{503,9},{4085,12},{65423,16},{65424,16},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {59,6},{1016,10},{65430,16},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {122,7},{2039,11},{65438,16},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {123,7},{4086,12},{65446,16},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {250,8},{4087,12},{65454,16},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {504,9},{32704,15},{65462,16},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {505,9},{65470,16},{65471,16},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {506,9},{65479,16},{65480,16},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1017,10},{65488,16},{65489,16},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1018,10},{65497,16},{65498,16},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2040,11},{65506,16},{65507,16},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {65515,16},{65516,16},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2041,11},{65525,16},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0} + }; + static const unsigned short UVAC_HT[256][2] = { + {0,2},{1,2},{4,3},{10,4},{24,5},{25,5},{56,6},{120,7},{500,9},{1014,10},{4084,12},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {11,4},{57,6},{246,8},{501,9},{2038,11},{4085,12},{65416,16},{65417,16},{65418,16},{65419,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {26,5},{247,8},{1015,10},{4086,12},{32706,15},{65420,16},{65421,16},{65422,16},{65423,16},{65424,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {27,5},{248,8},{1016,10},{4087,12},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{65430,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {58,6},{502,9},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{65438,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {59,6},{1017,10},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{65446,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {121,7},{2039,11},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{65454,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {122,7},{2040,11},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{65462,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {249,8},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{65470,16},{65471,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {503,9},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{65479,16},{65480,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {504,9},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{65488,16},{65489,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {505,9},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{65497,16},{65498,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {506,9},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{65506,16},{65507,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2041,11},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{65515,16},{65516,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {16352,14},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{65525,16},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1018,10},{32707,15},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0} + }; + static const int YQT[] = {16,11,10,16,24,40,51,61,12,12,14,19,26,58,60,55,14,13,16,24,40,57,69,56,14,17,22,29,51,87,80,62,18,22, + 37,56,68,109,103,77,24,35,55,64,81,104,113,92,49,64,78,87,103,121,120,101,72,92,95,98,112,100,103,99}; + static const int UVQT[] = {17,18,24,47,99,99,99,99,18,21,26,66,99,99,99,99,24,26,56,99,99,99,99,99,47,66,99,99,99,99,99,99, + 99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99}; + static const float aasf[] = { 1.0f * 2.828427125f, 1.387039845f * 2.828427125f, 1.306562965f * 2.828427125f, 1.175875602f * 2.828427125f, + 1.0f * 2.828427125f, 0.785694958f * 2.828427125f, 0.541196100f * 2.828427125f, 0.275899379f * 2.828427125f }; + + int row, col, i, k, subsample; + float fdtbl_Y[64], fdtbl_UV[64]; + unsigned char YTable[64], UVTable[64]; + + if(!data || !width || !height || comp > 4 || comp < 1) { + return 0; + } + + quality = quality ? quality : 90; + subsample = quality <= 90 ? 1 : 0; + quality = quality < 1 ? 1 : quality > 100 ? 100 : quality; + quality = quality < 50 ? 5000 / quality : 200 - quality * 2; + + for(i = 0; i < 64; ++i) { + int uvti, yti = (YQT[i]*quality+50)/100; + YTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (yti < 1 ? 1 : yti > 255 ? 255 : yti); + uvti = (UVQT[i]*quality+50)/100; + UVTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (uvti < 1 ? 1 : uvti > 255 ? 255 : uvti); + } + + for(row = 0, k = 0; row < 8; ++row) { + for(col = 0; col < 8; ++col, ++k) { + fdtbl_Y[k] = 1 / (YTable [stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]); + fdtbl_UV[k] = 1 / (UVTable[stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]); + } + } + + // Write Headers + { + static const unsigned char head0[] = { 0xFF,0xD8,0xFF,0xE0,0,0x10,'J','F','I','F',0,1,1,0,0,1,0,1,0,0,0xFF,0xDB,0,0x84,0 }; + static const unsigned char head2[] = { 0xFF,0xDA,0,0xC,3,1,0,2,0x11,3,0x11,0,0x3F,0 }; + const unsigned char head1[] = { 0xFF,0xC0,0,0x11,8,(unsigned char)(height>>8),STBIW_UCHAR(height),(unsigned char)(width>>8),STBIW_UCHAR(width), + 3,1,(unsigned char)(subsample?0x22:0x11),0,2,0x11,1,3,0x11,1,0xFF,0xC4,0x01,0xA2,0 }; + s->func(s->context, (void*)head0, sizeof(head0)); + s->func(s->context, (void*)YTable, sizeof(YTable)); + stbiw__putc(s, 1); + s->func(s->context, UVTable, sizeof(UVTable)); + s->func(s->context, (void*)head1, sizeof(head1)); + s->func(s->context, (void*)(std_dc_luminance_nrcodes+1), sizeof(std_dc_luminance_nrcodes)-1); + s->func(s->context, (void*)std_dc_luminance_values, sizeof(std_dc_luminance_values)); + stbiw__putc(s, 0x10); // HTYACinfo + s->func(s->context, (void*)(std_ac_luminance_nrcodes+1), sizeof(std_ac_luminance_nrcodes)-1); + s->func(s->context, (void*)std_ac_luminance_values, sizeof(std_ac_luminance_values)); + stbiw__putc(s, 1); // HTUDCinfo + s->func(s->context, (void*)(std_dc_chrominance_nrcodes+1), sizeof(std_dc_chrominance_nrcodes)-1); + s->func(s->context, (void*)std_dc_chrominance_values, sizeof(std_dc_chrominance_values)); + stbiw__putc(s, 0x11); // HTUACinfo + s->func(s->context, (void*)(std_ac_chrominance_nrcodes+1), sizeof(std_ac_chrominance_nrcodes)-1); + s->func(s->context, (void*)std_ac_chrominance_values, sizeof(std_ac_chrominance_values)); + s->func(s->context, (void*)head2, sizeof(head2)); + } + + // Encode 8x8 macroblocks + { + static const unsigned short fillBits[] = {0x7F, 7}; + int DCY=0, DCU=0, DCV=0; + int bitBuf=0, bitCnt=0; + // comp == 2 is grey+alpha (alpha is ignored) + int ofsG = comp > 2 ? 1 : 0, ofsB = comp > 2 ? 2 : 0; + const unsigned char *dataR = (const unsigned char *)data; + const unsigned char *dataG = dataR + ofsG; + const unsigned char *dataB = dataR + ofsB; + int x, y, pos; + if(subsample) { + for(y = 0; y < height; y += 16) { + for(x = 0; x < width; x += 16) { + float Y[256], U[256], V[256]; + for(row = y, pos = 0; row < y+16; ++row) { + // row >= height => use last input row + int clamped_row = (row < height) ? row : height - 1; + int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp; + for(col = x; col < x+16; ++col, ++pos) { + // if col >= width => use pixel from last input column + int p = base_p + ((col < width) ? col : (width-1))*comp; + float r = dataR[p], g = dataG[p], b = dataB[p]; + Y[pos]= +0.29900f*r + 0.58700f*g + 0.11400f*b - 128; + U[pos]= -0.16874f*r - 0.33126f*g + 0.50000f*b; + V[pos]= +0.50000f*r - 0.41869f*g - 0.08131f*b; + } + } + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+0, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+8, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+128, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+136, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + + // subsample U,V + { + float subU[64], subV[64]; + int yy, xx; + for(yy = 0, pos = 0; yy < 8; ++yy) { + for(xx = 0; xx < 8; ++xx, ++pos) { + int j = yy*32+xx*2; + subU[pos] = (U[j+0] + U[j+1] + U[j+16] + U[j+17]) * 0.25f; + subV[pos] = (V[j+0] + V[j+1] + V[j+16] + V[j+17]) * 0.25f; + } + } + DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, subU, 8, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, subV, 8, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + } + } + } + } else { + for(y = 0; y < height; y += 8) { + for(x = 0; x < width; x += 8) { + float Y[64], U[64], V[64]; + for(row = y, pos = 0; row < y+8; ++row) { + // row >= height => use last input row + int clamped_row = (row < height) ? row : height - 1; + int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp; + for(col = x; col < x+8; ++col, ++pos) { + // if col >= width => use pixel from last input column + int p = base_p + ((col < width) ? col : (width-1))*comp; + float r = dataR[p], g = dataG[p], b = dataB[p]; + Y[pos]= +0.29900f*r + 0.58700f*g + 0.11400f*b - 128; + U[pos]= -0.16874f*r - 0.33126f*g + 0.50000f*b; + V[pos]= +0.50000f*r - 0.41869f*g - 0.08131f*b; + } + } + + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y, 8, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, U, 8, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, V, 8, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + } + } + } + + // Do the bit alignment of the EOI marker + stbiw__jpg_writeBits(s, &bitBuf, &bitCnt, fillBits); + } + + // EOI + stbiw__putc(s, 0xFF); + stbiw__putc(s, 0xD9); + + return 1; +} + +STBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_jpg_core(&s, x, y, comp, (void *) data, quality); +} + + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void *data, int quality) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_jpg_core(&s, x, y, comp, data, quality); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif + +#endif // STB_IMAGE_WRITE_IMPLEMENTATION + +/* Revision history + 1.16 (2021-07-11) + make Deflate code emit uncompressed blocks when it would otherwise expand + support writing BMPs with alpha channel + 1.15 (2020-07-13) unknown + 1.14 (2020-02-02) updated JPEG writer to downsample chroma channels + 1.13 + 1.12 + 1.11 (2019-08-11) + + 1.10 (2019-02-07) + support utf8 filenames in Windows; fix warnings and platform ifdefs + 1.09 (2018-02-11) + fix typo in zlib quality API, improve STB_I_W_STATIC in C++ + 1.08 (2018-01-29) + add stbi__flip_vertically_on_write, external zlib, zlib quality, choose PNG filter + 1.07 (2017-07-24) + doc fix + 1.06 (2017-07-23) + writing JPEG (using Jon Olick's code) + 1.05 ??? + 1.04 (2017-03-03) + monochrome BMP expansion + 1.03 ??? + 1.02 (2016-04-02) + avoid allocating large structures on the stack + 1.01 (2016-01-16) + STBIW_REALLOC_SIZED: support allocators with no realloc support + avoid race-condition in crc initialization + minor compile issues + 1.00 (2015-09-14) + installable file IO function + 0.99 (2015-09-13) + warning fixes; TGA rle support + 0.98 (2015-04-08) + added STBIW_MALLOC, STBIW_ASSERT etc + 0.97 (2015-01-18) + fixed HDR asserts, rewrote HDR rle logic + 0.96 (2015-01-17) + add HDR output + fix monochrome BMP + 0.95 (2014-08-17) + add monochrome TGA output + 0.94 (2014-05-31) + rename private functions to avoid conflicts with stb_image.h + 0.93 (2014-05-27) + warning fixes + 0.92 (2010-08-01) + casts to unsigned char to fix warnings + 0.91 (2010-07-17) + first public release + 0.90 first internal release +*/ + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/game_configs/GoWRagnarokIni.zip b/game_configs/GoWRagnarokIni.zip new file mode 100644 index 00000000..6ca56942 Binary files /dev/null and b/game_configs/GoWRagnarokIni.zip differ diff --git a/game_configs/uevr/b1-Win64-Shipping.zip b/game_configs/uevr/b1-Win64-Shipping.zip index 2a6ec27a..09dc0664 100644 Binary files a/game_configs/uevr/b1-Win64-Shipping.zip and b/game_configs/uevr/b1-Win64-Shipping.zip differ diff --git a/installer/README.md b/installer/README.md new file mode 100644 index 00000000..37eccf1c --- /dev/null +++ b/installer/README.md @@ -0,0 +1,43 @@ +# VRto3D Installer + +A Windows installer (Inno Setup) that automates VRto3D installation, optional WibbleWobble install, and a few common cleanup tasks. + +## What it does + +The installer wizard offers five tasks: + +- **Install / update VRto3D driver** (default on) — downloads `vrto3d.zip` from the latest GitHub release and copies the `vrto3d` folder into SteamVR's `drivers/` directory. Falls back to a `vrto3d.zip` placed next to the installer .exe if the download fails. +- **Install WibbleWobble for Frame Sequential 3D** (default off) — downloads `WibbleWobbleBeta7.zip`, lets the user pick a parent folder, extracts the `WibbleWobble` folder there, and runs `WibbleWobble\WibbleWobbleClient\Register.bat` elevated. Also removes the legacy `\drivers\WibbleWobbleVR` driver, which conflicts with VRto3D's built-in WibbleWobble presenter. +- **Remove legacy ReShade from `SteamVR\bin\win64`** (default on) — deletes only files identified as ReShade artifacts (DLL hooks containing the `ReShade` signature, plus `ReShade.ini`/`.log`/`reshade-shaders\`/`reshade-presets\`/`reshade-cache\` and `3DToElse.fx`). SteamVR's own files are not touched. +- **Remove third-party SteamVR drivers and reset `steamvr.vrsettings`** (default on) — deletes any folder under `\drivers\` that isn't on the Valve allowlist (`gamepad`, `htc`, `indexcontroller`, `indexhmd`, `lighthouse`, `null`, `oculus`, `oculus_legacy`, `prism`, `tundra_labs`, `vrlink`, `vrto3d`) and deletes `\config\steamvr.vrsettings`. +- **Launch SteamVR when finished** (default on) — starts SteamVR via `steam://run/250820` from the Finish page. + +## How SteamVR is located + +1. Reads `HKCU\Software\Valve\Steam\SteamPath` from the registry. +2. Parses `\steamapps\libraryfolders.vdf` and looks for app `250820` (SteamVR) — handles users with SteamVR installed on a different drive. +3. Falls back to `\steamapps\common\SteamVR` if the VDF parse misses. + +If neither Steam nor SteamVR is detected, the installer offers to open Steam's install page (`steam://install/250820`) or lets the user point to a SteamVR folder manually. + +## Building + +Requires Inno Setup 6.4 or later (for the bundled 7-Zip extractor). + +```powershell +choco install innosetup -y +iscc installer\VRto3D-Installer.iss +``` + +Output: `installer\Output\VRto3D-Installer.exe`. + +## Distribution + +Built in CI by `.github/workflows/Build.yml` and uploaded as a separate release asset alongside `vrto3d.zip`. Users can either: + +- Download just `VRto3D-Installer.exe` and let it pull `vrto3d.zip` over the network at install time. +- Download both `VRto3D-Installer.exe` and `vrto3d.zip` into the same folder for offline installs — the installer auto-detects the local zip when the download fails. + +## Logs + +Every action is appended to `\logs\VRto3D-Installer.log` (e.g. `C:\Program Files (x86)\Steam\logs\VRto3D-Installer.log`). If Steam isn't detected, the log falls back to `%TEMP%\VRto3D-Installer.log`. Useful for diagnosing skipped tasks (e.g. "SteamVR path not set, skipping"). diff --git a/installer/VRto3D-Installer.iss b/installer/VRto3D-Installer.iss new file mode 100644 index 00000000..a1dad488 --- /dev/null +++ b/installer/VRto3D-Installer.iss @@ -0,0 +1,1101 @@ +; VRto3D Installer +; Inno Setup 6.4+ script. Build with: iscc installer\VRto3D-Installer.iss + +#define AppName "VRto3D" +#define AppPublisher "oneup03" +#define AppURL "https://github.com/oneup03/VRto3D" +#define VRto3DReleaseUrl "https://github.com/oneup03/VRto3D/releases/latest/download/vrto3d.zip" +#define VRto3DPreReleaseUrl "https://github.com/oneup03/VRto3D/releases/download/latest/VRto3D.zip" +#define WWZipUrl "https://github.com/user-attachments/files/29946580/WibbleWobbleBeta11.zip" +#define SteamVRAppId "250820" + +[Setup] +AppId={{A6F4D9E2-7C3B-4A2E-9F1B-1B3E2D7C8F40} +AppName={#AppName} +AppVersion=1.0 +AppVerName={#AppName} +AppPublisher={#AppPublisher} +AppPublisherURL={#AppURL} +AppSupportURL={#AppURL} +DefaultDirName={autopf}\VRto3D +DisableWelcomePage=no +DisableDirPage=yes +DisableProgramGroupPage=yes +DisableReadyPage=no +Uninstallable=no +PrivilegesRequired=admin +OutputDir=Output +OutputBaseFilename=VRto3D-Installer +WizardStyle=modern +ArchitecturesInstallIn64BitMode=x64compatible +SolidCompression=yes +Compression=lzma2 +ArchiveExtraction=full +ShowLanguageDialog=no +CloseApplications=no + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "install"; Description: "Install / update VRto3D driver"; GroupDescription: "Actions:"; Flags: checkedonce +Name: "install\release"; Description: "Latest official release"; Flags: exclusive +Name: "install\prerelease"; Description: "Latest pre-release"; Flags: exclusive unchecked +Name: "install\local"; Description: "Local VRto3D.zip (next to installer)"; Flags: exclusive unchecked +Name: "cleanreshade"; Description: "Remove legacy ReShade from SteamVR\bin\win64"; GroupDescription: "Cleanup (recommended):" +Name: "cleandrivers"; Description: "Remove third-party SteamVR drivers and reset steamvr.vrsettings"; GroupDescription: "Cleanup (recommended):" +Name: "wibblewobble"; Description: "Install WibbleWobble for Frame Sequential 3D"; GroupDescription: "Optional:"; Flags: unchecked +Name: "addleiasrpath"; Description: "Fix LeiaSR library loading (requires restart)"; GroupDescription: "Optional:"; Flags: unchecked +Name: "launchsteamvr"; Description: "Launch SteamVR when finished"; GroupDescription: "Finish:" + +[Code] +var + SteamPath: String; + SteamVRPath: String; + ReleaseTag: String; + ReleaseTitle: String; + PreReleaseTag: String; + PreReleaseTitle: String; + LocalZipNote: String; + LogPath: String; + + SteamPage: TInputDirWizardPage; + WWPage: TInputDirWizardPage; + +{ ============================================================ } +{ Logging } +{ ============================================================ } + +procedure LogLine(const S: String); +var + Line: AnsiString; +begin + if LogPath = '' then Exit; + Line := AnsiString(GetDateTimeString('yyyy-mm-dd hh:nn:ss', '-', ':') + ' ' + S + #13#10); + SaveStringToFile(LogPath, Line, FileExists(LogPath)); +end; + +{ ============================================================ } +{ Path / string helpers } +{ ============================================================ } + +function NormalizeSlashes(const S: String): String; +var + I: Integer; + R: String; +begin + R := S; + for I := 1 to Length(R) do + if R[I] = '/' then + R[I] := '\'; + Result := R; +end; + +function PathCombine(const A, B: String): String; +begin + if A = '' then + Result := B + else if A[Length(A)] = '\' then + Result := A + B + else + Result := A + '\' + B; +end; + +{ Pull the second quoted string out of a VDF "key" "value" line. + Returns empty string if no such pair exists. } +function VdfLineValue(const Line: String): String; +var + Q1, Q2, Q3, Q4: Integer; + S: String; +begin + Result := ''; + S := Line; + Q1 := Pos('"', S); + if Q1 = 0 then Exit; + Q2 := Pos('"', Copy(S, Q1 + 1, MaxInt)); + if Q2 = 0 then Exit; + Q2 := Q2 + Q1; + Q3 := Pos('"', Copy(S, Q2 + 1, MaxInt)); + if Q3 = 0 then Exit; + Q3 := Q3 + Q2; + Q4 := Pos('"', Copy(S, Q3 + 1, MaxInt)); + if Q4 = 0 then Exit; + Q4 := Q4 + Q3; + Result := Copy(S, Q3 + 1, Q4 - Q3 - 1); +end; + +function VdfLineKey(const Line: String): String; +var + Q1, Q2: Integer; + S: String; +begin + Result := ''; + S := Line; + Q1 := Pos('"', S); + if Q1 = 0 then Exit; + Q2 := Pos('"', Copy(S, Q1 + 1, MaxInt)); + if Q2 = 0 then Exit; + Q2 := Q2 + Q1; + Result := LowerCase(Copy(S, Q1 + 1, Q2 - Q1 - 1)); +end; + +function SplitLines(const Text: String): TArrayOfString; +var + I, J, N: Integer; +begin + N := 0; + SetArrayLength(Result, 0); + J := 1; + for I := 1 to Length(Text) do + begin + if Text[I] = #10 then + begin + SetArrayLength(Result, N + 1); + Result[N] := Copy(Text, J, I - J); + if (Length(Result[N]) > 0) and (Result[N][Length(Result[N])] = #13) then + Result[N] := Copy(Result[N], 1, Length(Result[N]) - 1); + N := N + 1; + J := I + 1; + end; + end; + if J <= Length(Text) then + begin + SetArrayLength(Result, N + 1); + Result[N] := Copy(Text, J, Length(Text) - J + 1); + end; +end; + +{ Parse libraryfolders.vdf and return the library root that contains app SteamVRAppId. + Returns empty string if the app isn't listed in any library. } +function FindSteamVRLibrary(const VdfPath: String): String; +var + Raw: AnsiString; + Lines: TArrayOfString; + I: Integer; + Trimmed: String; + CurPath: String; + Key: String; + InApps: Boolean; + AppsDepth: Integer; +begin + Result := ''; + if not FileExists(VdfPath) then Exit; + if not LoadStringFromFile(VdfPath, Raw) then Exit; + Lines := SplitLines(String(Raw)); + + CurPath := ''; + InApps := False; + AppsDepth := 0; + + for I := 0 to GetArrayLength(Lines) - 1 do + begin + Trimmed := Trim(Lines[I]); + if Trimmed = '' then Continue; + + Key := VdfLineKey(Trimmed); + + if (not InApps) and (Key = 'path') then + begin + CurPath := VdfLineValue(Trimmed); + StringChangeEx(CurPath, '\\', '\', True); + CurPath := NormalizeSlashes(CurPath); + Continue; + end; + + if (not InApps) and (Key = 'apps') then + begin + InApps := True; + AppsDepth := 0; + Continue; + end; + + if InApps then + begin + if Trimmed = '{' then + begin + AppsDepth := AppsDepth + 1; + Continue; + end; + if Trimmed = '}' then + begin + AppsDepth := AppsDepth - 1; + if AppsDepth <= 0 then + InApps := False; + Continue; + end; + if Key = '{#SteamVRAppId}' then + begin + if CurPath <> '' then + begin + Result := CurPath; + Exit; + end; + end; + end; + end; +end; + +{ ============================================================ } +{ Steam path detection } +{ ============================================================ } + +function DetectSteamPath: String; +var + S: String; +begin + Result := ''; + if RegQueryStringValue(HKCU, 'Software\Valve\Steam', 'SteamPath', S) then + Result := NormalizeSlashes(S); +end; + +function DetectSteamVRPath: String; +var + Lib, Candidate: String; +begin + Result := ''; + if SteamPath = '' then Exit; + + Lib := FindSteamVRLibrary(PathCombine(SteamPath, 'steamapps\libraryfolders.vdf')); + if Lib <> '' then + begin + Candidate := PathCombine(Lib, 'steamapps\common\SteamVR'); + if DirExists(Candidate) then + begin + Result := Candidate; + Exit; + end; + end; + + Candidate := PathCombine(SteamPath, 'steamapps\common\SteamVR'); + if DirExists(Candidate) then + Result := Candidate; +end; + +{ ============================================================ } +{ GitHub release lookup } +{ ============================================================ } + +function ExtractJsonString(const Body, Field: String): String; +var + Key: String; + P, Q1, Q2: Integer; + Out_: String; +begin + Result := ''; + Key := '"' + Field + '"'; + P := Pos(Key, Body); + if P = 0 then Exit; + P := P + Length(Key); + while (P <= Length(Body)) and ((Body[P] = ' ') or (Body[P] = ':') or (Body[P] = #9)) do + P := P + 1; + if (P > Length(Body)) or (Body[P] <> '"') then Exit; + Q1 := P + 1; + Q2 := Q1; + while Q2 <= Length(Body) do + begin + if Body[Q2] = '\' then + begin + Q2 := Q2 + 2; + Continue; + end; + if Body[Q2] = '"' then Break; + Q2 := Q2 + 1; + end; + Out_ := Copy(Body, Q1, Q2 - Q1); + StringChangeEx(Out_, '\"', '"', True); + StringChangeEx(Out_, '\\', '\', True); + StringChangeEx(Out_, '\n', #10, True); + Result := Out_; +end; + +procedure FetchReleaseInfo(const Endpoint: String; out Tag, Title: String); +var + WinHttp: Variant; + Body: String; + Status: Integer; +begin + Tag := ''; + Title := ''; + LogLine('FetchReleaseInfo: GET ' + Endpoint); + try + WinHttp := CreateOleObject('WinHttp.WinHttpRequest.5.1'); + WinHttp.SetTimeouts(10000, 10000, 10000, 10000); + WinHttp.Open('GET', 'https://api.github.com/repos/oneup03/VRto3D' + Endpoint, False); + WinHttp.SetRequestHeader('User-Agent', 'VRto3D-Installer'); + WinHttp.SetRequestHeader('Accept', 'application/vnd.github+json'); + WinHttp.Send(''); + Status := WinHttp.Status; + LogLine('FetchReleaseInfo: HTTP ' + IntToStr(Status)); + if Status = 200 then + begin + Body := WinHttp.ResponseText; + Tag := ExtractJsonString(Body, 'tag_name'); + Title := ExtractJsonString(Body, 'name'); + LogLine('FetchReleaseInfo: tag="' + Tag + '" title="' + Title + '"'); + end; + except + LogLine('FetchReleaseInfo error: ' + GetExceptionMessage); + end; +end; + +procedure FetchLatestRelease; +begin + FetchReleaseInfo('/releases/latest', ReleaseTag, ReleaseTitle); + FetchReleaseInfo('/releases/tags/latest', PreReleaseTag, PreReleaseTitle); +end; + +{ ============================================================ } +{ File / folder helpers } +{ ============================================================ } + +function DeleteFolderRecursive(const Path: String): Boolean; +begin + Result := True; + if not DirExists(Path) then Exit; + Result := DelTree(Path, True, True, True); + if Result then + LogLine('Deleted folder: ' + Path) + else + LogLine('Failed to delete folder: ' + Path); +end; + +function DeleteFileLogged(const Path: String): Boolean; +begin + Result := True; + if not FileExists(Path) then Exit; + Result := DeleteFile(Path); + if Result then + LogLine('Deleted file: ' + Path) + else + LogLine('Failed to delete file: ' + Path); +end; + +{ Copy Src directory tree to Dest (Dest's parent must exist). } +function CopyFolderRecursive(const Src, Dest: String): Boolean; +var + ResultCode: Integer; + Cmd: String; +begin + ForceDirectories(ExtractFilePath(RemoveBackslashUnlessRoot(Dest))); + Cmd := '/C xcopy "' + Src + '" "' + Dest + '" /E /I /Y /Q'; + if not Exec(ExpandConstant('{cmd}'), Cmd, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then + begin + LogLine('Exec(xcopy) failed for ' + Src + ' -> ' + Dest); + Result := False; + Exit; + end; + Result := DirExists(Dest); + if Result then + LogLine('Copied: ' + Src + ' -> ' + Dest) + else + LogLine('xcopy returned ' + IntToStr(ResultCode) + ' but dest missing: ' + Dest); +end; + +{ Conservative ReShade detection - look for the literal "ReShade" byte string anywhere + in the file content. Reliable for ReShade DLLs (which embed it in their .rsrc data) + and won't match Valve's own dxgi/d3d* DLLs. } +function IsReShadeDll(const Path: String): Boolean; +var + Raw: AnsiString; + I, N: Integer; +begin + Result := False; + if not FileExists(Path) then Exit; + if not LoadStringFromFile(Path, Raw) then Exit; + N := Length(Raw); + if N < 7 then Exit; + for I := 1 to N - 6 do + begin + if (Raw[I] = 'R') and + (Raw[I + 1] = 'e') and + (Raw[I + 2] = 'S') and + (Raw[I + 3] = 'h') and + (Raw[I + 4] = 'a') and + (Raw[I + 5] = 'd') and + (Raw[I + 6] = 'e') then + begin + Result := True; + Exit; + end; + end; +end; + +function InstallerDir: String; +begin + Result := ExtractFilePath(ExpandConstant('{srcexe}')); +end; + +{ Look for a local VRto3D zip next to the installer .exe (either case variant). } +function FindLocalVRto3DZip: String; +var + Candidate: String; +begin + Result := ''; + Candidate := PathCombine(InstallerDir, 'vrto3d.zip'); + if FileExists(Candidate) then + begin + Result := Candidate; + Exit; + end; + Candidate := PathCombine(InstallerDir, 'VRto3D.zip'); + if FileExists(Candidate) then + Result := Candidate; +end; + +{ In-place text patch on a small ASCII file. No-op (file left untouched) when the + pattern isn't present, so these are safe to keep even after WibbleWobble drops + the interactive prompts on its side. } +procedure PatchTextFile(const Path, Find, Replace: String); +var + Raw: AnsiString; + S: String; + Count: Integer; +begin + if not FileExists(Path) then + begin + LogLine('PatchTextFile: file not present, skipping: ' + Path); + Exit; + end; + if not LoadStringFromFile(Path, Raw) then + begin + LogLine('PatchTextFile: load failed for ' + Path); + Exit; + end; + S := String(Raw); + Count := StringChangeEx(S, Find, Replace, True); + if Count = 0 then + begin + LogLine('PatchTextFile: pattern not found, left as-is: ' + Path); + Exit; + end; + if SaveStringToFile(Path, AnsiString(S), False) then + LogLine('PatchTextFile: applied ' + IntToStr(Count) + ' change(s) in ' + Path) + else + LogLine('PatchTextFile: save failed for ' + Path); +end; + +{ ============================================================ } +{ Download / extract } +{ ============================================================ } + +function ResolveZip(const Url, FileName: String; out ZipPath: String): Boolean; +var + TmpZip, LocalZip: String; +begin + Result := False; + TmpZip := ExpandConstant('{tmp}\') + FileName; + + try + DownloadTemporaryFile(Url, FileName, '', nil); + if FileExists(TmpZip) then + begin + ZipPath := TmpZip; + LogLine('Downloaded ' + FileName); + Result := True; + Exit; + end; + except + LogLine('Download failed for ' + FileName + ': ' + GetExceptionMessage); + end; + + if LowerCase(FileName) = 'vrto3d.zip' then + LocalZip := FindLocalVRto3DZip + else + begin + LocalZip := PathCombine(InstallerDir, FileName); + if not FileExists(LocalZip) then + LocalZip := ''; + end; + + if LocalZip <> '' then + begin + ZipPath := LocalZip; + LogLine('Using local zip: ' + LocalZip); + Result := True; + Exit; + end; + + LogLine('No source available for ' + FileName + ' (online + offline both failed).'); +end; + +{ ============================================================ } +{ Tasks } +{ ============================================================ } + +procedure TaskInstallVRto3D; +var + ZipPath, ExtractDir, SrcDir, DestDir, ChannelUrl, Channel, LocalZip: String; +begin + if SteamVRPath = '' then + begin + LogLine('TaskInstallVRto3D: SteamVR path not set, skipping.'); + Exit; + end; + + if WizardIsTaskSelected('install\local') then + begin + Channel := 'local'; + LocalZip := FindLocalVRto3DZip; + if LocalZip = '' then + begin + LogLine('TaskInstallVRto3D: local zip not found next to installer.'); + MsgBox( + 'No vrto3d.zip / VRto3D.zip found next to this installer.' #13#10 #13#10 + + 'Place the zip next to VRto3D-Installer.exe and re-run, or pick a different channel.', + mbError, MB_OK); + Exit; + end; + ZipPath := LocalZip; + LogLine('TaskInstallVRto3D: channel=local using ' + ZipPath); + end + else + begin + if WizardIsTaskSelected('install\prerelease') then + begin + ChannelUrl := '{#VRto3DPreReleaseUrl}'; + Channel := 'pre-release'; + end + else + begin + ChannelUrl := '{#VRto3DReleaseUrl}'; + Channel := 'release'; + end; + LogLine('TaskInstallVRto3D: channel=' + Channel + ' url=' + ChannelUrl); + + if not ResolveZip(ChannelUrl, 'vrto3d.zip', ZipPath) then + begin + MsgBox( + 'Could not download vrto3d.zip and no local copy was found next to the installer.' #13#10 #13#10 + + 'Reconnect to the internet, or download vrto3d.zip from:' #13#10 + + ' https://github.com/oneup03/VRto3D/releases/latest' #13#10 + + 'and place it next to this installer, then re-run.', + mbError, MB_OK); + Exit; + end; + end; + + ExtractDir := ExpandConstant('{tmp}\vrto3d_extract'); + ForceDirectories(ExtractDir); + ExtractArchive(ZipPath, ExtractDir, '', True, nil); + + SrcDir := PathCombine(ExtractDir, 'drivers\vrto3d'); + if not DirExists(SrcDir) then + SrcDir := PathCombine(ExtractDir, 'vrto3d'); + if not DirExists(SrcDir) then + begin + LogLine('TaskInstallVRto3D: vrto3d folder not found in zip extracted at ' + ExtractDir); + MsgBox('The vrto3d.zip archive does not contain a vrto3d folder. Aborting install.', mbError, MB_OK); + Exit; + end; + + DestDir := PathCombine(SteamVRPath, 'drivers\vrto3d'); + if DirExists(DestDir) then + DeleteFolderRecursive(DestDir); + ForceDirectories(PathCombine(SteamVRPath, 'drivers')); + CopyFolderRecursive(SrcDir, DestDir); + LogLine('Installed VRto3D driver to ' + DestDir); +end; + +procedure DeleteReShadePresetGlob(const Bin64: String); +var + Rec: TFindRec; + Found: Boolean; +begin + Found := FindFirst(PathCombine(Bin64, 'ReShadePreset*.ini'), Rec); + if not Found then Exit; + try + repeat + DeleteFileLogged(PathCombine(Bin64, Rec.Name)); + until not FindNext(Rec); + finally + FindClose(Rec); + end; +end; + +procedure TaskCleanReshade; +var + Bin64, P: String; + I: Integer; + Dlls: array[0..5] of String; + Files: array[0..2] of String; + Folders: array[0..2] of String; +begin + if SteamVRPath = '' then + begin + LogLine('TaskCleanReshade: SteamVR path not set, skipping.'); + Exit; + end; + Bin64 := PathCombine(SteamVRPath, 'bin\win64'); + if not DirExists(Bin64) then + begin + LogLine('TaskCleanReshade: ' + Bin64 + ' does not exist, skipping.'); + Exit; + end; + + Dlls[0] := 'dxgi.dll'; + Dlls[1] := 'd3d9.dll'; + Dlls[2] := 'd3d10.dll'; + Dlls[3] := 'd3d11.dll'; + Dlls[4] := 'd3d12.dll'; + Dlls[5] := 'opengl32.dll'; + for I := 0 to 5 do + begin + P := PathCombine(Bin64, Dlls[I]); + if FileExists(P) and IsReShadeDll(P) then + DeleteFileLogged(P); + end; + + Files[0] := 'ReShade.ini'; + Files[1] := 'ReShade.log'; + Files[2] := '3DToElse.fx'; + for I := 0 to 2 do + DeleteFileLogged(PathCombine(Bin64, Files[I])); + + DeleteReShadePresetGlob(Bin64); + + Folders[0] := 'reshade-shaders'; + Folders[1] := 'reshade-presets'; + Folders[2] := 'reshade-cache'; + for I := 0 to 2 do + DeleteFolderRecursive(PathCombine(Bin64, Folders[I])); +end; + +procedure TaskCleanDrivers; +var + DriversRoot, VrSettings, ItemPath, NameLower: String; + Rec: TFindRec; + Allow: array[0..11] of String; + Keep, Found: Boolean; + I: Integer; +begin + if SteamVRPath = '' then + begin + LogLine('TaskCleanDrivers: SteamVR path not set, skipping.'); + Exit; + end; + DriversRoot := PathCombine(SteamVRPath, 'drivers'); + if not DirExists(DriversRoot) then + begin + LogLine('TaskCleanDrivers: ' + DriversRoot + ' does not exist, skipping.'); + Exit; + end; + + Allow[0] := 'gamepad'; + Allow[1] := 'htc'; + Allow[2] := 'indexcontroller'; + Allow[3] := 'indexhmd'; + Allow[4] := 'lighthouse'; + Allow[5] := 'null'; + Allow[6] := 'oculus'; + Allow[7] := 'oculus_legacy'; + Allow[8] := 'prism'; + Allow[9] := 'tundra_labs'; + Allow[10] := 'vrlink'; + Allow[11] := 'vrto3d'; + + Found := FindFirst(PathCombine(DriversRoot, '*'), Rec); + if Found then + try + repeat + if (Rec.Name = '.') or (Rec.Name = '..') then Continue; + if (Rec.Attributes and FILE_ATTRIBUTE_DIRECTORY) = 0 then Continue; + NameLower := LowerCase(Rec.Name); + Keep := False; + for I := 0 to 11 do + if NameLower = Allow[I] then + begin + Keep := True; + Break; + end; + if not Keep then + begin + ItemPath := PathCombine(DriversRoot, Rec.Name); + DeleteFolderRecursive(ItemPath); + end; + until not FindNext(Rec); + finally + FindClose(Rec); + end; + + if SteamPath <> '' then + begin + VrSettings := PathCombine(SteamPath, 'config\steamvr.vrsettings'); + DeleteFileLogged(VrSettings); + end; +end; + +procedure TaskInstallWibbleWobble; +var + ZipPath, ExtractDir, SrcDir, DestDir, RegBat, LegacyDriver: String; + ResultCode: Integer; +begin + if SteamVRPath <> '' then + begin + LegacyDriver := PathCombine(SteamVRPath, 'drivers\WibbleWobbleVR'); + if DirExists(LegacyDriver) then + begin + LogLine('Removing legacy WibbleWobbleVR SteamVR driver: ' + LegacyDriver); + DeleteFolderRecursive(LegacyDriver); + end; + end; + + if not ResolveZip('{#WWZipUrl}', 'WibbleWobbleBeta7.2.zip', ZipPath) then + begin + MsgBox( + 'Could not download WibbleWobbleBeta7.2.zip and no local copy was found next to the installer.' #13#10 #13#10 + + 'Skipping WibbleWobble install. The legacy WibbleWobbleVR driver (if present) was still removed.', + mbInformation, MB_OK); + Exit; + end; + + ExtractDir := ExpandConstant('{tmp}\ww_extract'); + ForceDirectories(ExtractDir); + ExtractArchive(ZipPath, ExtractDir, '', True, nil); + + SrcDir := PathCombine(ExtractDir, 'WibbleWobble'); + if not DirExists(SrcDir) then + begin + LogLine('TaskInstallWibbleWobble: WibbleWobble folder not found inside zip at ' + ExtractDir); + MsgBox('The WibbleWobble zip did not contain a WibbleWobble folder.', mbError, MB_OK); + Exit; + end; + + if WWPage = nil then + DestDir := 'C:\Apps\WibbleWobble' + else + DestDir := RemoveBackslashUnlessRoot(Trim(WWPage.Values[0])); + + if DirExists(DestDir) then + begin + if MsgBox( + 'A WibbleWobble folder already exists at:' #13#10 + DestDir + #13#10 #13#10 + + 'Overwrite it?', + mbConfirmation, MB_YESNO) <> IDYES then + begin + LogLine('User declined WibbleWobble overwrite at ' + DestDir); + Exit; + end; + DeleteFolderRecursive(DestDir); + end; + + ForceDirectories(DestDir); + CopyFolderRecursive(SrcDir, DestDir); + + RegBat := PathCombine(DestDir, 'WibbleWobbleClient\Register.bat'); + if not FileExists(RegBat) then + begin + LogLine('TaskInstallWibbleWobble: Register.bat not found at ' + RegBat); + MsgBox( + 'Register.bat not found at:' #13#10 + RegBat + #13#10 #13#10 + + 'WibbleWobble files were copied but registration was skipped.', + mbError, MB_OK); + Exit; + end; + + { Strip the interactive prompts so Register.bat and its child PowerShell run + unattended. Each call is a no-op if WibbleWobble already ships without the + prompt, so keeping them in cannot break a fixed bat/ps1. } + PatchTextFile(RegBat, #13#10 + 'pause', ''); + PatchTextFile(RegBat, '"%install_path%\"' + #13#10, '"%install_path%\" /f' + #13#10); + PatchTextFile(PathCombine(DestDir, 'WibbleWobbleClient\SetRealtimePrivilege.ps1'), + 'Read-Host "Press Enter to exit"', ''); + + LogLine('Running Register.bat (installer is already elevated): ' + RegBat); + if not Exec(ExpandConstant('{cmd}'), + '/C ""' + RegBat + '""', + ExtractFilePath(RegBat), + SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode) then + begin + LogLine('Exec failed for Register.bat'); + MsgBox( + 'Failed to launch Register.bat. You can run it manually as administrator from:' #13#10 + + RegBat, + mbError, MB_OK); + end + else + LogLine('Register.bat exited with code ' + IntToStr(ResultCode)); +end; + +procedure TaskAddLeiaSRPath; +var + LeiaPath, CurrentPath: String; + ResultCode: Integer; +begin + LeiaPath := 'C:\Program Files\LeiaSR\Platform\bin'; + CurrentPath := GetEnv('PATH'); + if Pos(LowerCase(LeiaPath), LowerCase(CurrentPath)) > 0 then + begin + LogLine('TaskAddLeiaSRPath: ' + LeiaPath + ' already on PATH, skipping.'); + Exit; + end; + LogLine('TaskAddLeiaSRPath: prepending ' + LeiaPath + ' to user PATH (reboot required)'); + if not Exec(ExpandConstant('{cmd}'), + '/C setx PATH "' + LeiaPath + ';%PATH%"', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then + LogLine('setx exec failed') + else + LogLine('setx exited with code ' + IntToStr(ResultCode)); +end; + +procedure TaskLaunchSteamVR; +var + ResultCode: Integer; +begin + LogLine('Launching SteamVR via steam://run/{#SteamVRAppId}'); + ShellExec('open', 'steam://run/{#SteamVRAppId}', '', '', SW_SHOWNORMAL, ewNoWait, ResultCode); +end; + +{ ============================================================ } +{ Wizard hooks } +{ ============================================================ } + +function InitializeSetup: Boolean; +var + Resp: Integer; + ResultCode: Integer; + LogDir: String; +begin + Result := True; + SteamPath := DetectSteamPath; + + if SteamPath <> '' then + begin + LogDir := PathCombine(SteamPath, 'logs'); + ForceDirectories(LogDir); + LogPath := PathCombine(LogDir, 'VRto3D-Installer.log'); + end + else + LogPath := ExpandConstant('{tmp}\VRto3D-Installer.log'); + + LogLine('--- VRto3D Installer started ---'); + LogLine('Steam path: ' + SteamPath); + if SteamPath = '' then + begin + Resp := MsgBox( + 'Steam was not found on this machine.' #13#10 #13#10 + + 'Install Steam from https://store.steampowered.com/about/ , then re-run this installer.' #13#10 #13#10 + + 'Continue anyway and pick a SteamVR folder manually?', + mbConfirmation, MB_YESNO); + if Resp <> IDYES then + begin + Result := False; + Exit; + end; + end; + + SteamVRPath := DetectSteamVRPath; + if SteamVRPath = '' then + begin + Resp := MsgBox( + 'SteamVR was not detected in any Steam library folder.' #13#10 #13#10 + + 'Yes - open Steam''s install page for SteamVR (re-run this installer afterwards).' #13#10 + + 'No - continue and pick the SteamVR folder manually.' #13#10 + + 'Cancel - abort the installer.', + mbConfirmation, MB_YESNOCANCEL); + if Resp = IDYES then + begin + ShellExec('open', 'steam://install/{#SteamVRAppId}', '', '', SW_SHOWNORMAL, ewNoWait, ResultCode); + Result := False; + Exit; + end + else if Resp = IDCANCEL then + begin + Result := False; + Exit; + end; + end; + + FetchLatestRelease; + + if FindLocalVRto3DZip <> '' then + LocalZipNote := #13#10 + 'Local zip detected: ' + FindLocalVRto3DZip + else + LocalZipNote := ''; +end; + +procedure InitializeWizard; +var + WelcomeText: String; +begin + if ReleaseTag <> '' then + WelcomeText := 'Latest release: ' + ReleaseTag + else + WelcomeText := 'Latest release: unknown'; + if PreReleaseTitle <> '' then + WelcomeText := WelcomeText + #13#10 + 'Latest pre-release: ' + PreReleaseTitle + else if PreReleaseTag <> '' then + WelcomeText := WelcomeText + #13#10 + 'Latest pre-release: ' + PreReleaseTag + else + WelcomeText := WelcomeText + #13#10 + 'Latest pre-release: unknown'; + WelcomeText := WelcomeText + LocalZipNote; + + WizardForm.WelcomeLabel2.Caption := + WizardForm.WelcomeLabel2.Caption + #13#10 + #13#10 + WelcomeText; + + SteamPage := CreateInputDirPage(wpWelcome, + 'SteamVR Location', + 'Confirm where SteamVR is installed.', + 'VRto3D will be installed into the drivers subfolder of this path.', + False, ''); + SteamPage.Add('SteamVR root folder:'); + if SteamVRPath <> '' then + SteamPage.Values[0] := SteamVRPath + else if SteamPath <> '' then + SteamPage.Values[0] := PathCombine(SteamPath, 'steamapps\common\SteamVR') + else + SteamPage.Values[0] := 'C:\Program Files (x86)\Steam\steamapps\common\SteamVR'; + + WWPage := CreateInputDirPage(wpSelectTasks, + 'WibbleWobble Location', + 'Choose where WibbleWobble will be installed.', + 'WibbleWobble files will be extracted directly into this folder.', + False, ''); + WWPage.Add('WibbleWobble install folder:'); + WWPage.Values[0] := 'C:\Apps\WibbleWobble'; +end; + +function ShouldSkipPage(PageID: Integer): Boolean; +begin + Result := False; + if (WWPage <> nil) and (PageID = WWPage.ID) then + Result := not WizardIsTaskSelected('wibblewobble'); +end; + +function NextButtonClick(CurPageID: Integer): Boolean; +begin + Result := True; + if (SteamPage <> nil) and (CurPageID = SteamPage.ID) then + begin + SteamVRPath := RemoveBackslashUnlessRoot(Trim(SteamPage.Values[0])); + if SteamVRPath = '' then + begin + MsgBox('Please specify the SteamVR folder.', mbError, MB_OK); + Result := False; + Exit; + end; + if not DirExists(SteamVRPath) then + begin + if MsgBox('The folder' #13#10 + SteamVRPath + #13#10 + 'does not exist. Continue anyway?', + mbConfirmation, MB_YESNO) <> IDYES then + begin + Result := False; + Exit; + end; + end; + LogLine('SteamVR root selected: ' + SteamVRPath); + end; +end; + +function UpdateReadyMemo(Space, NewLine, MemoUserInfoInfo, MemoDirInfo, + MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String; +var + S: String; +begin + S := 'SteamVR root:' + NewLine + Space + SteamVRPath + NewLine + NewLine; + if WizardIsTaskSelected('install') then + begin + S := S + 'Install / update VRto3D driver:' + NewLine + + Space + SteamVRPath + '\drivers\vrto3d' + NewLine; + if WizardIsTaskSelected('install\local') then + S := S + Space + 'Channel: local zip next to installer' + NewLine + else if WizardIsTaskSelected('install\prerelease') then + S := S + Space + 'Channel: pre-release (' + PreReleaseTag + ')' + NewLine + else + S := S + Space + 'Channel: official release (' + ReleaseTag + ')' + NewLine; + S := S + NewLine; + end; + if WizardIsTaskSelected('wibblewobble') and (WWPage <> nil) then + S := S + 'Install WibbleWobble:' + NewLine + + Space + WWPage.Values[0] + NewLine + + Space + '(legacy ' + SteamVRPath + '\drivers\WibbleWobbleVR will be removed if present)' + NewLine + NewLine; + if WizardIsTaskSelected('cleanreshade') then + S := S + 'Remove ReShade from:' + NewLine + + Space + SteamVRPath + '\bin\win64' + NewLine + + Space + '(only files identified as ReShade will be deleted)' + NewLine + NewLine; + if WizardIsTaskSelected('cleandrivers') then + begin + S := S + 'Remove third-party SteamVR drivers under:' + NewLine + + Space + SteamVRPath + '\drivers' + NewLine + + Space + '(default Valve drivers and vrto3d are kept)' + NewLine; + if SteamPath <> '' then + S := S + Space + 'Also delete: ' + SteamPath + '\config\steamvr.vrsettings' + NewLine; + S := S + NewLine; + end; + if WizardIsTaskSelected('addleiasrpath') then + S := S + 'Fix LeiaSR library loading (requires restart):' + NewLine + + Space + 'Add C:\Program Files\LeiaSR\Platform\bin to user PATH' + NewLine + NewLine; + if WizardIsTaskSelected('launchsteamvr') then + S := S + 'Launch SteamVR after install completes' + NewLine; + Result := S; +end; + +procedure CurStepChanged(CurStep: TSetupStep); +begin + if CurStep <> ssInstall then Exit; + + WizardForm.StatusLabel.Caption := 'Running selected tasks...'; + + if WizardIsTaskSelected('install') then + begin + WizardForm.StatusLabel.Caption := 'Installing VRto3D driver...'; + try + TaskInstallVRto3D; + except + LogLine('install task error: ' + GetExceptionMessage); + end; + end; + + if WizardIsTaskSelected('wibblewobble') then + begin + WizardForm.StatusLabel.Caption := 'Installing WibbleWobble...'; + try + TaskInstallWibbleWobble; + except + LogLine('wibblewobble task error: ' + GetExceptionMessage); + end; + end; + + if WizardIsTaskSelected('cleanreshade') then + begin + WizardForm.StatusLabel.Caption := 'Cleaning up legacy ReShade...'; + try + TaskCleanReshade; + except + LogLine('cleanreshade task error: ' + GetExceptionMessage); + end; + end; + + if WizardIsTaskSelected('cleandrivers') then + begin + WizardForm.StatusLabel.Caption := 'Removing third-party SteamVR drivers...'; + try + TaskCleanDrivers; + except + LogLine('cleandrivers task error: ' + GetExceptionMessage); + end; + end; + + if WizardIsTaskSelected('addleiasrpath') then + begin + WizardForm.StatusLabel.Caption := 'Adding LeiaSR Platform\bin to PATH...'; + try + TaskAddLeiaSRPath; + except + LogLine('addleiasrpath task error: ' + GetExceptionMessage); + end; + end; +end; + +procedure CurPageChanged(CurPageID: Integer); +begin + if CurPageID = wpFinished then + begin + if WizardIsTaskSelected('launchsteamvr') then + TaskLaunchSteamVR; + end; +end; + +procedure DeinitializeSetup; +begin + LogLine('--- VRto3D Installer finished ---'); +end; diff --git a/installer/install.sh b/installer/install.sh new file mode 100644 index 00000000..84868abe --- /dev/null +++ b/installer/install.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +# VRto3D Linux installer: copies the driver into SteamVR's drivers/ folder +# (auto-discovered, same layout as the Windows install) and registers it in +# steamvr.vrsettings. Ships at the root of vrto3d-linux64.tar.gz next to the +# drivers/ payload. No dependencies beyond coreutils + awk. +# +# Usage: ./install.sh [--uninstall] [--steam ] +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +UNINSTALL=0 +STEAM_ROOT="" + +while [ $# -gt 0 ]; do + case "$1" in + --uninstall) UNINSTALL=1 ;; + --steam) STEAM_ROOT="${2:?--steam needs a path}"; shift ;; + -h|--help) + echo "Usage: $0 [--uninstall] [--steam ]" + exit 0 ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac + shift +done + +die() { echo "error: $*" >&2; exit 1; } + +# --- locate the Steam root --------------------------------------------------- +if [ -z "$STEAM_ROOT" ]; then + for cand in \ + "$HOME/.local/share/Steam" \ + "$HOME/.steam/steam" \ + "$HOME/.steam/root" \ + "$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam"; do + if [ -d "$cand/steamapps" ]; then + STEAM_ROOT="$(readlink -f "$cand")" + break + fi + done +fi +[ -n "$STEAM_ROOT" ] && [ -d "$STEAM_ROOT/steamapps" ] \ + || die "Steam not found - pass its root with --steam " + +# --- locate SteamVR (may live in any Steam library folder) ------------------- +STEAMVR="" +LIBS="$STEAM_ROOT" +VDF="$STEAM_ROOT/steamapps/libraryfolders.vdf" +if [ -f "$VDF" ]; then + LIBS="$LIBS +$(sed -n 's/^[[:space:]]*"path"[[:space:]]*"\(.*\)"[[:space:]]*$/\1/p' "$VDF")" +fi +while IFS= read -r lib; do + if [ -n "$lib" ] && [ -d "$lib/steamapps/common/SteamVR" ]; then + STEAMVR="$lib/steamapps/common/SteamVR" + break + fi +done < "$VRSETTINGS" <<'EOF' +{ + "steamvr" : { + "requireHmd" : true, + "forcedDriver" : "vrto3d", + "activateMultipleDrivers" : true + } +} +EOF +} + +# Replace/insert the three managed keys, preserving everything else. +# Exits non-zero when the file shape is unexpected; caller falls back. +patch_vrsettings() { + awk ' + BEGIN { injected = 0; bad = 0; n = 0 } + # drop existing occurrences of the keys we manage + /^[[:space:]]*"(requireHmd|forcedDriver|activateMultipleDrivers)"[[:space:]]*:/ { next } + { + if (!injected && index($0, "\"steamvr\"") > 0) { + if ($0 ~ /"steamvr"[[:space:]]*:[[:space:]]*[{][[:space:]]*[}],?[[:space:]]*$/) { + # empty inline section: expand it + match($0, /^[[:space:]]*/); ind = substr($0, 1, RLENGTH) + tail = ""; if ($0 ~ /,[[:space:]]*$/) tail = "," + out[n++] = ind "\"steamvr\" : {" + out[n++] = ind " \"requireHmd\" : true," + out[n++] = ind " \"forcedDriver\" : \"vrto3d\"," + out[n++] = ind " \"activateMultipleDrivers\" : true" + out[n++] = ind "}" tail + injected = 1 + next + } + if ($0 ~ /"steamvr"[[:space:]]*:[[:space:]]*[{][[:space:]]*$/) { + # multiline section: inject right after the opening brace + match($0, /^[[:space:]]*/); ind = substr($0, 1, RLENGTH) + out[n++] = $0 + out[n++] = ind " \"requireHmd\" : true," + out[n++] = ind " \"forcedDriver\" : \"vrto3d\"," + out[n++] = ind " \"activateMultipleDrivers\" : true," + injected = 1 + next + } + bad = 1; exit # "steamvr" in a shape we do not understand + } + out[n++] = $0 + } + END { + if (bad) exit 2 + if (!injected) { + # no steamvr section: add one right after the top-level brace + if (n == 0 || out[0] !~ /^[[:space:]]*[{][[:space:]]*$/) exit 2 + m = 0; new[m++] = out[0] + new[m++] = " \"steamvr\" : {" + new[m++] = " \"requireHmd\" : true," + new[m++] = " \"forcedDriver\" : \"vrto3d\"," + new[m++] = " \"activateMultipleDrivers\" : true" + new[m++] = " }," + for (i = 1; i < n; i++) new[m++] = out[i] + n = m + for (i = 0; i < n; i++) out[i] = new[i] + } + # drop commas left dangling before a closing brace/bracket + for (i = 0; i < n; i++) { + if (i + 1 < n && out[i] ~ /,[[:space:]]*$/ && out[i+1] ~ /^[[:space:]]*[]}]/) + sub(/,[[:space:]]*$/, "", out[i]) + print out[i] + } + }' "$VRSETTINGS" > "$VRSETTINGS.tmp" || { rm -f "$VRSETTINGS.tmp"; return 1; } + mv "$VRSETTINGS.tmp" "$VRSETTINGS" +} + +# Remove the keys this installer set (only when VRto3D is the forced driver). +strip_vrsettings() { + grep -q '"forcedDriver"[[:space:]]*:[[:space:]]*"vrto3d"' "$VRSETTINGS" || return 0 + awk ' + /^[[:space:]]*"(requireHmd|forcedDriver)"[[:space:]]*:/ { next } + { out[n++] = $0 } + END { + for (i = 0; i < n; i++) { + if (i + 1 < n && out[i] ~ /,[[:space:]]*$/ && out[i+1] ~ /^[[:space:]]*[]}]/) + sub(/,[[:space:]]*$/, "", out[i]) + print out[i] + } + }' "$VRSETTINGS" > "$VRSETTINGS.tmp" || { rm -f "$VRSETTINGS.tmp"; return 1; } + mv "$VRSETTINGS.tmp" "$VRSETTINGS" +} + +# --- uninstall ---------------------------------------------------------------- +if [ "$UNINSTALL" = 1 ]; then + rm -rf "$DRIVER_DST" + [ -f "$VRSETTINGS" ] && strip_vrsettings + echo "VRto3D removed from $STEAMVR" + echo "(config/profiles left in $STEAM_ROOT/config/vrto3d - delete manually if unwanted)" + exit 0 +fi + +# --- install -------------------------------------------------------------------- +[ -d "$SCRIPT_DIR/drivers/vrto3d" ] || die "drivers/vrto3d not found next to install.sh" + +rm -rf "$DRIVER_DST" +mkdir -p "$STEAMVR/drivers" +cp -a "$SCRIPT_DIR/drivers/vrto3d" "$DRIVER_DST" + +# Register as the forced HMD driver (the settings the README documents). +if [ -f "$VRSETTINGS" ]; then + cp "$VRSETTINGS" "$VRSETTINGS.bak" + if ! patch_vrsettings; then + echo "note: could not merge into steamvr.vrsettings - writing a fresh one" >&2 + echo " (previous file kept at $VRSETTINGS.bak)" >&2 + write_fresh_vrsettings + fi +else + write_fresh_vrsettings +fi + +echo "VRto3D installed to $DRIVER_DST" + +# Hotkeys/gamepad read /dev/input directly, which needs the input group. +if ! id -nG | grep -qw input; then + echo + echo "NOTE: your user is not in the 'input' group - hotkeys and gamepad" + echo "input will not work until you run:" + echo " sudo usermod -aG input ${USER:-$(id -un)}" + echo "and log out and back in." +fi + +echo +echo "Done. Start SteamVR - the 3D output window should appear on your display." +echo "Open the OSD with Ctrl + Home to pick display, output mode, and resolution." diff --git a/tools/make_fp_edid.py b/tools/make_fp_edid.py new file mode 100644 index 00000000..d3bf4441 --- /dev/null +++ b/tools/make_fp_edid.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Generate EDID firmware binaries for HDMI 1.4 frame-packed timings. + +For Wayland sessions (no runtime modelines) or displays whose EDID doesn't +advertise the frame-packed timing, load one of these via the kernel: + + sudo mkdir -p /usr/lib/firmware/edid # SteamOS: steamos-readonly disable first + sudo cp vrto3d_fp1080p60cvt.bin /usr/lib/firmware/edid/ + # then add to kernel cmdline (connector name from `ls /sys/class/drm`): + drm.edid_firmware=HDMI-A-1:edid/vrto3d_fp1080p60cvt.bin + +On X11 sessions this is unnecessary — VRto3D applies the modeline at runtime +via XRandR (see presenter/x11_modeline.cpp). + +Timings mirror s_frame_pack_timings in external/VRto3DLib/src/json_manager.cpp. +""" +import struct +import sys + +# name, active_w, active_h, refresh, h_total, h_front, h_sync, h_back, v_total, v_front, v_sync, v_back +TIMINGS = [ + ("vrto3d_fp720p60", 1280, 1470, 60, 1650, 110, 40, 220, 1500, 5, 5, 20), + ("vrto3d_fp1080p24", 1920, 2205, 24, 2750, 638, 44, 148, 2250, 4, 5, 36), + ("vrto3d_fp1080p60", 1920, 2205, 60, 2750, 638, 44, 148, 2250, 4, 5, 36), + ("vrto3d_fp1080p60cvt", 1920, 2205, 60, 2080, 48, 32, 80, 2250, 4, 5, 36), +] + + +def dtd(active_w, active_h, refresh, h_total, h_front, h_sync, h_back, v_total, v_front, v_sync, v_back): + """18-byte EDID Detailed Timing Descriptor.""" + pclk_10khz = round(h_total * v_total * refresh / 10000) + h_blank = h_total - active_w + v_blank = v_total - active_h + d = bytearray(18) + struct.pack_into("> 8) << 4) | (h_blank >> 8) + d[5] = active_h & 0xFF + d[6] = v_blank & 0xFF + d[7] = ((active_h >> 8) << 4) | (v_blank >> 8) + d[8] = h_front & 0xFF + d[9] = h_sync & 0xFF + d[10] = ((v_front & 0xF) << 4) | (v_sync & 0xF) + d[11] = (((h_front >> 8) & 3) << 6) | (((h_sync >> 8) & 3) << 4) \ + | (((v_front >> 4) & 3) << 2) | ((v_sync >> 4) & 3) + # Image size unknown (0), no borders. + d[17] = 0x1E # digital separate sync, +hsync +vsync + return bytes(d) + + +def display_name_descriptor(name): + payload = (name[:12] + "\n").ljust(13, " ").encode("ascii") + return b"\x00\x00\x00\xfc\x00" + payload + + +def dummy_descriptor(): + return b"\x00\x00\x00\x10\x00" + b"\x00" * 13 + + +def make_edid(name, timing): + e = bytearray(128) + e[0:8] = b"\x00\xff\xff\xff\xff\xff\xff\x00" + # Manufacturer "VTD", product 0x3D01 + mfg = ((ord("V") - 64) << 10) | ((ord("T") - 64) << 5) | (ord("D") - 64) + struct.pack_into(">H", e, 8, mfg) + struct.pack_into(" + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +class LowPassFilter { + float y_ = 0.0f, s_ = 0.0f, a_ = 0.5f; + bool initialized_ = false; + +public: + LowPassFilter() = default; + explicit LowPassFilter(float alpha, float initval = 0.0f) + : y_(initval), s_(initval) + , a_(alpha < 0.0001f ? 0.0001f : (alpha > 1.0f ? 1.0f : alpha)) {} + + float filter(float value) { + if (initialized_) { + s_ = a_ * value + (1.0f - a_) * s_; + } else { + s_ = value; + initialized_ = true; + } + y_ = value; + return s_; + } + + float filterWithAlpha(float value, float alpha) { + a_ = (alpha < 0.0001f) ? 0.0001f : (alpha > 1.0f ? 1.0f : alpha); + return filter(value); + } + + bool hasLastRawValue() const { return initialized_; } + float lastRawValue() const { return y_; } + float lastFilteredValue() const { return s_; } + void reset() { initialized_ = false; y_ = 0.0f; s_ = 0.0f; } +}; + +class OneEuroFilter { + float freq_, mincutoff_, beta_, dcutoff_; + LowPassFilter x_, dx_; + float lasttime_ = -1.0f; + + float alpha(float cutoff) const { + float te = 1.0f / freq_; + float tau = 1.0f / (2.0f * static_cast(M_PI) * cutoff); + return 1.0f / (1.0f + tau / te); + } + +public: + OneEuroFilter() : freq_(60.0f), mincutoff_(1.0f), beta_(0.0f), dcutoff_(1.0f) { + x_ = LowPassFilter(alpha(mincutoff_)); + dx_ = LowPassFilter(alpha(dcutoff_)); + } + + OneEuroFilter(float freq, float mincutoff = 1.0f, float beta = 0.0f, float dcutoff = 1.0f) + : freq_(freq > 0.0f ? freq : 60.0f) + , mincutoff_(mincutoff > 0.0f ? mincutoff : 1.0f) + , beta_(beta) + , dcutoff_(dcutoff > 0.0f ? dcutoff : 1.0f) + { + x_ = LowPassFilter(alpha(mincutoff_)); + dx_ = LowPassFilter(alpha(dcutoff_)); + } + + float filter(float value, float timestamp = -1.0f) { + if (lasttime_ >= 0.0f && timestamp >= 0.0f && timestamp > lasttime_) { + float dt = timestamp - lasttime_; + if (dt > 0.0f) freq_ = 1.0f / dt; + } + lasttime_ = timestamp; + + float dvalue = x_.hasLastRawValue() + ? (value - x_.lastFilteredValue()) * freq_ + : 0.0f; + float edvalue = dx_.filterWithAlpha(dvalue, alpha(dcutoff_)); + + float cutoff = mincutoff_ + beta_ * std::fabs(edvalue); + + return x_.filterWithAlpha(value, alpha(cutoff)); + } + + void setFrequency(float f) { if (f > 0.0f) freq_ = f; } + void setMinCutoff(float mc) { if (mc > 0.0f) mincutoff_ = mc; } + void setBeta(float b) { beta_ = b; } + float getMinCutoff() const { return mincutoff_; } + float getBeta() const { return beta_; } + float getFrequency() const { return freq_; } + void reset() { x_.reset(); dx_.reset(); lasttime_ = -1.0f; } +}; diff --git a/utils/vrmath/util_vrmath.vcxproj b/utils/vrmath/util_vrmath.vcxproj index da1e5b6e..fadb22cb 100644 --- a/utils/vrmath/util_vrmath.vcxproj +++ b/utils/vrmath/util_vrmath.vcxproj @@ -146,6 +146,7 @@ + diff --git a/vrto3d/CMakeLists.txt b/vrto3d/CMakeLists.txt new file mode 100644 index 00000000..05605604 --- /dev/null +++ b/vrto3d/CMakeLists.txt @@ -0,0 +1,286 @@ +cmake_minimum_required(VERSION 3.21) +project(vrto3d_driver LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(ROOT "${CMAKE_CURRENT_SOURCE_DIR}/..") + +# Sources shared by both platforms. +set(DRIVER_SOURCES + src/hmd_device_driver.cpp + src/hmd_driver_factory.cpp + src/device_provider.cpp + src/vr_recenter.cpp + src/osd/osd_menu.cpp +) + +set(IMGUI_DIR "${ROOT}/external/imgui") + +if(WIN32) + list(APPEND DRIVER_SOURCES + src/dx11_renderer.cpp + src/screenshot.cpp + src/auto_depth.cpp + src/platform_win32.cpp + src/presenter/presenter_factory.cpp + src/presenter/window_presenter.cpp + src/presenter/leiasr_presenter.cpp + src/presenter/nvstereo_dx9_presenter.cpp + src/presenter/wibblewobble_presenter.cpp + src/presenter/display_timing_helper.cpp + src/osd/osd_renderer.cpp + src/osd/osd_input.cpp + ) +else() + list(APPEND DRIVER_SOURCES + src/platform_linux.cpp + src/vk/vk_context.cpp + src/vk/vk_renderer.cpp + src/vk/direct_mode_component_vk.cpp + src/presenter/vk_swapchain_util.cpp + src/presenter/vk_presenter_factory.cpp + src/presenter/wayland_presenter.cpp + src/presenter/x11_presenter.cpp + src/presenter/x11_modeline.cpp + src/presenter/wibblewobble_presenter_linux.cpp + src/presenter/wayland_protocols/xdg-shell-client-protocol.c + src/presenter/wayland_protocols/wlr-layer-shell-client-protocol.c + src/osd/osd_renderer_vk.cpp + src/osd/osd_input_linux.cpp + ) +endif() + +# Dear ImGui (vendored as submodule at external/imgui). +# Compiled directly into the driver — no separate target. +set(IMGUI_SOURCES + ${IMGUI_DIR}/imgui.cpp + ${IMGUI_DIR}/imgui_draw.cpp + ${IMGUI_DIR}/imgui_tables.cpp + ${IMGUI_DIR}/imgui_widgets.cpp +) +if(WIN32) + list(APPEND IMGUI_SOURCES ${IMGUI_DIR}/backends/imgui_impl_dx11.cpp) +else() + list(APPEND IMGUI_SOURCES ${IMGUI_DIR}/backends/imgui_impl_vulkan.cpp) +endif() +list(APPEND DRIVER_SOURCES ${IMGUI_SOURCES}) + +add_library(driver_vrto3d SHARED ${DRIVER_SOURCES}) + +target_include_directories(driver_vrto3d PRIVATE + src + ${ROOT}/external/VRto3DLib/include + ${ROOT}/external/json/include + ${ROOT}/external/openvr/headers + ${ROOT}/utils/vrmath + ${IMGUI_DIR} + ${IMGUI_DIR}/backends +) + +# VRto3DLib submodule — compile its sources directly. No transitive target yet. +target_sources(driver_vrto3d PRIVATE + ${ROOT}/external/VRto3DLib/src/json_manager.cpp + ${ROOT}/external/VRto3DLib/src/key_names.cpp + ${ROOT}/external/VRto3DLib/src/app_id_mgr.cpp +) + +if(WIN32) + target_include_directories(driver_vrto3d PRIVATE + ${ROOT}/external/SR-lib/SR-SDK-1.34.10/x64/include + ${ROOT}/external/SR-lib/SR-SDK-1.34.10/x64/third_party/OpenCV/include + ${ROOT}/external/nvapi + ${ROOT}/external/adl_sdk/include + ) + + # Pre-built openvr_api import lib ships with the submodule. + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(OPENVR_LIB_DIR "${ROOT}/external/openvr/lib/win64") + else() + set(OPENVR_LIB_DIR "${ROOT}/external/openvr/lib/win32") + endif() + target_link_libraries(driver_vrto3d PRIVATE + "${OPENVR_LIB_DIR}/openvr_api.lib" + "${ROOT}/external/SR-lib/SR-SDK-1.34.10/x64/lib/simulatedreality.lib" + "${ROOT}/external/SR-lib/SR-SDK-1.34.10/x64/lib/SimulatedRealityCore.lib" + "${ROOT}/external/SR-lib/SR-SDK-1.34.10/x64/lib/SimulatedRealityDirectX.lib" + "${ROOT}/external/SR-lib/SR-SDK-1.34.10/x64/lib/DimencoWeaving.lib" + "${ROOT}/external/SR-lib/SR-SDK-1.34.10/x64/third_party/OpenCV/lib/x64/opencv_core343.lib" + "${ROOT}/external/nvapi/amd64/nvapi64.lib" + delayimp + d3d11 + dxgi + dxguid + d3dcompiler + d3d9 + xinput9_1_0 + ws2_32 + user32 + gdi32 + urlmon + ) + # Delay-load LeiaSR + OpenCV + NVAPI DLLs so the driver still loads when + # those runtimes aren't installed. The DLLs are only resolved when their + # first symbol is actually called (i.e., when LeiaSrPresenter::Init runs, + # or when NV3D-Lib calls NvAPI_Initialize inside CreateInterfaceDX11 — + # the lib SEH-guards that call so a missing nvapi64.dll fails cleanly). + target_link_options(driver_vrto3d PRIVATE + "/DELAYLOAD:simulatedreality.dll" + "/DELAYLOAD:SimulatedRealityCore.dll" + "/DELAYLOAD:SimulatedRealityDirectX.dll" + "/DELAYLOAD:DimencoWeaving.dll" + "/DELAYLOAD:opencv_core343.dll" + "/DELAYLOAD:nvapi64.dll" + "/DELAYLOAD:openvr_api.dll" + ) + target_compile_definitions(driver_vrto3d PRIVATE + WIN32_LEAN_AND_MEAN + NOMINMAX + UNICODE + _UNICODE + ) + + # --- NV3D-Lib (external/NV3D-Lib, MSBuild-only upstream) ----------------- + # Compiled here as a separate static target so its own vendored nvapi / + # json / minhook headers can't shadow VRto3D's copies (the driver uses + # external/nvapi and external/json/include directly). Only the DX11 + # backend TU is built: the DX12/OGL/Vulkan backends are separate TUs whose + # CreateInterface* factories nothing references, and skipping them avoids + # a Vulkan-Headers dependency (only vulkan_backend.cpp includes it). + # MinHook is compiled INTO this target (from NV3D-Lib's own nested + # submodule) — it powers the lib's 3D Vision suppressor; VRto3D no longer + # carries a minhook copy of its own. + set(NV3DLIB_DIR "${ROOT}/external/NV3D-Lib/NV3DLib") + set(NV3DLIB_EXT "${ROOT}/external/NV3D-Lib/external") + + # nvtimings_embedded.h is gitignored upstream and regenerated at build + # time. Generate into the submodule's src/ (the same location NV3D-Lib's + # own MSBuild pre-build writes): lightboost.cpp includes it with quotes, + # so the sibling-directory lookup always wins over include paths — a + # binary-dir copy could be shadowed by a stale sibling. + set(NV3D_EMBED_HDR "${NV3DLIB_DIR}/src/nvtimings_embedded.h") + add_custom_command( + OUTPUT "${NV3D_EMBED_HDR}" + COMMAND powershell.exe -NoProfile -ExecutionPolicy Bypass + -File "${ROOT}/external/NV3D-Lib/tools/embed_nvtimings.ps1" + -InputJson "${ROOT}/external/NV3D-Lib/nvtimings.json" + -OutputHeader "${NV3D_EMBED_HDR}" + DEPENDS "${ROOT}/external/NV3D-Lib/nvtimings.json" + "${ROOT}/external/NV3D-Lib/tools/embed_nvtimings.ps1" + COMMENT "Generating NV3D-Lib nvtimings_embedded.h" + ) + + set(NV3DLIB_SOURCES + ${NV3DLIB_DIR}/src/NV3D.cpp + ${NV3DLIB_DIR}/src/d3d9_presenter.cpp + ${NV3DLIB_DIR}/src/lightboost.cpp + ${NV3DLIB_DIR}/src/log.cpp + ${NV3DLIB_DIR}/src/nv_3dvision_suppressor.cpp + ${NV3DLIB_DIR}/src/present_window.cpp + ${NV3DLIB_DIR}/src/backends/dx11_backend.cpp + ${NV3D_EMBED_HDR} + ${NV3DLIB_EXT}/minhook/src/buffer.c + ${NV3DLIB_EXT}/minhook/src/hook.c + ${NV3DLIB_EXT}/minhook/src/trampoline.c + ) + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + list(APPEND NV3DLIB_SOURCES ${NV3DLIB_EXT}/minhook/src/hde/hde64.c) + else() + list(APPEND NV3DLIB_SOURCES ${NV3DLIB_EXT}/minhook/src/hde/hde32.c) + endif() + + add_library(nv3dlib STATIC ${NV3DLIB_SOURCES}) + target_include_directories(nv3dlib + PUBLIC ${NV3DLIB_DIR}/include + PRIVATE ${NV3DLIB_DIR}/src + ${NV3DLIB_EXT}/nvapi + ${NV3DLIB_EXT}/minhook/include + ${NV3DLIB_EXT}/json/single_include + ) + # Parity with NV3DLib.vcxproj (note: no UNICODE here — the lib uses + # explicit W-suffixed Win32 APIs). + target_compile_definitions(nv3dlib PRIVATE + WIN32_LEAN_AND_MEAN + NOMINMAX + _CRT_SECURE_NO_WARNINGS + ) + set_property(TARGET nv3dlib PROPERTY CXX_STANDARD 17) + set_property(TARGET nv3dlib PROPERTY CXX_STANDARD_REQUIRED ON) + # nvapi64.lib / d3d9 / d3d11 / dxgi already on the driver's link line + # satisfy the archive's externals. + target_link_libraries(driver_vrto3d PRIVATE nv3dlib) +else() + # Linux: the driver links no openvr import lib (the driver context comes + # in through HmdDriverFactory); everything else is system libraries. + target_sources(driver_vrto3d PRIVATE + ${ROOT}/external/VRto3DLib/src/linux_input.cpp + ) + target_include_directories(driver_vrto3d PRIVATE + ${ROOT}/external/stb + src/presenter/wayland_protocols + /usr/include/libdrm + ) + find_package(Threads REQUIRED) + # libopenvr_api.so is needed for the OpenVR *client* API used by + # vr_recenter.cpp (VR_Init/VR_GetGenericInterface); the driver-side + # interfaces come in through HmdDriverFactory and need no library. + target_link_libraries(driver_vrto3d PRIVATE + "${ROOT}/external/openvr/lib/linux64/libopenvr_api.so" + vulkan + X11 + Xext + Xrandr + wayland-client + xkbcommon + dl + Threads::Threads + ) + target_compile_options(driver_vrto3d PRIVATE -fvisibility=hidden -fPIC) + set_target_properties(driver_vrto3d PROPERTIES + PREFIX "" + VISIBILITY_INLINES_HIDDEN ON + BUILD_RPATH "$ORIGIN" + INSTALL_RPATH "$ORIGIN" + ) +endif() + +# Post-build: copy manifest + settings next to the binary and drop the driver +# into SteamVR's drivers/ dir for quick iteration. +set(OUT_DIR "${CMAKE_BINARY_DIR}/output/drivers/vrto3d") +if(WIN32) + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(BIN_SUBDIR "bin/win64") + set(OPENVR_DLL "${ROOT}/external/openvr/bin/win64/openvr_api.dll") + else() + set(BIN_SUBDIR "bin/win32") + set(OPENVR_DLL "${ROOT}/external/openvr/bin/win32/openvr_api.dll") + endif() +else() + set(BIN_SUBDIR "bin/linux64") +endif() + +add_custom_command(TARGET driver_vrto3d POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${OUT_DIR}/${BIN_SUBDIR}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ + "${OUT_DIR}/${BIN_SUBDIR}/$" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_CURRENT_SOURCE_DIR}/vrto3d" + "${OUT_DIR}" + COMMENT "Staging driver_vrto3d into ${OUT_DIR}" +) + +if(WIN32) + add_custom_command(TARGET driver_vrto3d POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${OPENVR_DLL}" + "${OUT_DIR}/${BIN_SUBDIR}/openvr_api.dll" + COMMENT "Staging openvr_api.dll next to driver_vrto3d" + ) +else() + # RPATH $ORIGIN resolves this staged copy at dlopen time. + add_custom_command(TARGET driver_vrto3d POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${ROOT}/external/openvr/bin/linux64/libopenvr_api.so" + "${OUT_DIR}/${BIN_SUBDIR}/libopenvr_api.so" + COMMENT "Staging libopenvr_api.so next to driver_vrto3d" + ) +endif() diff --git a/vrto3d/protocols/wlr-layer-shell-unstable-v1.xml b/vrto3d/protocols/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 00000000..f458f65b --- /dev/null +++ b/vrto3d/protocols/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,393 @@ + + + + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and have margins, or be + positioned in a specific part of the screen. They are intended for + surfaces that are drawn as part of the desktop, like panels and docks. + + + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + After creating a layer_surface object and setting it up, the client + must perform an initial commit without any buffer attached. + The compositor will reply with a layer_surface.configure event. + The client must acknowledge it and is then allowed to attach a buffer + to map the surface. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + + + + + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + + This request indicates that the client will not use the layer_shell + object any more. Objects that have been created through this instance + are not affected. + + + + + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (layer, size, anchor, exclusive zone, + margin, interactivity) is double-buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. + + Attaching a null buffer to a layer surface unmaps it. + + Unmapping a layer_surface means that the surface cannot be shown by the + compositor until it is explicitly mapped again. The layer_surface + returns to the state it had right after layer_shell.get_layer_surface. + The client can re-map the surface by performing a commit without any + buffer attached, waiting for a configure event and handling it as usual. + + + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + + + + + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthogonal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + + + + + + + Requests that the compositor avoids occluding an area with other + surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to one + edge or an edge and both perpendicular edges. If the surface is not + anchored, anchored to only two perpendicular edges (a corner), anchored + to only two parallel edges or anchored to all edges, a positive value + will be treated the same as zero. + + A positive zone is the distance from the edge in surface-local + coordinates to consider exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive exclusive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + + + + + Set how keyboard events are delivered to this surface. By default, + layer shell surfaces do not receive keyboard events; this request can + be used to change this. + + This setting is inherited by child surfaces set by the get_popup + request. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Keyboard interactivity is double-buffered, see wl_surface.commit. + + + + + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + This request destroys the layer surface. + + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + + + + + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + + + + + + + + + + + + + + + + + + + + + + Change the layer that the surface is rendered on. + + Layer is double-buffered, see wl_surface.commit. + + + + + + + + + Types of keyboard interaction possible for layer shell surfaces. The + rationale for this is twofold: (1) some applications are not interested + in keyboard events and not allowing them to be focused can improve the + desktop experience; (2) some applications will want to take exclusive + keyboard focus. + + + + + This value indicates that this surface is not interested in keyboard + events and the compositor should never assign it the keyboard focus. + + This is the default value, set for newly created layer shell surfaces. + + This is useful for e.g. desktop widgets that display information or + only have interaction with non-keyboard input devices. + + + + + + Request exclusive keyboard focus if this surface is above the shell surface layer. + + For the top and overlay layers, the seat will always give + exclusive keyboard focus to the top-most layer which has keyboard + interactivity set to exclusive. If this layer contains multiple + surfaces with keyboard interactivity set to exclusive, the compositor + determines the one receiving keyboard events in an implementation- + defined manner. In this case, no guarantee is made when this surface + will receive keyboard focus (if ever). + + For the bottom and background layers, the compositor is allowed to use + normal focus semantics. + + This setting is mainly intended for applications that need to ensure + they receive all keyboard events, such as as lock screens or on-screen + keyboards. + + + + + + This requests the compositor to allow this surface to be focused and + unfocused by the user in an implementation-defined manner. The user + should be able to unfocus this surface even regardless of the layer + it is on. + + Typically, the compositor will want to use its normal mechanism to + manage keyboard focus between layer shell surfaces with this setting + and regular toplevels on the desktop layer (e.g. click to focus). + Nevertheless, it is possible for a compositor to require a special + interaction to focus or unfocus layer shell surfaces (e.g. requiring + a click even if focus follows the mouse normally, or providing a + keybinding to switch focus between layers). + + This setting is mainly intended for desktop shell components (e.g. + panels) that allow keyboard interaction. Using this option can allow + implementing a desktop shell that can be fully usable without the + mouse. + + + + + diff --git a/vrto3d/protocols/xdg-shell.xml b/vrto3d/protocols/xdg-shell.xml new file mode 100644 index 00000000..39ecf8ac --- /dev/null +++ b/vrto3d/protocols/xdg-shell.xml @@ -0,0 +1,1418 @@ + + + + + Copyright © 2008-2013 Kristian Høgsberg + Copyright © 2013 Rafael Antognolli + Copyright © 2013 Jasper St. Pierre + Copyright © 2010-2013 Intel Corporation + Copyright © 2015-2017 Samsung Electronics Co., Ltd + Copyright © 2015-2017 Red Hat Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + The xdg_wm_base interface is exposed as a global object enabling clients + to turn their wl_surfaces into windows in a desktop environment. It + defines the basic functionality needed for clients and the compositor to + create windows that can be dragged, resized, maximized, etc, as well as + creating transient windows such as popup menus. + + + + + + + + + + + + + + + Destroy this xdg_wm_base object. + + Destroying a bound xdg_wm_base object while there are surfaces + still alive created by this xdg_wm_base object instance is illegal + and will result in a defunct_surfaces error. + + + + + + Create a positioner object. A positioner object is used to position + surfaces relative to some parent surface. See the interface description + and xdg_surface.get_popup for details. + + + + + + + This creates an xdg_surface for the given surface. While xdg_surface + itself is not a role, the corresponding surface may only be assigned + a role extending xdg_surface, such as xdg_toplevel or xdg_popup. It is + illegal to create an xdg_surface for a wl_surface which already has an + assigned role and this will result in a role error. + + This creates an xdg_surface for the given surface. An xdg_surface is + used as basis to define a role to a given surface, such as xdg_toplevel + or xdg_popup. It also manages functionality shared between xdg_surface + based surface roles. + + See the documentation of xdg_surface for more details about what an + xdg_surface is and how it is used. + + + + + + + + A client must respond to a ping event with a pong request or + the client may be deemed unresponsive. See xdg_wm_base.ping + and xdg_wm_base.error.unresponsive. + + + + + + + The ping event asks the client if it's still alive. Pass the + serial specified in the event back to the compositor by sending + a "pong" request back with the specified serial. See xdg_wm_base.pong. + + Compositors can use this to determine if the client is still + alive. It's unspecified what will happen if the client doesn't + respond to the ping request, or in what timeframe. Clients should + try to respond in a reasonable amount of time. The “unresponsive” + error is provided for compositors that wish to disconnect unresponsive + clients. + + A compositor is free to ping in any way it wants, but a client must + always respond to any xdg_wm_base object it created. + + + + + + + + The xdg_positioner provides a collection of rules for the placement of a + child surface relative to a parent surface. Rules can be defined to ensure + the child surface remains within the visible area's borders, and to + specify how the child surface changes its position, such as sliding along + an axis, or flipping around a rectangle. These positioner-created rules are + constrained by the requirement that a child surface must intersect with or + be at least partially adjacent to its parent surface. + + See the various requests for details about possible rules. + + At the time of the request, the compositor makes a copy of the rules + specified by the xdg_positioner. Thus, after the request is complete the + xdg_positioner object can be destroyed or reused; further changes to the + object will have no effect on previous usages. + + For an xdg_positioner object to be considered complete, it must have a + non-zero size set by set_size, and a non-zero anchor rectangle set by + set_anchor_rect. Passing an incomplete xdg_positioner object when + positioning a surface raises an invalid_positioner error. + + + + + + + + + Notify the compositor that the xdg_positioner will no longer be used. + + + + + + Set the size of the surface that is to be positioned with the positioner + object. The size is in surface-local coordinates and corresponds to the + window geometry. See xdg_surface.set_window_geometry. + + If a zero or negative size is set the invalid_input error is raised. + + + + + + + + Specify the anchor rectangle within the parent surface that the child + surface will be placed relative to. The rectangle is relative to the + window geometry as defined by xdg_surface.set_window_geometry of the + parent surface. + + When the xdg_positioner object is used to position a child surface, the + anchor rectangle may not extend outside the window geometry of the + positioned child's parent surface. + + If a negative size is set the invalid_input error is raised. + + + + + + + + + + + + + + + + + + + + + + Defines the anchor point for the anchor rectangle. The specified anchor + is used derive an anchor point that the child surface will be + positioned relative to. If a corner anchor is set (e.g. 'top_left' or + 'bottom_right'), the anchor point will be at the specified corner; + otherwise, the derived anchor point will be centered on the specified + edge, or in the center of the anchor rectangle if no edge is specified. + + + + + + + + + + + + + + + + + + + Defines in what direction a surface should be positioned, relative to + the anchor point of the parent surface. If a corner gravity is + specified (e.g. 'bottom_right' or 'top_left'), then the child surface + will be placed towards the specified gravity; otherwise, the child + surface will be centered over the anchor point on any axis that had no + gravity specified. If the gravity is not in the ‘gravity’ enum, an + invalid_input error is raised. + + + + + + + The constraint adjustment value define ways the compositor will adjust + the position of the surface, if the unadjusted position would result + in the surface being partly constrained. + + Whether a surface is considered 'constrained' is left to the compositor + to determine. For example, the surface may be partly outside the + compositor's defined 'work area', thus necessitating the child surface's + position be adjusted until it is entirely inside the work area. + + The adjustments can be combined, according to a defined precedence: 1) + Flip, 2) Slide, 3) Resize. + + + + Don't alter the surface position even if it is constrained on some + axis, for example partially outside the edge of an output. + + + + + Slide the surface along the x axis until it is no longer constrained. + + First try to slide towards the direction of the gravity on the x axis + until either the edge in the opposite direction of the gravity is + unconstrained or the edge in the direction of the gravity is + constrained. + + Then try to slide towards the opposite direction of the gravity on the + x axis until either the edge in the direction of the gravity is + unconstrained or the edge in the opposite direction of the gravity is + constrained. + + + + + Slide the surface along the y axis until it is no longer constrained. + + First try to slide towards the direction of the gravity on the y axis + until either the edge in the opposite direction of the gravity is + unconstrained or the edge in the direction of the gravity is + constrained. + + Then try to slide towards the opposite direction of the gravity on the + y axis until either the edge in the direction of the gravity is + unconstrained or the edge in the opposite direction of the gravity is + constrained. + + + + + Invert the anchor and gravity on the x axis if the surface is + constrained on the x axis. For example, if the left edge of the + surface is constrained, the gravity is 'left' and the anchor is + 'left', change the gravity to 'right' and the anchor to 'right'. + + If the adjusted position also ends up being constrained, the resulting + position of the flip_x adjustment will be the one before the + adjustment. + + + + + Invert the anchor and gravity on the y axis if the surface is + constrained on the y axis. For example, if the bottom edge of the + surface is constrained, the gravity is 'bottom' and the anchor is + 'bottom', change the gravity to 'top' and the anchor to 'top'. + + The adjusted position is calculated given the original anchor + rectangle and offset, but with the new flipped anchor and gravity + values. + + If the adjusted position also ends up being constrained, the resulting + position of the flip_y adjustment will be the one before the + adjustment. + + + + + Resize the surface horizontally so that it is completely + unconstrained. + + + + + Resize the surface vertically so that it is completely unconstrained. + + + + + + + Specify how the window should be positioned if the originally intended + position caused the surface to be constrained, meaning at least + partially outside positioning boundaries set by the compositor. The + adjustment is set by constructing a bitmask describing the adjustment to + be made when the surface is constrained on that axis. + + If no bit for one axis is set, the compositor will assume that the child + surface should not change its position on that axis when constrained. + + If more than one bit for one axis is set, the order of how adjustments + are applied is specified in the corresponding adjustment descriptions. + + The default adjustment is none. + + + + + + + Specify the surface position offset relative to the position of the + anchor on the anchor rectangle and the anchor on the surface. For + example if the anchor of the anchor rectangle is at (x, y), the surface + has the gravity bottom|right, and the offset is (ox, oy), the calculated + surface position will be (x + ox, y + oy). The offset position of the + surface is the one used for constraint testing. See + set_constraint_adjustment. + + An example use case is placing a popup menu on top of a user interface + element, while aligning the user interface element of the parent surface + with some user interface element placed somewhere in the popup surface. + + + + + + + + + + When set reactive, the surface is reconstrained if the conditions used + for constraining changed, e.g. the parent window moved. + + If the conditions changed and the popup was reconstrained, an + xdg_popup.configure event is sent with updated geometry, followed by an + xdg_surface.configure event. + + + + + + Set the parent window geometry the compositor should use when + positioning the popup. The compositor may use this information to + determine the future state the popup should be constrained using. If + this doesn't match the dimension of the parent the popup is eventually + positioned against, the behavior is undefined. + + The arguments are given in the surface-local coordinate space. + + + + + + + + Set the serial of an xdg_surface.configure event this positioner will be + used in response to. The compositor may use this information together + with set_parent_size to determine what future state the popup should be + constrained using. + + + + + + + + An interface that may be implemented by a wl_surface, for + implementations that provide a desktop-style user interface. + + It provides a base set of functionality required to construct user + interface elements requiring management by the compositor, such as + toplevel windows, menus, etc. The types of functionality are split into + xdg_surface roles. + + Creating an xdg_surface does not set the role for a wl_surface. In order + to map an xdg_surface, the client must create a role-specific object + using, e.g., get_toplevel, get_popup. The wl_surface for any given + xdg_surface can have at most one role, and may not be assigned any role + not based on xdg_surface. + + A role must be assigned before any other requests are made to the + xdg_surface object. + + The client must call wl_surface.commit on the corresponding wl_surface + for the xdg_surface state to take effect. + + Creating an xdg_surface from a wl_surface which has a buffer attached or + committed is a client error, and any attempts by a client to attach or + manipulate a buffer prior to the first xdg_surface.configure call must + also be treated as errors. + + After creating a role-specific object and setting it up (e.g. by sending + the title, app ID, size constraints, parent, etc), the client must + perform an initial commit without any buffer attached. The compositor + will reply with initial wl_surface state such as + wl_surface.preferred_buffer_scale followed by an xdg_surface.configure + event. The client must acknowledge it and is then allowed to attach a + buffer to map the surface. + + Mapping an xdg_surface-based role surface is defined as making it + possible for the surface to be shown by the compositor. Note that + a mapped surface is not guaranteed to be visible once it is mapped. + + For an xdg_surface to be mapped by the compositor, the following + conditions must be met: + (1) the client has assigned an xdg_surface-based role to the surface + (2) the client has set and committed the xdg_surface state and the + role-dependent state to the surface + (3) the client has committed a buffer to the surface + + A newly-unmapped surface is considered to have met condition (1) out + of the 3 required conditions for mapping a surface if its role surface + has not been destroyed, i.e. the client must perform the initial commit + again before attaching a buffer. + + + + + + + + + + + + + + Destroy the xdg_surface object. An xdg_surface must only be destroyed + after its role object has been destroyed, otherwise + a defunct_role_object error is raised. + + + + + + This creates an xdg_toplevel object for the given xdg_surface and gives + the associated wl_surface the xdg_toplevel role. + + See the documentation of xdg_toplevel for more details about what an + xdg_toplevel is and how it is used. + + + + + + + This creates an xdg_popup object for the given xdg_surface and gives + the associated wl_surface the xdg_popup role. + + If null is passed as a parent, a parent surface must be specified using + some other protocol, before committing the initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + + + The window geometry of a surface is its "visible bounds" from the + user's perspective. Client-side decorations often have invisible + portions like drop-shadows which should be ignored for the + purposes of aligning, placing and constraining windows. Note that + in some situations, compositors may clip rendering to the window + geometry, so the client should avoid putting functional elements + outside of it. + + The window geometry is double-buffered state, see wl_surface.commit. + + When maintaining a position, the compositor should treat the (x, y) + coordinate of the window geometry as the top left corner of the window. + A client changing the (x, y) window geometry coordinate should in + general not alter the position of the window. + + Once the window geometry of the surface is set, it is not possible to + unset it, and it will remain the same until set_window_geometry is + called again, even if a new subsurface or buffer is attached. + + If never set, the value is the full bounds of the surface, + including any subsurfaces. This updates dynamically on every + commit. This unset is meant for extremely simple clients. + + The arguments are given in the surface-local coordinate space of + the wl_surface associated with this xdg_surface, and may extend outside + of the wl_surface itself to mark parts of the subsurface tree as part of + the window geometry. + + When applied, the effective window geometry will be the set window + geometry clamped to the bounding rectangle of the combined + geometry of the surface of the xdg_surface and the associated + subsurfaces. + + The effective geometry will not be recalculated unless a new call to + set_window_geometry is done and the new pending surface state is + subsequently applied. + + The width and height of the effective window geometry must be + greater than zero. Setting an invalid size will raise an + invalid_size error. + + + + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + For instance, for toplevel surfaces the compositor might use this + information to move a surface to the top left only when the client has + drawn itself for the maximized or fullscreen state. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + Acking a configure event that was never sent raises an invalid_serial + error. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + Sending an ack_configure request consumes the serial number sent with + the request, as well as serial numbers sent by all configure events + sent on this xdg_surface prior to the configure event referenced by + the committed serial. + + It is an error to issue multiple ack_configure requests referencing a + serial from the same configure event, or to issue an ack_configure + request referencing a serial from a configure event issued before the + event identified by the last ack_configure request for the same + xdg_surface. Doing so will raise an invalid_serial error. + + + + + + + The configure event marks the end of a configure sequence. A configure + sequence is a set of one or more events configuring the state of the + xdg_surface, including the final xdg_surface.configure event. + + Where applicable, xdg_surface surface roles will during a configure + sequence extend this event as a latched state sent as events before the + xdg_surface.configure event. Such events should be considered to make up + a set of atomically applied configuration states, where the + xdg_surface.configure commits the accumulated state. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + If the client receives multiple configure events before it can respond + to one, it is free to discard all but the last event it received. + + + + + + + + + This interface defines an xdg_surface role which allows a surface to, + among other things, set window-like properties such as maximize, + fullscreen, and minimize, set application-specific metadata like title and + id, and well as trigger user interactive operations such as interactive + resize and move. + + A xdg_toplevel by default is responsible for providing the full intended + visual representation of the toplevel, which depending on the window + state, may mean things like a title bar, window controls and drop shadow. + + Unmapping an xdg_toplevel means that the surface cannot be shown + by the compositor until it is explicitly mapped again. + All active operations (e.g., move, resize) are canceled and all + attributes (e.g. title, state, stacking, ...) are discarded for + an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + the state it had right after xdg_surface.get_toplevel. The client + can re-map the toplevel by performing a commit without any buffer + attached, waiting for a configure event and handling it as usual (see + xdg_surface description). + + Attaching a null buffer to a toplevel unmaps the surface. + + + + + This request destroys the role surface and unmaps the surface; + see "Unmapping" behavior in interface section for details. + + + + + + + + + + + + Set the "parent" of this surface. This surface should be stacked + above the parent surface and all other ancestor surfaces. + + Parent surfaces should be set on dialogs, toolboxes, or other + "auxiliary" surfaces, so that the parent is raised when the dialog + is raised. + + Setting a null parent for a child surface unsets its parent. Setting + a null parent for a surface which currently has no parent is a no-op. + + Only mapped surfaces can have child surfaces. Setting a parent which + is not mapped is equivalent to setting a null parent. If a surface + becomes unmapped, its children's parent is set to the parent of + the now-unmapped surface. If the now-unmapped surface has no parent, + its children's parent is unset. If the now-unmapped surface becomes + mapped again, its parent-child relationship is not restored. + + The parent toplevel must not be one of the child toplevel's + descendants, and the parent must be different from the child toplevel, + otherwise the invalid_parent protocol error is raised. + + + + + + + Set a short title for the surface. + + This string may be used to identify the surface in a task bar, + window list, or other user interface elements provided by the + compositor. + + The string must be encoded in UTF-8. + + + + + + + Set an application identifier for the surface. + + The app ID identifies the general class of applications to which + the surface belongs. The compositor can use this to group multiple + surfaces together, or to determine how to launch a new application. + + For D-Bus activatable applications, the app ID is used as the D-Bus + service name. + + The compositor shell will try to group application surfaces together + by their app ID. As a best practice, it is suggested to select app + ID's that match the basename of the application's .desktop file. + For example, "org.freedesktop.FooViewer" where the .desktop file is + "org.freedesktop.FooViewer.desktop". + + Like other properties, a set_app_id request can be sent after the + xdg_toplevel has been mapped to update the property. + + See the desktop-entry specification [0] for more details on + application identifiers and how they relate to well-known D-Bus + names and .desktop files. + + [0] https://standards.freedesktop.org/desktop-entry-spec/ + + + + + + + Clients implementing client-side decorations might want to show + a context menu when right-clicking on the decorations, giving the + user a menu that they can use to maximize or minimize the window. + + This request asks the compositor to pop up such a window menu at + the given position, relative to the local surface coordinates of + the parent surface. There are no guarantees as to what menu items + the window menu contains, or even if a window menu will be drawn + at all. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. + + + + + + + + + + Start an interactive, user-driven move of the surface. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. The passed + serial is used to determine the type of interactive move (touch, + pointer, etc). + + The server may ignore move requests depending on the state of + the surface (e.g. fullscreen or maximized), or if the passed serial + is no longer valid. + + If triggered, the surface will lose the focus of the device + (wl_pointer, wl_touch, etc) used for the move. It is up to the + compositor to visually indicate that the move is taking place, such as + updating a pointer cursor, during the move. There is no guarantee + that the device focus will return when the move is completed. + + + + + + + + These values are used to indicate which edge of a surface + is being dragged in a resize operation. + + + + + + + + + + + + + + + Start a user-driven, interactive resize of the surface. + + This request must be used in response to some sort of user action + like a button press, key press, or touch down event. The passed + serial is used to determine the type of interactive resize (touch, + pointer, etc). + + The server may ignore resize requests depending on the state of + the surface (e.g. fullscreen or maximized). + + If triggered, the client will receive configure events with the + "resize" state enum value and the expected sizes. See the "resize" + enum value for more details about what is required. The client + must also acknowledge configure events using "ack_configure". After + the resize is completed, the client will receive another "configure" + event without the resize state. + + If triggered, the surface also will lose the focus of the device + (wl_pointer, wl_touch, etc) used for the resize. It is up to the + compositor to visually indicate that the resize is taking place, + such as updating a pointer cursor, during the resize. There is no + guarantee that the device focus will return when the resize is + completed. + + The edges parameter specifies how the surface should be resized, and + is one of the values of the resize_edge enum. Values not matching + a variant of the enum will cause the invalid_resize_edge protocol error. + The compositor may use this information to update the surface position + for example when dragging the top left corner. The compositor may also + use this information to adapt its behavior, e.g. choose an appropriate + cursor image. + + + + + + + + + The different state values used on the surface. This is designed for + state values like maximized, fullscreen. It is paired with the + configure event to ensure that both the client and the compositor + setting the state can be synchronized. + + States set in this way are double-buffered, see wl_surface.commit. + + + + The surface is maximized. The window geometry specified in the configure + event must be obeyed by the client, or the xdg_wm_base.invalid_surface_state + error is raised. + + The client should draw without shadow or other + decoration outside of the window geometry. + + + + + The surface is fullscreen. The window geometry specified in the + configure event is a maximum; the client cannot resize beyond it. For + a surface to cover the whole fullscreened area, the geometry + dimensions must be obeyed by the client. For more details, see + xdg_toplevel.set_fullscreen. + + + + + The surface is being resized. The window geometry specified in the + configure event is a maximum; the client cannot resize beyond it. + Clients that have aspect ratio or cell sizing configuration can use + a smaller size, however. + + + + + Client window decorations should be painted as if the window is + active. Do not assume this means that the window actually has + keyboard or pointer focus. + + + + + The window is currently in a tiled layout and the left edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the left edge. + + + + + The window is currently in a tiled layout and the right edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the right edge. + + + + + The window is currently in a tiled layout and the top edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the top edge. + + + + + The window is currently in a tiled layout and the bottom edge is + considered to be adjacent to another part of the tiling grid. + + The client should draw without shadow or other decoration outside of + the window geometry on the bottom edge. + + + + + The surface is currently not ordinarily being repainted; for + example because its content is occluded by another window, or its + outputs are switched off due to screen locking. + + + + + The left edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + The right edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + The top edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + The bottom edge of the window is currently constrained, meaning it + shouldn't attempt to resize from that edge. It can for example mean + it's tiled next to a monitor edge on the constrained side of the + window. + + + + + + + Set a maximum size for the window. + + The client can specify a maximum size so that the compositor does + not try to configure the window beyond this size. + + The width and height arguments are in window geometry coordinates. + See xdg_surface.set_window_geometry. + + Values set in this way are double-buffered, see wl_surface.commit. + + The compositor can use this information to allow or disallow + different states like maximize or fullscreen and draw accurate + animations. + + Similarly, a tiling window manager may use this information to + place and resize client windows in a more effective way. + + The client should not rely on the compositor to obey the maximum + size. The compositor may decide to ignore the values set by the + client and request a larger size. + + If never set, or a value of zero in the request, means that the + client has no expected maximum size in the given dimension. + As a result, a client wishing to reset the maximum size + to an unspecified state can use zero for width and height in the + request. + + Requesting a maximum size to be smaller than the minimum size of + a surface is illegal and will result in an invalid_size error. + + The width and height must be greater than or equal to zero. Using + strictly negative values for width or height will result in a + invalid_size error. + + + + + + + + Set a minimum size for the window. + + The client can specify a minimum size so that the compositor does + not try to configure the window below this size. + + The width and height arguments are in window geometry coordinates. + See xdg_surface.set_window_geometry. + + Values set in this way are double-buffered, see wl_surface.commit. + + The compositor can use this information to allow or disallow + different states like maximize or fullscreen and draw accurate + animations. + + Similarly, a tiling window manager may use this information to + place and resize client windows in a more effective way. + + The client should not rely on the compositor to obey the minimum + size. The compositor may decide to ignore the values set by the + client and request a smaller size. + + If never set, or a value of zero in the request, means that the + client has no expected minimum size in the given dimension. + As a result, a client wishing to reset the minimum size + to an unspecified state can use zero for width and height in the + request. + + Requesting a minimum size to be larger than the maximum size of + a surface is illegal and will result in an invalid_size error. + + The width and height must be greater than or equal to zero. Using + strictly negative values for width and height will result in a + invalid_size error. + + + + + + + + Maximize the surface. + + After requesting that the surface should be maximized, the compositor + will respond by emitting a configure event. Whether this configure + actually sets the window maximized is subject to compositor policies. + The client must then update its content, drawing in the configured + state. The client must also acknowledge the configure when committing + the new content (see ack_configure). + + It is up to the compositor to decide how and where to maximize the + surface, for example which output and what region of the screen should + be used. + + If the surface was already maximized, the compositor will still emit + a configure event with the "maximized" state. + + If the surface is in a fullscreen state, this request has no direct + effect. It may alter the state the surface is returned to when + unmaximized unless overridden by the compositor. + + + + + + Unmaximize the surface. + + After requesting that the surface should be unmaximized, the compositor + will respond by emitting a configure event. Whether this actually + un-maximizes the window is subject to compositor policies. + If available and applicable, the compositor will include the window + geometry dimensions the window had prior to being maximized in the + configure event. The client must then update its content, drawing it in + the configured state. The client must also acknowledge the configure + when committing the new content (see ack_configure). + + It is up to the compositor to position the surface after it was + unmaximized; usually the position the surface had before maximizing, if + applicable. + + If the surface was already not maximized, the compositor will still + emit a configure event without the "maximized" state. + + If the surface is in a fullscreen state, this request has no direct + effect. It may alter the state the surface is returned to when + unmaximized unless overridden by the compositor. + + + + + + Make the surface fullscreen. + + After requesting that the surface should be fullscreened, the + compositor will respond by emitting a configure event. Whether the + client is actually put into a fullscreen state is subject to compositor + policies. The client must also acknowledge the configure when + committing the new content (see ack_configure). + + The output passed by the request indicates the client's preference as + to which display it should be set fullscreen on. If this value is NULL, + it's up to the compositor to choose which display will be used to map + this surface. + + If the surface doesn't cover the whole output, the compositor will + position the surface in the center of the output and compensate with + with border fill covering the rest of the output. The content of the + border fill is undefined, but should be assumed to be in some way that + attempts to blend into the surrounding area (e.g. solid black). + + If the fullscreened surface is not opaque, the compositor must make + sure that other screen content not part of the same surface tree (made + up of subsurfaces, popups or similarly coupled surfaces) are not + visible below the fullscreened surface. + + + + + + + Make the surface no longer fullscreen. + + After requesting that the surface should be unfullscreened, the + compositor will respond by emitting a configure event. + Whether this actually removes the fullscreen state of the client is + subject to compositor policies. + + Making a surface unfullscreen sets states for the surface based on the following: + * the state(s) it may have had before becoming fullscreen + * any state(s) decided by the compositor + * any state(s) requested by the client while the surface was fullscreen + + The compositor may include the previous window geometry dimensions in + the configure event, if applicable. + + The client must also acknowledge the configure when committing the new + content (see ack_configure). + + + + + + Request that the compositor minimize your surface. There is no + way to know if the surface is currently minimized, nor is there + any way to unset minimization on this surface. + + If you are looking to throttle redrawing when minimized, please + instead use the wl_surface.frame event for this, as this will + also work with live previews on windows in Alt-Tab, Expose or + similar compositor features. + + + + + + This configure event asks the client to resize its toplevel surface or + to change its state. The configured state should not be applied + immediately. See xdg_surface.configure for details. + + The width and height arguments specify a hint to the window + about how its surface should be resized in window geometry + coordinates. See set_window_geometry. + + If the width or height arguments are zero, it means the client + should decide its own window dimension. This may happen when the + compositor needs to configure the state of the surface but doesn't + have any information about any previous or expected dimension. + + The states listed in the event specify how the width/height + arguments should be interpreted, and possibly how it should be + drawn. + + Clients must send an ack_configure in response to this event. See + xdg_surface.configure and xdg_surface.ack_configure for details. + + + + + + + + + The close event is sent by the compositor when the user + wants the surface to be closed. This should be equivalent to + the user clicking the close button in client-side decorations, + if your application has any. + + This is only a request that the user intends to close the + window. The client may choose to ignore this request, or show + a dialog to ask the user to save their data, etc. + + + + + + + + The configure_bounds event may be sent prior to a xdg_toplevel.configure + event to communicate the bounds a window geometry size is recommended + to constrain to. + + The passed width and height are in surface coordinate space. If width + and height are 0, it means bounds is unknown and equivalent to as if no + configure_bounds event was ever sent for this surface. + + The bounds can for example correspond to the size of a monitor excluding + any panels or other shell components, so that a surface isn't created in + a way that it cannot fit. + + The bounds may change at any point, and in such a case, a new + xdg_toplevel.configure_bounds will be sent, followed by + xdg_toplevel.configure and xdg_surface.configure. + + + + + + + + + + + + + + + + + This event advertises the capabilities supported by the compositor. If + a capability isn't supported, clients should hide or disable the UI + elements that expose this functionality. For instance, if the + compositor doesn't advertise support for minimized toplevels, a button + triggering the set_minimized request should not be displayed. + + The compositor will ignore requests it doesn't support. For instance, + a compositor which doesn't advertise support for minimized will ignore + set_minimized requests. + + Compositors must send this event once before the first + xdg_surface.configure event. When the capabilities change, compositors + must send this event again and then send an xdg_surface.configure + event. + + The configured state should not be applied immediately. See + xdg_surface.configure for details. + + The capabilities are sent as an array of 32-bit unsigned integers in + native endianness. + + + + + + + + A popup surface is a short-lived, temporary surface. It can be used to + implement for example menus, popovers, tooltips and other similar user + interface concepts. + + A popup can be made to take an explicit grab. See xdg_popup.grab for + details. + + When the popup is dismissed, a popup_done event will be sent out, and at + the same time the surface will be unmapped. See the xdg_popup.popup_done + event for details. + + Explicitly destroying the xdg_popup object will also dismiss the popup and + unmap the surface. Clients that want to dismiss the popup when another + surface of their own is clicked should dismiss the popup using the destroy + request. + + A newly created xdg_popup will be stacked on top of all previously created + xdg_popup surfaces associated with the same xdg_toplevel. + + The parent of an xdg_popup must be mapped (see the xdg_surface + description) before the xdg_popup itself. + + The client must call wl_surface.commit on the corresponding wl_surface + for the xdg_popup state to take effect. + + + + + + + + + This destroys the popup. Explicitly destroying the xdg_popup + object will also dismiss the popup, and unmap the surface. + + If this xdg_popup is not the "topmost" popup, the + xdg_wm_base.not_the_topmost_popup protocol error will be sent. + + + + + + This request makes the created popup take an explicit grab. An explicit + grab will be dismissed when the user dismisses the popup, or when the + client destroys the xdg_popup. This can be done by the user clicking + outside the surface, using the keyboard, or even locking the screen + through closing the lid or a timeout. + + If the compositor denies the grab, the popup will be immediately + dismissed. + + This request must be used in response to some sort of user action like a + button press, key press, or touch down event. The serial number of the + event should be passed as 'serial'. + + The parent of a grabbing popup must either be an xdg_toplevel surface or + another xdg_popup with an explicit grab. If the parent is another + xdg_popup it means that the popups are nested, with this popup now being + the topmost popup. + + Nested popups must be destroyed in the reverse order they were created + in, e.g. the only popup you are allowed to destroy at all times is the + topmost one. + + When compositors choose to dismiss a popup, they may dismiss every + nested grabbing popup as well. When a compositor dismisses popups, it + will follow the same dismissing order as required from the client. + + If the topmost grabbing popup is destroyed, the grab will be returned to + the parent of the popup, if that parent previously had an explicit grab. + + If the parent is a grabbing popup which has already been dismissed, this + popup will be immediately dismissed. If the parent is a popup that did + not take an explicit grab, an error will be raised. + + During a popup grab, the client owning the grab will receive pointer + and touch events for all their surfaces as normal (similar to an + "owner-events" grab in X11 parlance), while the top most grabbing popup + will always have keyboard focus. + + + + + + + + This event asks the popup surface to configure itself given the + configuration. The configured state should not be applied immediately. + See xdg_surface.configure for details. + + The x and y arguments represent the position the popup was placed at + given the xdg_positioner rule, relative to the upper left corner of the + window geometry of the parent surface. + + For version 2 or older, the configure event for an xdg_popup is only + ever sent once for the initial configuration. Starting with version 3, + it may be sent again if the popup is setup with an xdg_positioner with + set_reactive requested, or in response to xdg_popup.reposition requests. + + + + + + + + + + The popup_done event is sent out when a popup is dismissed by the + compositor. The client should destroy the xdg_popup object at this + point. + + + + + + + + Reposition an already-mapped popup. The popup will be placed given the + details in the passed xdg_positioner object, and a + xdg_popup.repositioned followed by xdg_popup.configure and + xdg_surface.configure will be emitted in response. Any parameters set + by the previous positioner will be discarded. + + The passed token will be sent in the corresponding + xdg_popup.repositioned event. The new popup position will not take + effect until the corresponding configure event is acknowledged by the + client. See xdg_popup.repositioned for details. The token itself is + opaque, and has no other special meaning. + + If multiple reposition requests are sent, the compositor may skip all + but the last one. + + If the popup is repositioned in response to a configure event for its + parent, the client should send an xdg_positioner.set_parent_configure + and possibly an xdg_positioner.set_parent_size request to allow the + compositor to properly constrain the popup. + + If the popup is repositioned together with a parent that is being + resized, but not in response to a configure event, the client should + send an xdg_positioner.set_parent_size request. + + + + + + + + The repositioned event is sent as part of a popup configuration + sequence, together with xdg_popup.configure and lastly + xdg_surface.configure to notify the completion of a reposition request. + + The repositioned event is to notify about the completion of a + xdg_popup.reposition request. The token argument is the token passed + in the xdg_popup.reposition request. + + Immediately after this event is emitted, xdg_popup.configure and + xdg_surface.configure will be sent with the updated size and position, + as well as a new configure serial. + + The client should optionally update the content of the popup, but must + acknowledge the new popup configuration for the new position to take + effect. See xdg_surface.ack_configure for details. + + + + + + diff --git a/vrto3d/shaders/compile_shaders.sh b/vrto3d/shaders/compile_shaders.sh new file mode 100755 index 00000000..711cc68c --- /dev/null +++ b/vrto3d/shaders/compile_shaders.sh @@ -0,0 +1,73 @@ +#!/bin/sh +# +# This file is part of VRto3D. +# +# VRto3D is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# VRto3D is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with VRto3D. If not, see . +# +# Compiles the repack shaders to SPIR-V and wraps them into C headers under +# generated/. The headers are checked in as a toolchain fallback so the build +# only needs a C++ compiler; re-run this script after editing any shader. +# +# Requires: glslc (shaderc), typically from the Vulkan SDK or distro package. + +set -eu +cd "$(dirname "$0")" + +mkdir -p generated + +# emit +# Produces generated/_spv.h defining: +# static const uint32_t _spv[] = { ... }; +# glslc -mfmt=c emits just the {...} initializer list; we wrap it here. +emit() { + in=$1 + name=$2 + out="generated/${name}_spv.h" + tmp=$(mktemp) + + glslc -O -mfmt=c "$in" -o "$tmp" + + { + printf '/*\n' + printf ' * This file is part of VRto3D.\n' + printf ' *\n' + printf ' * VRto3D is free software: you can redistribute it and/or modify\n' + printf ' * it under the terms of the GNU Lesser General Public License as published by\n' + printf ' * the Free Software Foundation, either version 3 of the License, or\n' + printf ' * (at your option) any later version.\n' + printf ' *\n' + printf ' * VRto3D is distributed in the hope that it will be useful,\n' + printf ' * but WITHOUT ANY WARRANTY; without even the implied warranty of\n' + printf ' * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n' + printf ' * GNU Lesser General Public License for more details.\n' + printf ' *\n' + printf ' * You should have received a copy of the GNU Lesser General Public License\n' + printf ' * along with VRto3D. If not, see .\n' + printf ' */\n' + printf '/* Generated by compile_shaders.sh from %s — do not edit. */\n' "$in" + printf '#pragma once\n' + printf '\n' + printf '#include \n' + printf '\n' + printf 'static const uint32_t %s_spv[] =\n' "$name" + cat "$tmp" + printf ';\n' + } > "$out" + + rm -f "$tmp" + echo "wrote $out" +} + +emit fullscreen.vert fullscreen_vert +emit repack.frag repack_frag diff --git a/vrto3d/shaders/fullscreen.vert b/vrto3d/shaders/fullscreen.vert new file mode 100644 index 00000000..2bbb7d50 --- /dev/null +++ b/vrto3d/shaders/fullscreen.vert @@ -0,0 +1,39 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#version 450 + +// Fullscreen triangle from gl_VertexIndex — no vertex buffer. +// +// UV convention: Vulkan clip space is Y-down (NDC y = -1 is the TOP of the +// viewport), so vertex 0 at (-1,-1) is the top-left corner of the output +// window and uv = (pos + 1) / 2 yields uv == (0,0) there. uv.y therefore +// increases downward — uv (0,0) = top-left of the OUTPUT window, matching +// both the D3D11 presenter's texcoord convention and the sbs texture's +// row order (v = 0 samples the top row). Do NOT flip uv.y. +// +// gl_VertexIndex 0 -> pos (-1,-1) uv (0,0) +// gl_VertexIndex 1 -> pos ( 3,-1) uv (2,0) +// gl_VertexIndex 2 -> pos (-1, 3) uv (0,2) + +layout(location = 0) out vec2 uv; + +void main() { + vec2 pos = vec2((gl_VertexIndex == 1) ? 3.0 : -1.0, + (gl_VertexIndex == 2) ? 3.0 : -1.0); + gl_Position = vec4(pos, 0.0, 1.0); + uv = (pos + 1.0) * 0.5; +} diff --git a/vrto3d/shaders/generated/fullscreen_vert_spv.h b/vrto3d/shaders/generated/fullscreen_vert_spv.h new file mode 100644 index 00000000..28e54791 --- /dev/null +++ b/vrto3d/shaders/generated/fullscreen_vert_spv.h @@ -0,0 +1,80 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +/* Generated by compile_shaders.sh from fullscreen.vert — do not edit. */ +#pragma once + +#include + +static const uint32_t fullscreen_vert_spv[] = +{0x07230203,0x00010000,0x000d000b,0x00000031, +0x00000000,0x00020011,0x00000001,0x0006000b, +0x00000001,0x4c534c47,0x6474732e,0x3035342e, +0x00000000,0x0003000e,0x00000000,0x00000001, +0x0008000f,0x00000000,0x00000004,0x6e69616d, +0x00000000,0x0000000c,0x0000001f,0x0000002a, +0x00040047,0x0000000c,0x0000000b,0x0000002a, +0x00030047,0x0000001d,0x00000002,0x00050048, +0x0000001d,0x00000000,0x0000000b,0x00000000, +0x00050048,0x0000001d,0x00000001,0x0000000b, +0x00000001,0x00050048,0x0000001d,0x00000002, +0x0000000b,0x00000003,0x00050048,0x0000001d, +0x00000003,0x0000000b,0x00000004,0x00040047, +0x0000002a,0x0000001e,0x00000000,0x00020013, +0x00000002,0x00030021,0x00000003,0x00000002, +0x00030016,0x00000006,0x00000020,0x00040017, +0x00000007,0x00000006,0x00000002,0x00040015, +0x0000000a,0x00000020,0x00000001,0x00040020, +0x0000000b,0x00000001,0x0000000a,0x0004003b, +0x0000000b,0x0000000c,0x00000001,0x0004002b, +0x0000000a,0x0000000e,0x00000001,0x00020014, +0x0000000f,0x0004002b,0x00000006,0x00000011, +0x40400000,0x0004002b,0x00000006,0x00000012, +0xbf800000,0x0004002b,0x0000000a,0x00000015, +0x00000002,0x00040017,0x00000019,0x00000006, +0x00000004,0x00040015,0x0000001a,0x00000020, +0x00000000,0x0004002b,0x0000001a,0x0000001b, +0x00000001,0x0004001c,0x0000001c,0x00000006, +0x0000001b,0x0006001e,0x0000001d,0x00000019, +0x00000006,0x0000001c,0x0000001c,0x00040020, +0x0000001e,0x00000003,0x0000001d,0x0004003b, +0x0000001e,0x0000001f,0x00000003,0x0004002b, +0x0000000a,0x00000020,0x00000000,0x0004002b, +0x00000006,0x00000022,0x00000000,0x0004002b, +0x00000006,0x00000023,0x3f800000,0x00040020, +0x00000027,0x00000003,0x00000019,0x00040020, +0x00000029,0x00000003,0x00000007,0x0004003b, +0x00000029,0x0000002a,0x00000003,0x0004002b, +0x00000006,0x0000002e,0x3f000000,0x0005002c, +0x00000007,0x00000030,0x00000023,0x00000023, +0x00050036,0x00000002,0x00000004,0x00000000, +0x00000003,0x000200f8,0x00000005,0x0004003d, +0x0000000a,0x0000000d,0x0000000c,0x000500aa, +0x0000000f,0x00000010,0x0000000d,0x0000000e, +0x000600a9,0x00000006,0x00000013,0x00000010, +0x00000011,0x00000012,0x000500aa,0x0000000f, +0x00000016,0x0000000d,0x00000015,0x000600a9, +0x00000006,0x00000017,0x00000016,0x00000011, +0x00000012,0x00050050,0x00000007,0x00000018, +0x00000013,0x00000017,0x00070050,0x00000019, +0x00000026,0x00000013,0x00000017,0x00000022, +0x00000023,0x00050041,0x00000027,0x00000028, +0x0000001f,0x00000020,0x0003003e,0x00000028, +0x00000026,0x00050081,0x00000007,0x0000002d, +0x00000018,0x00000030,0x0005008e,0x00000007, +0x0000002f,0x0000002d,0x0000002e,0x0003003e, +0x0000002a,0x0000002f,0x000100fd,0x00010038} +; diff --git a/vrto3d/shaders/generated/repack_frag_spv.h b/vrto3d/shaders/generated/repack_frag_spv.h new file mode 100644 index 00000000..dc5ca718 --- /dev/null +++ b/vrto3d/shaders/generated/repack_frag_spv.h @@ -0,0 +1,1051 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +/* Generated by compile_shaders.sh from repack.frag — do not edit. */ +#pragma once + +#include + +static const uint32_t repack_frag_spv[] = +{0x07230203,0x00010000,0x000d000b,0x00000b45, +0x00000000,0x00020011,0x00000001,0x0006000b, +0x00000001,0x4c534c47,0x6474732e,0x3035342e, +0x00000000,0x0003000e,0x00000000,0x00000001, +0x0008000f,0x00000004,0x00000004,0x6e69616d, +0x00000000,0x000000b8,0x0000013c,0x00000502, +0x00030010,0x00000004,0x00000007,0x00030047, +0x0000001c,0x00000002,0x00050048,0x0000001c, +0x00000000,0x00000023,0x00000000,0x00050048, +0x0000001c,0x00000001,0x00000023,0x00000008, +0x00050048,0x0000001c,0x00000002,0x00000023, +0x00000010,0x00050048,0x0000001c,0x00000003, +0x00000023,0x00000014,0x00050048,0x0000001c, +0x00000004,0x00000023,0x00000018,0x00050048, +0x0000001c,0x00000005,0x00000023,0x0000001c, +0x00050048,0x0000001c,0x00000006,0x00000023, +0x00000020,0x00050048,0x0000001c,0x00000007, +0x00000023,0x00000030,0x00050048,0x0000001c, +0x00000008,0x00000023,0x00000040,0x00050048, +0x0000001c,0x00000009,0x00000023,0x00000050, +0x00040047,0x0000003a,0x00000021,0x00000000, +0x00040047,0x0000003a,0x00000022,0x00000000, +0x00040047,0x000000b8,0x0000001e,0x00000000, +0x00040047,0x0000013c,0x0000000b,0x0000000f, +0x00040047,0x00000502,0x0000001e,0x00000000, +0x00020013,0x00000002,0x00030021,0x00000003, +0x00000002,0x00040015,0x00000006,0x00000020, +0x00000001,0x00030016,0x00000008,0x00000020, +0x00040017,0x0000000a,0x00000008,0x00000004, +0x00040017,0x00000011,0x00000008,0x00000003, +0x00040017,0x0000001b,0x00000006,0x00000002, +0x000c001e,0x0000001c,0x0000001b,0x0000001b, +0x00000006,0x00000006,0x00000006,0x00000008, +0x0000000a,0x0000000a,0x0000000a,0x0000000a, +0x00040020,0x0000001d,0x00000009,0x0000001c, +0x0004003b,0x0000001d,0x0000001e,0x00000009, +0x0004002b,0x00000006,0x0000001f,0x00000003, +0x00040020,0x00000020,0x00000009,0x00000006, +0x0004002b,0x00000006,0x00000023,0x00000000, +0x00020014,0x00000024,0x0004002b,0x00000006, +0x00000029,0x00000001,0x0004002b,0x00000008, +0x00000030,0x3f000000,0x00090019,0x00000037, +0x00000008,0x00000001,0x00000000,0x00000000, +0x00000000,0x00000001,0x00000000,0x0003001b, +0x00000038,0x00000037,0x00040020,0x00000039, +0x00000000,0x00000038,0x0004003b,0x00000039, +0x0000003a,0x00000000,0x00040017,0x0000003e, +0x00000008,0x00000002,0x0004002b,0x00000006, +0x00000044,0x00000009,0x00040015,0x00000045, +0x00000020,0x00000000,0x0004002b,0x00000045, +0x00000046,0x00000000,0x00040020,0x00000047, +0x00000009,0x00000008,0x0004002b,0x00000045, +0x0000004b,0x00000001,0x0004002b,0x00000045, +0x0000004f,0x00000002,0x0004002b,0x00000006, +0x00000055,0x00000005,0x0004002b,0x00000008, +0x00000060,0x3f800000,0x0004002b,0x00000008, +0x0000006e,0x00000000,0x0004002b,0x00000008, +0x00000077,0x3fc00000,0x0004002b,0x00000006, +0x00000078,0x00000006,0x00040020,0x00000079, +0x00000009,0x0000000a,0x0004002b,0x00000006, +0x0000008c,0x00000008,0x0004002b,0x00000006, +0x00000094,0x00000007,0x0004002b,0x00000006, +0x000000a2,0x00000002,0x0004002b,0x00000006, +0x000000a8,0x0000000d,0x00040020,0x000000b7, +0x00000001,0x0000003e,0x0004003b,0x000000b7, +0x000000b8,0x00000001,0x00040020,0x000000b9, +0x00000001,0x00000008,0x0004002b,0x00000008, +0x000000c7,0x40000000,0x0004002b,0x00000006, +0x000000de,0x0000000a,0x0004002b,0x00000006, +0x000000e2,0x0000000b,0x0004002b,0x00000006, +0x000000e6,0x0000000c,0x0004002b,0x00000008, +0x000000f0,0x41f00000,0x0004002b,0x00000008, +0x000000f6,0x42340000,0x0007002c,0x0000000a, +0x00000134,0x0000006e,0x0000006e,0x0000006e, +0x00000060,0x00040020,0x0000013b,0x00000001, +0x0000000a,0x0004003b,0x0000013b,0x0000013c, +0x00000001,0x0004002b,0x00000006,0x00000163, +0x00000004,0x0004002b,0x00000008,0x00000184, +0x3e800000,0x0004002b,0x00000008,0x0000018b, +0x3f400000,0x0004002b,0x00000006,0x000001b4, +0x0000000e,0x0004002b,0x00000006,0x000001e3, +0x00000017,0x0004002b,0x00000006,0x00000204, +0x0000000f,0x0004002b,0x00000006,0x00000211, +0x00000010,0x0004002b,0x00000008,0x00000216, +0x3edfbe77,0x0004002b,0x00000008,0x0000021a, +0x3ee5e354,0x0004002b,0x00000008,0x0000021f, +0x3e27ef9e,0x0004002b,0x00000008,0x00000224, +0x3c343958,0x0004002b,0x00000008,0x00000229, +0x3d03126f,0x0004002b,0x00000008,0x0000022e, +0x3be56042,0x0004002b,0x00000008,0x00000235, +0xbd7df3b6,0x0004002b,0x00000008,0x00000239, +0x3d7df3b6,0x0004002b,0x00000008,0x0000023e, +0x3cc49ba6,0x0004002b,0x00000008,0x00000243, +0x3ec10625,0x0004002b,0x00000008,0x00000248, +0x3f42d0e5,0x0004002b,0x00000008,0x0000024d, +0x3c1374bc,0x0004002b,0x00000008,0x00000254, +0xbd449ba6,0x0004002b,0x00000008,0x00000258, +0x3d4ccccd,0x0004002b,0x00000008,0x0000025d, +0x3c8b4396,0x0004002b,0x00000008,0x00000262, +0x3cd4fdf4,0x0004002b,0x00000008,0x00000267, +0x3dbe76c9,0x0004002b,0x00000008,0x0000026c, +0x3f9df3b6,0x0004002b,0x00000006,0x00000278, +0x00000011,0x0004002b,0x00000008,0x0000027e, +0x3bc49ba6,0x0004002b,0x00000008,0x00000285, +0x3ee66666,0x0004002b,0x00000008,0x000002b3, +0x3f933333,0x0004002b,0x00000006,0x0000030f, +0x00000012,0x0004002b,0x00000008,0x00000314, +0x3ee0c49c,0x0004002b,0x00000008,0x00000315, +0x3ee4dd2f,0x0004002b,0x00000008,0x00000316, +0x3e178d50,0x0006002c,0x00000011,0x00000317, +0x00000314,0x00000315,0x00000316,0x0004002b, +0x00000008,0x0000031c,0x3dc28f5c,0x0004002b, +0x00000008,0x0000031d,0x3f6f1aa0,0x0004002b, +0x00000008,0x0000031e,0xbba3d70a,0x0006002c, +0x00000011,0x0000031f,0x0000031c,0x0000031d, +0x0000031e,0x0004002b,0x00000008,0x00000324, +0xbc9374bc,0x0004002b,0x00000008,0x00000325, +0xbce56042,0x0004002b,0x00000008,0x00000326, +0x3f874bc7,0x0006002c,0x00000011,0x00000327, +0x00000324,0x00000325,0x00000326,0x0004002b, +0x00000006,0x00000338,0x00000013,0x0004002b, +0x00000006,0x00000345,0x00000014,0x0004002b, +0x00000008,0x0000034d,0x3e21cac1,0x0004002b, +0x00000008,0x00000352,0x3d1fbe77,0x0004002b, +0x00000008,0x00000357,0x3f076c8b,0x0004002b, +0x00000008,0x0000035c,0x3f347ae1,0x0004002b, +0x00000008,0x00000367,0x3e916873,0x0004002b, +0x00000008,0x0000036b,0x3f2b020c,0x0004002b, +0x00000008,0x00000370,0x3e126e98,0x0004002b, +0x00000008,0x00000375,0x3c83126f,0x0004002b, +0x00000008,0x0000037a,0x3c75c28f,0x0004002b, +0x00000008,0x0000037f,0x3d851eb8,0x0004002b, +0x00000008,0x00000386,0xbc75c28f,0x0004002b, +0x00000008,0x0000038a,0x3cdd2f1b,0x0004002b, +0x00000008,0x0000038f,0x3cac0831,0x0004002b, +0x00000008,0x00000398,0x3d99999a,0x0004002b, +0x00000008,0x0000039d,0x3f6fdf3b,0x0004002b, +0x00000006,0x000003a9,0x00000015,0x0004002b, +0x00000008,0x000003b9,0x3f4ccccd,0x0004002b, +0x00000008,0x000003e5,0x3f866666,0x0004002b, +0x00000006,0x0000043f,0x00000016,0x0004002b, +0x00000008,0x00000474,0x3f8ccccd,0x00040020, +0x00000501,0x00000003,0x0000000a,0x0004003b, +0x00000501,0x00000502,0x00000003,0x0006002c, +0x00000011,0x00000b16,0x0000006e,0x0000006e, +0x0000006e,0x0006002c,0x00000011,0x00000b17, +0x00000060,0x00000060,0x00000060,0x0006002c, +0x00000011,0x00000b18,0x00000077,0x00000077, +0x00000077,0x0006002c,0x00000011,0x00000b19, +0x00000030,0x00000030,0x00000030,0x0004002b, +0x00000008,0x00000b1b,0x3e8ccccd,0x0007002c, +0x0000000a,0x00000b1c,0x00000285,0x00000b1b, +0x00000b1b,0x00000060,0x0007002c,0x0000000a, +0x00000b1d,0x0000006e,0x0000006e,0x0000006e, +0x0000006e,0x0007002c,0x0000000a,0x00000b1e, +0x00000060,0x00000060,0x00000060,0x00000060, +0x0007002c,0x0000000a,0x00000b1f,0x0000006e, +0x00000060,0x0000006e,0x00000060,0x0007002c, +0x0000000a,0x00000b20,0x0000006e,0x0000006e, +0x00000060,0x00000060,0x0004002b,0x00000008, +0x00000b21,0xbb449ba6,0x0004002b,0x00000008, +0x00000b22,0xbac49ba6,0x0004002b,0x00000008, +0x00000b23,0x3b449ba6,0x0004002b,0x00000008, +0x00000b24,0x3e4ccccc,0x0007002c,0x0000000a, +0x00000b25,0x000003b9,0x00000b24,0x0000006e, +0x00000060,0x0007002c,0x0000000a,0x00000b26, +0x00000b1b,0x00000285,0x00000b1b,0x00000060, +0x0007002c,0x0000000a,0x00000b27,0x0000006e, +0x00000b24,0x000003b9,0x00000060,0x0004002b, +0x00000008,0x00000b28,0x3c072b02,0x0004002b, +0x00000008,0x00000b29,0xbb872b02,0x0004002b, +0x00000008,0x00000b2a,0xbc072b02,0x0004002b, +0x00000008,0x00000b2b,0x3b872b02,0x0007002c, +0x0000000a,0x00000b2c,0x00000060,0x0000006e, +0x0000006e,0x00000060,0x0007002c,0x0000000a, +0x00000b2d,0x00000b1b,0x00000b1b,0x00000285, +0x00000060,0x0004002b,0x00000008,0x00000b2f, +0x3f80511a,0x0004002b,0x00000008,0x00000b30, +0x3ccac083,0x0004002b,0x00000008,0x00000b31, +0xbc4ac083,0x0004002b,0x00000008,0x00000b32, +0xbccac083,0x0004002b,0x00000008,0x00000b33, +0x3d4ac083,0x00050036,0x00000002,0x00000004, +0x00000000,0x00000003,0x000200f8,0x00000005, +0x000300f7,0x000008f4,0x00000000,0x000300fb, +0x00000046,0x0000056a,0x000200f8,0x0000056a, +0x00050041,0x00000020,0x0000056b,0x0000001e, +0x000000a2,0x0004003d,0x00000006,0x0000056c, +0x0000056b,0x000500aa,0x00000024,0x0000056e, +0x0000056c,0x00000023,0x000500aa,0x00000024, +0x00000570,0x0000056c,0x000000a8,0x000500a6, +0x00000024,0x00000571,0x0000056e,0x00000570, +0x000500aa,0x00000024,0x00000573,0x0000056c, +0x00000055,0x000500a6,0x00000024,0x00000574, +0x00000571,0x00000573,0x000500aa,0x00000024, +0x00000576,0x0000056c,0x00000078,0x000500a6, +0x00000024,0x00000577,0x00000574,0x00000576, +0x000500aa,0x00000024,0x00000579,0x0000056c, +0x00000094,0x000500a6,0x00000024,0x0000057a, +0x00000577,0x00000579,0x000300f7,0x00000593, +0x00000000,0x000400fa,0x0000057a,0x0000057b, +0x00000593,0x000200f8,0x0000057b,0x00050041, +0x000000b9,0x0000057c,0x000000b8,0x00000046, +0x0004003d,0x00000008,0x0000057d,0x0000057c, +0x000500b8,0x00000024,0x0000057e,0x0000057d, +0x00000030,0x000600a9,0x00000006,0x0000057f, +0x0000057e,0x00000023,0x00000029,0x000300f7, +0x0000058c,0x00000000,0x000400fa,0x0000057e, +0x00000583,0x00000587,0x000200f8,0x00000587, +0x00050083,0x00000008,0x0000058a,0x0000057d, +0x00000030,0x00050085,0x00000008,0x0000058b, +0x0000058a,0x000000c7,0x000200f9,0x0000058c, +0x000200f8,0x00000583,0x00050085,0x00000008, +0x00000586,0x0000057d,0x000000c7,0x000200f9, +0x0000058c,0x000200f8,0x0000058c,0x000700f5, +0x00000008,0x00000b10,0x00000586,0x00000583, +0x0000058b,0x00000587,0x00050041,0x000000b9, +0x00000590,0x000000b8,0x0000004b,0x0004003d, +0x00000008,0x00000591,0x00000590,0x00050041, +0x00000020,0x000008fb,0x0000001e,0x0000001f, +0x0004003d,0x00000006,0x000008fc,0x000008fb, +0x000500ab,0x00000024,0x000008fd,0x000008fc, +0x00000023,0x000300f7,0x00000903,0x00000000, +0x000400fa,0x000008fd,0x000008fe,0x00000901, +0x000200f8,0x00000901,0x000200f9,0x00000903, +0x000200f8,0x000008fe,0x00050082,0x00000006, +0x00000900,0x00000029,0x0000057f,0x000200f9, +0x00000903,0x000200f8,0x00000903,0x000700f5, +0x00000006,0x00000b11,0x00000900,0x000008fe, +0x0000057f,0x00000901,0x0004006f,0x00000008, +0x00000908,0x00000b11,0x00050081,0x00000008, +0x00000b3e,0x00000b10,0x00000908,0x00050085, +0x00000008,0x0000090a,0x00000030,0x00000b3e, +0x0004003d,0x00000038,0x0000090b,0x0000003a, +0x00050050,0x0000003e,0x0000090e,0x0000090a, +0x00000591,0x00050057,0x0000000a,0x0000090f, +0x0000090b,0x0000090e,0x000200f9,0x000008f4, +0x000200f8,0x00000593,0x000500aa,0x00000024, +0x00000595,0x0000056c,0x00000029,0x000500aa, +0x00000024,0x00000597,0x0000056c,0x00000044, +0x000500a6,0x00000024,0x00000598,0x00000595, +0x00000597,0x000500aa,0x00000024,0x0000059a, +0x0000056c,0x000000de,0x000500a6,0x00000024, +0x0000059b,0x00000598,0x0000059a,0x000500aa, +0x00000024,0x0000059d,0x0000056c,0x000000e2, +0x000500a6,0x00000024,0x0000059e,0x0000059b, +0x0000059d,0x000500aa,0x00000024,0x000005a0, +0x0000056c,0x000000e6,0x000500a6,0x00000024, +0x000005a1,0x0000059e,0x000005a0,0x000300f7, +0x000005dd,0x00000000,0x000400fa,0x000005a1, +0x000005a2,0x000005dd,0x000200f8,0x000005a2, +0x000300f7,0x000005ab,0x00000000,0x000400fa, +0x00000597,0x000005a5,0x000005a6,0x000200f8, +0x000005a6,0x000500ab,0x00000024,0x000005a8, +0x0000056c,0x00000029,0x000600a9,0x00000008, +0x00000b3f,0x000005a8,0x000000f6,0x0000006e, +0x000200f9,0x000005ab,0x000200f8,0x000005a5, +0x000200f9,0x000005ab,0x000200f8,0x000005ab, +0x000700f5,0x00000008,0x00000b0b,0x000000f0, +0x000005a5,0x00000b3f,0x000005a6,0x000500ba, +0x00000024,0x000005ad,0x00000b0b,0x0000006e, +0x000300f7,0x000005b5,0x00000000,0x000400fa, +0x000005ad,0x000005ae,0x000005b4,0x000200f8, +0x000005b4,0x000200f9,0x000005b5,0x000200f8, +0x000005ae,0x00060041,0x00000020,0x000005b0, +0x0000001e,0x00000023,0x0000004b,0x0004003d, +0x00000006,0x000005b1,0x000005b0,0x0004006f, +0x00000008,0x000005b2,0x000005b1,0x00050088, +0x00000008,0x000005b3,0x00000b0b,0x000005b2, +0x000200f9,0x000005b5,0x000200f8,0x000005b5, +0x000700f5,0x00000008,0x00000b0d,0x000005b3, +0x000005ae,0x0000006e,0x000005b4,0x00050083, +0x00000008,0x000005b8,0x00000060,0x00000b0d, +0x00050085,0x00000008,0x000005b9,0x000005b8, +0x00000030,0x00050041,0x000000b9,0x000005ba, +0x000000b8,0x0000004b,0x0004003d,0x00000008, +0x000005bb,0x000005ba,0x000500b8,0x00000024, +0x000005bd,0x000005bb,0x000005b9,0x000300f7, +0x000005dc,0x00000000,0x000400fa,0x000005bd, +0x000005be,0x000005c6,0x000200f8,0x000005c6, +0x00050081,0x00000008,0x000005cb,0x000005b9, +0x00000b0d,0x000500be,0x00000024,0x000005cc, +0x000005bb,0x000005cb,0x000300f7,0x000005db, +0x00000000,0x000400fa,0x000005cc,0x000005cd, +0x000005da,0x000200f8,0x000005da,0x000200f9, +0x000008f4,0x000200f8,0x000005cd,0x00050083, +0x00000008,0x000005d3,0x000005bb,0x000005cb, +0x00050088,0x00000008,0x000005d5,0x000005d3, +0x000005b9,0x00050041,0x000000b9,0x000005d6, +0x000000b8,0x00000046,0x0004003d,0x00000008, +0x000005d7,0x000005d6,0x00050041,0x00000020, +0x0000092f,0x0000001e,0x0000001f,0x0004003d, +0x00000006,0x00000930,0x0000092f,0x000500ab, +0x00000024,0x00000931,0x00000930,0x00000023, +0x000600a9,0x00000006,0x00000b40,0x00000931, +0x00000023,0x00000029,0x0004006f,0x00000008, +0x0000093c,0x00000b40,0x00050081,0x00000008, +0x00000b3c,0x000005d7,0x0000093c,0x00050085, +0x00000008,0x0000093e,0x00000030,0x00000b3c, +0x0004003d,0x00000038,0x0000093f,0x0000003a, +0x00050050,0x0000003e,0x00000942,0x0000093e, +0x000005d5,0x00050057,0x0000000a,0x00000943, +0x0000093f,0x00000942,0x000200f9,0x000008f4, +0x000200f8,0x000005db,0x000100ff,0x000200f8, +0x000005be,0x00050088,0x00000008,0x000005c2, +0x000005bb,0x000005b9,0x00050041,0x000000b9, +0x000005c3,0x000000b8,0x00000046,0x0004003d, +0x00000008,0x000005c4,0x000005c3,0x00050041, +0x00000020,0x00000915,0x0000001e,0x0000001f, +0x0004003d,0x00000006,0x00000916,0x00000915, +0x000500ab,0x00000024,0x00000917,0x00000916, +0x00000023,0x000600a9,0x00000006,0x00000b41, +0x00000917,0x00000029,0x00000023,0x0004006f, +0x00000008,0x00000922,0x00000b41,0x00050081, +0x00000008,0x00000b3d,0x000005c4,0x00000922, +0x00050085,0x00000008,0x00000924,0x00000030, +0x00000b3d,0x0004003d,0x00000038,0x00000925, +0x0000003a,0x00050050,0x0000003e,0x00000928, +0x00000924,0x000005c2,0x00050057,0x0000000a, +0x00000929,0x00000925,0x00000928,0x000200f9, +0x000008f4,0x000200f8,0x000005dc,0x000100ff, +0x000200f8,0x000005dd,0x000500aa,0x00000024, +0x000005df,0x0000056c,0x000000a2,0x000300f7, +0x000005ec,0x00000000,0x000400fa,0x000005df, +0x000005e0,0x000005ec,0x000200f8,0x000005e0, +0x00050041,0x000000b9,0x000005e1,0x0000013c, +0x0000004b,0x0004003d,0x00000008,0x000005e2, +0x000005e1,0x0004006e,0x00000006,0x000005e3, +0x000005e2,0x000500c7,0x00000006,0x000005e5, +0x000005e3,0x00000029,0x00050041,0x000000b9, +0x000005e7,0x000000b8,0x00000046,0x0004003d, +0x00000008,0x000005e8,0x000005e7,0x00050041, +0x000000b9,0x000005e9,0x000000b8,0x0000004b, +0x0004003d,0x00000008,0x000005ea,0x000005e9, +0x00050041,0x00000020,0x00000949,0x0000001e, +0x0000001f,0x0004003d,0x00000006,0x0000094a, +0x00000949,0x000500ab,0x00000024,0x0000094b, +0x0000094a,0x00000023,0x000300f7,0x00000951, +0x00000000,0x000400fa,0x0000094b,0x0000094c, +0x0000094f,0x000200f8,0x0000094f,0x000200f9, +0x00000951,0x000200f8,0x0000094c,0x00050082, +0x00000006,0x0000094e,0x00000029,0x000005e5, +0x000200f9,0x00000951,0x000200f8,0x00000951, +0x000700f5,0x00000006,0x00000b0a,0x0000094e, +0x0000094c,0x000005e5,0x0000094f,0x0004006f, +0x00000008,0x00000956,0x00000b0a,0x00050081, +0x00000008,0x00000b3b,0x000005e8,0x00000956, +0x00050085,0x00000008,0x00000958,0x00000030, +0x00000b3b,0x0004003d,0x00000038,0x00000959, +0x0000003a,0x00050050,0x0000003e,0x0000095c, +0x00000958,0x000005ea,0x00050057,0x0000000a, +0x0000095d,0x00000959,0x0000095c,0x000200f9, +0x000008f4,0x000200f8,0x000005ec,0x000500aa, +0x00000024,0x000005ee,0x0000056c,0x0000001f, +0x000300f7,0x000005fb,0x00000000,0x000400fa, +0x000005ee,0x000005ef,0x000005fb,0x000200f8, +0x000005ef,0x00050041,0x000000b9,0x000005f0, +0x0000013c,0x00000046,0x0004003d,0x00000008, +0x000005f1,0x000005f0,0x0004006e,0x00000006, +0x000005f2,0x000005f1,0x000500c7,0x00000006, +0x000005f4,0x000005f2,0x00000029,0x00050041, +0x000000b9,0x000005f6,0x000000b8,0x00000046, +0x0004003d,0x00000008,0x000005f7,0x000005f6, +0x00050041,0x000000b9,0x000005f8,0x000000b8, +0x0000004b,0x0004003d,0x00000008,0x000005f9, +0x000005f8,0x00050041,0x00000020,0x00000963, +0x0000001e,0x0000001f,0x0004003d,0x00000006, +0x00000964,0x00000963,0x000500ab,0x00000024, +0x00000965,0x00000964,0x00000023,0x000300f7, +0x0000096b,0x00000000,0x000400fa,0x00000965, +0x00000966,0x00000969,0x000200f8,0x00000969, +0x000200f9,0x0000096b,0x000200f8,0x00000966, +0x00050082,0x00000006,0x00000968,0x00000029, +0x000005f4,0x000200f9,0x0000096b,0x000200f8, +0x0000096b,0x000700f5,0x00000006,0x00000b09, +0x00000968,0x00000966,0x000005f4,0x00000969, +0x0004006f,0x00000008,0x00000970,0x00000b09, +0x00050081,0x00000008,0x00000b3a,0x000005f7, +0x00000970,0x00050085,0x00000008,0x00000972, +0x00000030,0x00000b3a,0x0004003d,0x00000038, +0x00000973,0x0000003a,0x00050050,0x0000003e, +0x00000976,0x00000972,0x000005f9,0x00050057, +0x0000000a,0x00000977,0x00000973,0x00000976, +0x000200f9,0x000008f4,0x000200f8,0x000005fb, +0x000500aa,0x00000024,0x000005fd,0x0000056c, +0x00000163,0x000300f7,0x0000060f,0x00000000, +0x000400fa,0x000005fd,0x000005fe,0x0000060f, +0x000200f8,0x000005fe,0x00050041,0x000000b9, +0x000005ff,0x0000013c,0x0000004b,0x0004003d, +0x00000008,0x00000600,0x000005ff,0x0004006e, +0x00000006,0x00000601,0x00000600,0x00050041, +0x000000b9,0x00000602,0x0000013c,0x00000046, +0x0004003d,0x00000008,0x00000603,0x00000602, +0x0004006e,0x00000006,0x00000604,0x00000603, +0x00050080,0x00000006,0x00000607,0x00000601, +0x00000604,0x000500c7,0x00000006,0x00000608, +0x00000607,0x00000029,0x00050041,0x000000b9, +0x0000060a,0x000000b8,0x00000046,0x0004003d, +0x00000008,0x0000060b,0x0000060a,0x00050041, +0x000000b9,0x0000060c,0x000000b8,0x0000004b, +0x0004003d,0x00000008,0x0000060d,0x0000060c, +0x00050041,0x00000020,0x0000097d,0x0000001e, +0x0000001f,0x0004003d,0x00000006,0x0000097e, +0x0000097d,0x000500ab,0x00000024,0x0000097f, +0x0000097e,0x00000023,0x000300f7,0x00000985, +0x00000000,0x000400fa,0x0000097f,0x00000980, +0x00000983,0x000200f8,0x00000983,0x000200f9, +0x00000985,0x000200f8,0x00000980,0x00050082, +0x00000006,0x00000982,0x00000029,0x00000608, +0x000200f9,0x00000985,0x000200f8,0x00000985, +0x000700f5,0x00000006,0x00000b08,0x00000982, +0x00000980,0x00000608,0x00000983,0x0004006f, +0x00000008,0x0000098a,0x00000b08,0x00050081, +0x00000008,0x00000b39,0x0000060b,0x0000098a, +0x00050085,0x00000008,0x0000098c,0x00000030, +0x00000b39,0x0004003d,0x00000038,0x0000098d, +0x0000003a,0x00050050,0x0000003e,0x00000990, +0x0000098c,0x0000060d,0x00050057,0x0000000a, +0x00000991,0x0000098d,0x00000990,0x000200f9, +0x000008f4,0x000200f8,0x0000060f,0x000500aa, +0x00000024,0x00000611,0x0000056c,0x0000008c, +0x000300f7,0x00000639,0x00000000,0x000400fa, +0x00000611,0x00000612,0x00000639,0x000200f8, +0x00000612,0x00050041,0x000000b9,0x00000613, +0x000000b8,0x0000004b,0x0004003d,0x00000008, +0x00000614,0x00000613,0x000500b8,0x00000024, +0x00000615,0x00000614,0x00000184,0x000400a8, +0x00000024,0x00000616,0x00000615,0x000300f7, +0x0000061b,0x00000000,0x000400fa,0x00000616, +0x00000617,0x0000061b,0x000200f8,0x00000617, +0x000500be,0x00000024,0x0000061a,0x00000614, +0x0000018b,0x000200f9,0x0000061b,0x000200f8, +0x0000061b,0x000700f5,0x00000024,0x0000061c, +0x00000615,0x00000612,0x0000061a,0x00000617, +0x000300f7,0x0000061e,0x00000000,0x000400fa, +0x0000061c,0x0000061d,0x0000061e,0x000200f8, +0x0000061d,0x000200f9,0x000008f4,0x000200f8, +0x0000061e,0x00050083,0x00000008,0x00000621, +0x00000614,0x00000184,0x00050085,0x00000008, +0x00000622,0x00000621,0x000000c7,0x00050041, +0x000000b9,0x00000623,0x000000b8,0x00000046, +0x0004003d,0x00000008,0x00000624,0x00000623, +0x000500b8,0x00000024,0x00000625,0x00000624, +0x00000030,0x000600a9,0x00000006,0x00000626, +0x00000625,0x00000023,0x00000029,0x000300f7, +0x00000633,0x00000000,0x000400fa,0x00000625, +0x0000062a,0x0000062e,0x000200f8,0x0000062e, +0x00050083,0x00000008,0x00000631,0x00000624, +0x00000030,0x00050085,0x00000008,0x00000632, +0x00000631,0x000000c7,0x000200f9,0x00000633, +0x000200f8,0x0000062a,0x00050085,0x00000008, +0x0000062d,0x00000624,0x000000c7,0x000200f9, +0x00000633,0x000200f8,0x00000633,0x000700f5, +0x00000008,0x00000b06,0x0000062d,0x0000062a, +0x00000632,0x0000062e,0x00050041,0x00000020, +0x00000997,0x0000001e,0x0000001f,0x0004003d, +0x00000006,0x00000998,0x00000997,0x000500ab, +0x00000024,0x00000999,0x00000998,0x00000023, +0x000300f7,0x0000099f,0x00000000,0x000400fa, +0x00000999,0x0000099a,0x0000099d,0x000200f8, +0x0000099d,0x000200f9,0x0000099f,0x000200f8, +0x0000099a,0x00050082,0x00000006,0x0000099c, +0x00000029,0x00000626,0x000200f9,0x0000099f, +0x000200f8,0x0000099f,0x000700f5,0x00000006, +0x00000b07,0x0000099c,0x0000099a,0x00000626, +0x0000099d,0x0004006f,0x00000008,0x000009a4, +0x00000b07,0x00050081,0x00000008,0x00000b38, +0x00000b06,0x000009a4,0x00050085,0x00000008, +0x000009a6,0x00000030,0x00000b38,0x0004003d, +0x00000038,0x000009a7,0x0000003a,0x00050050, +0x0000003e,0x000009aa,0x000009a6,0x00000622, +0x00050057,0x0000000a,0x000009ab,0x000009a7, +0x000009aa,0x000200f9,0x000008f4,0x000200f8, +0x00000639,0x000500aa,0x00000024,0x0000063b, +0x0000056c,0x000001b4,0x000300f7,0x0000065e, +0x00000000,0x000400fa,0x0000063b,0x0000063c, +0x0000065e,0x000200f8,0x0000063c,0x00050041, +0x000000b9,0x0000063d,0x000000b8,0x00000046, +0x0004003d,0x00000008,0x0000063e,0x0000063d, +0x000500b8,0x00000024,0x0000063f,0x0000063e, +0x00000030,0x000600a9,0x00000006,0x00000640, +0x0000063f,0x00000023,0x00000029,0x000300f7, +0x0000064d,0x00000000,0x000400fa,0x0000063f, +0x00000644,0x00000648,0x000200f8,0x00000648, +0x00050083,0x00000008,0x0000064b,0x0000063e, +0x00000030,0x00050085,0x00000008,0x0000064c, +0x0000064b,0x000000c7,0x000200f9,0x0000064d, +0x000200f8,0x00000644,0x00050085,0x00000008, +0x00000647,0x0000063e,0x000000c7,0x000200f9, +0x0000064d,0x000200f8,0x0000064d,0x000700f5, +0x00000008,0x00000b03,0x00000647,0x00000644, +0x0000064c,0x00000648,0x000500aa,0x00000024, +0x00000650,0x00000640,0x00000023,0x000300f7, +0x00000658,0x00000000,0x000400fa,0x00000650, +0x00000651,0x00000655,0x000200f8,0x00000655, +0x00050041,0x000000b9,0x00000656,0x000000b8, +0x0000004b,0x0004003d,0x00000008,0x00000657, +0x00000656,0x000200f9,0x00000658,0x000200f8, +0x00000651,0x00050041,0x000000b9,0x00000652, +0x000000b8,0x0000004b,0x0004003d,0x00000008, +0x00000653,0x00000652,0x00050083,0x00000008, +0x00000654,0x00000060,0x00000653,0x000200f9, +0x00000658,0x000200f8,0x00000658,0x000700f5, +0x00000008,0x00000b04,0x00000654,0x00000651, +0x00000657,0x00000655,0x00050041,0x00000020, +0x000009b1,0x0000001e,0x0000001f,0x0004003d, +0x00000006,0x000009b2,0x000009b1,0x000500ab, +0x00000024,0x000009b3,0x000009b2,0x00000023, +0x000300f7,0x000009b9,0x00000000,0x000400fa, +0x000009b3,0x000009b4,0x000009b7,0x000200f8, +0x000009b7,0x000200f9,0x000009b9,0x000200f8, +0x000009b4,0x00050082,0x00000006,0x000009b6, +0x00000029,0x00000640,0x000200f9,0x000009b9, +0x000200f8,0x000009b9,0x000700f5,0x00000006, +0x00000b05,0x000009b6,0x000009b4,0x00000640, +0x000009b7,0x0004006f,0x00000008,0x000009be, +0x00000b05,0x00050081,0x00000008,0x00000b37, +0x00000b03,0x000009be,0x00050085,0x00000008, +0x000009c0,0x00000030,0x00000b37,0x0004003d, +0x00000038,0x000009c1,0x0000003a,0x00050050, +0x0000003e,0x000009c4,0x000009c0,0x00000b04, +0x00050057,0x0000000a,0x000009c5,0x000009c1, +0x000009c4,0x000200f9,0x000008f4,0x000200f8, +0x0000065e,0x000500aa,0x00000024,0x00000660, +0x0000056c,0x000001e3,0x000300f7,0x00000667, +0x00000000,0x000400fa,0x00000660,0x00000661, +0x00000667,0x000200f8,0x00000661,0x00050041, +0x000000b9,0x00000662,0x000000b8,0x00000046, +0x0004003d,0x00000008,0x00000663,0x00000662, +0x00050041,0x000000b9,0x00000664,0x000000b8, +0x0000004b,0x0004003d,0x00000008,0x00000665, +0x00000664,0x00050041,0x00000020,0x000009cb, +0x0000001e,0x0000001f,0x0004003d,0x00000006, +0x000009cc,0x000009cb,0x000500ab,0x00000024, +0x000009cd,0x000009cc,0x00000023,0x000600a9, +0x00000006,0x00000b42,0x000009cd,0x00000029, +0x00000023,0x0004006f,0x00000008,0x000009d8, +0x00000b42,0x00050081,0x00000008,0x00000b36, +0x00000663,0x000009d8,0x00050085,0x00000008, +0x000009da,0x00000030,0x00000b36,0x0004003d, +0x00000038,0x000009db,0x0000003a,0x00050050, +0x0000003e,0x000009de,0x000009da,0x00000665, +0x00050057,0x0000000a,0x000009df,0x000009db, +0x000009de,0x000200f9,0x000008f4,0x000200f8, +0x00000667,0x00050041,0x000000b9,0x00000668, +0x000000b8,0x00000046,0x0004003d,0x00000008, +0x00000669,0x00000668,0x00050041,0x000000b9, +0x0000066a,0x000000b8,0x0000004b,0x0004003d, +0x00000008,0x0000066b,0x0000066a,0x00050041, +0x00000020,0x000009e5,0x0000001e,0x0000001f, +0x0004003d,0x00000006,0x000009e6,0x000009e5, +0x000500ab,0x00000024,0x000009e7,0x000009e6, +0x00000023,0x000600a9,0x00000006,0x00000b43, +0x000009e7,0x00000029,0x00000023,0x00050085, +0x00000008,0x000009f0,0x00000030,0x00000669, +0x0004006f,0x00000008,0x000009f2,0x00000b43, +0x00050085,0x00000008,0x000009f3,0x00000030, +0x000009f2,0x00050081,0x00000008,0x000009f4, +0x000009f0,0x000009f3,0x0004003d,0x00000038, +0x000009f5,0x0000003a,0x00050050,0x0000003e, +0x000009f8,0x000009f4,0x0000066b,0x00050057, +0x0000000a,0x000009f9,0x000009f5,0x000009f8, +0x000600a9,0x00000006,0x00000b44,0x000009e7, +0x00000023,0x00000029,0x0004006f,0x00000008, +0x00000a0c,0x00000b44,0x00050085,0x00000008, +0x00000a0d,0x00000030,0x00000a0c,0x00050081, +0x00000008,0x00000a0e,0x000009f0,0x00000a0d, +0x0004003d,0x00000038,0x00000a0f,0x0000003a, +0x00050050,0x0000003e,0x00000a12,0x00000a0e, +0x0000066b,0x00050057,0x0000000a,0x00000a13, +0x00000a0f,0x00000a12,0x000500aa,0x00000024, +0x00000673,0x0000056c,0x00000204,0x000300f7, +0x0000067c,0x00000000,0x000400fa,0x00000673, +0x00000674,0x0000067c,0x000200f8,0x00000674, +0x00050051,0x00000008,0x00000676,0x000009f9, +0x00000000,0x00050051,0x00000008,0x00000678, +0x00000a13,0x00000001,0x00050051,0x00000008, +0x0000067a,0x00000a13,0x00000002,0x00070050, +0x0000000a,0x0000067b,0x00000676,0x00000678, +0x0000067a,0x00000060,0x000200f9,0x000008f4, +0x000200f8,0x0000067c,0x000500aa,0x00000024, +0x0000067e,0x0000056c,0x00000211,0x000300f7, +0x000006cc,0x00000000,0x000400fa,0x0000067e, +0x0000067f,0x000006cc,0x000200f8,0x0000067f, +0x00050051,0x00000008,0x00000681,0x000009f9, +0x00000000,0x00050085,0x00000008,0x00000682, +0x00000216,0x00000681,0x00050051,0x00000008, +0x00000684,0x000009f9,0x00000001,0x00050085, +0x00000008,0x00000685,0x0000021a,0x00000684, +0x00050081,0x00000008,0x00000686,0x00000682, +0x00000685,0x00050051,0x00000008,0x00000688, +0x000009f9,0x00000002,0x00050085,0x00000008, +0x00000689,0x0000021f,0x00000688,0x00050081, +0x00000008,0x0000068a,0x00000686,0x00000689, +0x00050051,0x00000008,0x0000068c,0x00000a13, +0x00000000,0x00050085,0x00000008,0x0000068d, +0x00000224,0x0000068c,0x00050083,0x00000008, +0x0000068e,0x0000068a,0x0000068d,0x00050051, +0x00000008,0x00000690,0x00000a13,0x00000001, +0x00050085,0x00000008,0x00000691,0x00000229, +0x00000690,0x00050083,0x00000008,0x00000692, +0x0000068e,0x00000691,0x00050051,0x00000008, +0x00000694,0x00000a13,0x00000002,0x00050085, +0x00000008,0x00000695,0x0000022e,0x00000694, +0x00050083,0x00000008,0x00000696,0x00000692, +0x00000695,0x0008000c,0x00000008,0x00000697, +0x00000001,0x0000002b,0x00000696,0x0000006e, +0x00000060,0x00050085,0x00000008,0x0000069a, +0x00000235,0x00000681,0x00050085,0x00000008, +0x0000069d,0x00000239,0x00000684,0x00050083, +0x00000008,0x0000069e,0x0000069a,0x0000069d, +0x00050085,0x00000008,0x000006a1,0x0000023e, +0x00000688,0x00050083,0x00000008,0x000006a2, +0x0000069e,0x000006a1,0x00050085,0x00000008, +0x000006a5,0x00000243,0x0000068c,0x00050081, +0x00000008,0x000006a6,0x000006a2,0x000006a5, +0x00050085,0x00000008,0x000006a9,0x00000248, +0x00000690,0x00050081,0x00000008,0x000006aa, +0x000006a6,0x000006a9,0x00050085,0x00000008, +0x000006ad,0x0000024d,0x00000694,0x00050081, +0x00000008,0x000006ae,0x000006aa,0x000006ad, +0x0008000c,0x00000008,0x000006af,0x00000001, +0x0000002b,0x000006ae,0x0000006e,0x00000060, +0x00050085,0x00000008,0x000006b2,0x00000254, +0x00000681,0x00050085,0x00000008,0x000006b5, +0x00000258,0x00000684,0x00050083,0x00000008, +0x000006b6,0x000006b2,0x000006b5,0x00050085, +0x00000008,0x000006b9,0x0000025d,0x00000688, +0x00050083,0x00000008,0x000006ba,0x000006b6, +0x000006b9,0x00050085,0x00000008,0x000006bd, +0x00000262,0x0000068c,0x00050083,0x00000008, +0x000006be,0x000006ba,0x000006bd,0x00050085, +0x00000008,0x000006c1,0x00000267,0x00000690, +0x00050083,0x00000008,0x000006c2,0x000006be, +0x000006c1,0x00050085,0x00000008,0x000006c5, +0x0000026c,0x00000694,0x00050081,0x00000008, +0x000006c6,0x000006c2,0x000006c5,0x0008000c, +0x00000008,0x000006c7,0x00000001,0x0000002b, +0x000006c6,0x0000006e,0x00000060,0x00070050, +0x0000000a,0x000006cb,0x00000697,0x000006af, +0x000006c7,0x00000060,0x000200f9,0x000008f4, +0x000200f8,0x000006cc,0x000500aa,0x00000024, +0x000006ce,0x0000056c,0x00000278,0x000300f7, +0x00000754,0x00000000,0x000400fa,0x000006ce, +0x000006cf,0x00000754,0x000200f8,0x000006cf, +0x00050085,0x0000000a,0x000006df,0x000009f9, +0x00000b1c,0x0008000c,0x0000000a,0x000006e2, +0x00000001,0x0000002b,0x000006df,0x00000b1d, +0x00000b1e,0x00050051,0x00000008,0x000006e4, +0x000006e2,0x00000000,0x00050051,0x00000008, +0x000006e6,0x000006e2,0x00000001,0x00050081, +0x00000008,0x000006e7,0x000006e4,0x000006e6, +0x00050051,0x00000008,0x000006e9,0x000006e2, +0x00000002,0x00050081,0x00000008,0x000006ea, +0x000006e7,0x000006e9,0x0007000c,0x00000008, +0x000006eb,0x00000001,0x0000001a,0x000006ea, +0x00000060,0x00050085,0x0000000a,0x000006f2, +0x00000a13,0x00000b1f,0x0008000c,0x0000000a, +0x000006f5,0x00000001,0x0000002b,0x000006f2, +0x00000b1d,0x00000b1e,0x00050051,0x00000008, +0x000006f7,0x000006f5,0x00000000,0x00050051, +0x00000008,0x000006f9,0x000006f5,0x00000001, +0x00050081,0x00000008,0x000006fa,0x000006f7, +0x000006f9,0x00050051,0x00000008,0x000006fc, +0x000006f5,0x00000002,0x00050081,0x00000008, +0x000006fd,0x000006fa,0x000006fc,0x0007000c, +0x00000008,0x000006fe,0x00000001,0x0000001a, +0x000006fd,0x000002b3,0x00050085,0x0000000a, +0x00000705,0x00000a13,0x00000b20,0x0008000c, +0x0000000a,0x00000708,0x00000001,0x0000002b, +0x00000705,0x00000b1d,0x00000b1e,0x00050051, +0x00000008,0x0000070a,0x00000708,0x00000000, +0x00050051,0x00000008,0x0000070c,0x00000708, +0x00000001,0x00050081,0x00000008,0x0000070d, +0x0000070a,0x0000070c,0x00050051,0x00000008, +0x0000070f,0x00000708,0x00000002,0x00050081, +0x00000008,0x00000710,0x0000070d,0x0000070f, +0x0007000c,0x00000008,0x00000711,0x00000001, +0x0000001a,0x00000710,0x000002b3,0x00050085, +0x00000008,0x00000719,0x000006eb,0x0000027e, +0x00050081,0x00000008,0x0000071a,0x000006eb, +0x00000719,0x00050085,0x00000008,0x0000071f, +0x000006fe,0x00000b21,0x00050081,0x00000008, +0x00000720,0x0000071a,0x0000071f,0x00050085, +0x00000008,0x00000725,0x00000711,0x00000b21, +0x00050081,0x00000008,0x00000726,0x00000720, +0x00000725,0x00050085,0x00000008,0x0000072e, +0x000006eb,0x00000b22,0x00050081,0x00000008, +0x0000072f,0x000006fe,0x0000072e,0x00050085, +0x00000008,0x00000734,0x000006fe,0x00000b23, +0x00050081,0x00000008,0x00000735,0x0000072f, +0x00000734,0x00050085,0x00000008,0x0000073a, +0x00000711,0x00000b22,0x00050081,0x00000008, +0x0000073b,0x00000735,0x0000073a,0x00050081, +0x00000008,0x00000744,0x00000711,0x0000072e, +0x00050085,0x00000008,0x00000749,0x000006fe, +0x00000b22,0x00050081,0x00000008,0x0000074a, +0x00000744,0x00000749,0x00050085,0x00000008, +0x0000074f,0x00000711,0x00000b23,0x00050081, +0x00000008,0x00000750,0x0000074a,0x0000074f, +0x00070050,0x0000000a,0x00000b35,0x00000726, +0x0000073b,0x00000750,0x00000060,0x000200f9, +0x000008f4,0x000200f8,0x00000754,0x000500aa, +0x00000024,0x00000756,0x0000056c,0x0000030f, +0x000300f7,0x0000076c,0x00000000,0x000400fa, +0x00000756,0x00000757,0x0000076c,0x000200f8, +0x00000757,0x0008004f,0x00000011,0x00000759, +0x000009f9,0x000009f9,0x00000000,0x00000001, +0x00000002,0x00050094,0x00000008,0x0000075a, +0x00000317,0x00000759,0x0008004f,0x00000011, +0x0000075c,0x00000a13,0x00000a13,0x00000000, +0x00000001,0x00000002,0x00050094,0x00000008, +0x0000075d,0x0000031f,0x0000075c,0x00050094, +0x00000008,0x00000760,0x00000327,0x0000075c, +0x00060050,0x00000011,0x00000764,0x0000075a, +0x0000075d,0x00000760,0x0008000c,0x00000011, +0x00000767,0x00000001,0x0000002b,0x00000764, +0x00000b16,0x00000b17,0x00050051,0x00000008, +0x00000768,0x00000767,0x00000000,0x00050051, +0x00000008,0x00000769,0x00000767,0x00000001, +0x00050051,0x00000008,0x0000076a,0x00000767, +0x00000002,0x00070050,0x0000000a,0x0000076b, +0x00000768,0x00000769,0x0000076a,0x00000060, +0x000200f9,0x000008f4,0x000200f8,0x0000076c, +0x000500aa,0x00000024,0x0000076e,0x0000056c, +0x00000338,0x000300f7,0x00000777,0x00000000, +0x000400fa,0x0000076e,0x0000076f,0x00000777, +0x000200f8,0x0000076f,0x00050051,0x00000008, +0x00000771,0x00000a13,0x00000000,0x00050051, +0x00000008,0x00000773,0x000009f9,0x00000001, +0x00050051,0x00000008,0x00000775,0x00000a13, +0x00000002,0x00070050,0x0000000a,0x00000776, +0x00000771,0x00000773,0x00000775,0x00000060, +0x000200f9,0x000008f4,0x000200f8,0x00000777, +0x000500aa,0x00000024,0x00000779,0x0000056c, +0x00000345,0x000300f7,0x000007c7,0x00000000, +0x000400fa,0x00000779,0x0000077a,0x000007c7, +0x000200f8,0x0000077a,0x00050051,0x00000008, +0x0000077c,0x000009f9,0x00000000,0x00050085, +0x00000008,0x0000077d,0x00000235,0x0000077c, +0x00050051,0x00000008,0x0000077f,0x000009f9, +0x00000001,0x00050085,0x00000008,0x00000780, +0x0000034d,0x0000077f,0x00050083,0x00000008, +0x00000781,0x0000077d,0x00000780,0x00050051, +0x00000008,0x00000783,0x000009f9,0x00000002, +0x00050085,0x00000008,0x00000784,0x00000352, +0x00000783,0x00050083,0x00000008,0x00000785, +0x00000781,0x00000784,0x00050051,0x00000008, +0x00000787,0x00000a13,0x00000000,0x00050085, +0x00000008,0x00000788,0x00000357,0x00000787, +0x00050081,0x00000008,0x00000789,0x00000785, +0x00000788,0x00050051,0x00000008,0x0000078b, +0x00000a13,0x00000001,0x00050085,0x00000008, +0x0000078c,0x0000035c,0x0000078b,0x00050081, +0x00000008,0x0000078d,0x00000789,0x0000078c, +0x00050051,0x00000008,0x0000078f,0x00000a13, +0x00000002,0x00050085,0x00000008,0x00000790, +0x0000023e,0x0000078f,0x00050081,0x00000008, +0x00000791,0x0000078d,0x00000790,0x0008000c, +0x00000008,0x00000792,0x00000001,0x0000002b, +0x00000791,0x0000006e,0x00000060,0x00050085, +0x00000008,0x00000795,0x00000367,0x0000077c, +0x00050085,0x00000008,0x00000798,0x0000036b, +0x0000077f,0x00050081,0x00000008,0x00000799, +0x00000795,0x00000798,0x00050085,0x00000008, +0x0000079c,0x00000370,0x00000783,0x00050081, +0x00000008,0x0000079d,0x00000799,0x0000079c, +0x00050085,0x00000008,0x000007a0,0x00000375, +0x00000787,0x00050083,0x00000008,0x000007a1, +0x0000079d,0x000007a0,0x00050085,0x00000008, +0x000007a4,0x0000037a,0x0000078b,0x00050083, +0x00000008,0x000007a5,0x000007a1,0x000007a4, +0x00050085,0x00000008,0x000007a8,0x0000037f, +0x0000078f,0x00050081,0x00000008,0x000007a9, +0x000007a5,0x000007a8,0x0008000c,0x00000008, +0x000007aa,0x00000001,0x0000002b,0x000007a9, +0x0000006e,0x00000060,0x00050085,0x00000008, +0x000007ad,0x00000386,0x0000077c,0x00050085, +0x00000008,0x000007b0,0x0000038a,0x0000077f, +0x00050083,0x00000008,0x000007b1,0x000007ad, +0x000007b0,0x00050085,0x00000008,0x000007b4, +0x0000038f,0x00000783,0x00050081,0x00000008, +0x000007b5,0x000007b1,0x000007b4,0x00050085, +0x00000008,0x000007b8,0x0000024d,0x00000787, +0x00050081,0x00000008,0x000007b9,0x000007b5, +0x000007b8,0x00050085,0x00000008,0x000007bc, +0x00000398,0x0000078b,0x00050081,0x00000008, +0x000007bd,0x000007b9,0x000007bc,0x00050085, +0x00000008,0x000007c0,0x0000039d,0x0000078f, +0x00050081,0x00000008,0x000007c1,0x000007bd, +0x000007c0,0x0008000c,0x00000008,0x000007c2, +0x00000001,0x0000002b,0x000007c1,0x0000006e, +0x00000060,0x00070050,0x0000000a,0x000007c6, +0x00000792,0x000007aa,0x000007c2,0x00000060, +0x000200f9,0x000008f4,0x000200f8,0x000007c7, +0x000500aa,0x00000024,0x000007c9,0x0000056c, +0x000003a9,0x000300f7,0x00000851,0x00000000, +0x000400fa,0x000007c9,0x000007ca,0x00000851, +0x000200f8,0x000007ca,0x00050085,0x0000000a, +0x000007d7,0x00000a13,0x00000b25,0x0008000c, +0x0000000a,0x000007da,0x00000001,0x0000002b, +0x000007d7,0x00000b1d,0x00000b1e,0x00050051, +0x00000008,0x000007dc,0x000007da,0x00000000, +0x00050051,0x00000008,0x000007de,0x000007da, +0x00000001,0x00050081,0x00000008,0x000007df, +0x000007dc,0x000007de,0x00050051,0x00000008, +0x000007e1,0x000007da,0x00000002,0x00050081, +0x00000008,0x000007e2,0x000007df,0x000007e1, +0x0007000c,0x00000008,0x000007e3,0x00000001, +0x0000001a,0x000007e2,0x000002b3,0x00050085, +0x0000000a,0x000007ee,0x000009f9,0x00000b26, +0x0008000c,0x0000000a,0x000007f1,0x00000001, +0x0000002b,0x000007ee,0x00000b1d,0x00000b1e, +0x00050051,0x00000008,0x000007f3,0x000007f1, +0x00000000,0x00050051,0x00000008,0x000007f5, +0x000007f1,0x00000001,0x00050081,0x00000008, +0x000007f6,0x000007f3,0x000007f5,0x00050051, +0x00000008,0x000007f8,0x000007f1,0x00000002, +0x00050081,0x00000008,0x000007f9,0x000007f6, +0x000007f8,0x0007000c,0x00000008,0x000007fa, +0x00000001,0x0000001a,0x000007f9,0x000003e5, +0x00050085,0x0000000a,0x00000801,0x00000a13, +0x00000b27,0x0008000c,0x0000000a,0x00000804, +0x00000001,0x0000002b,0x00000801,0x00000b1d, +0x00000b1e,0x00050051,0x00000008,0x00000806, +0x00000804,0x00000000,0x00050051,0x00000008, +0x00000808,0x00000804,0x00000001,0x00050081, +0x00000008,0x00000809,0x00000806,0x00000808, +0x00050051,0x00000008,0x0000080b,0x00000804, +0x00000002,0x00050081,0x00000008,0x0000080c, +0x00000809,0x0000080b,0x0007000c,0x00000008, +0x0000080d,0x00000001,0x0000001a,0x0000080c, +0x000002b3,0x00050085,0x00000008,0x00000816, +0x000007e3,0x00000b28,0x00050081,0x00000008, +0x00000817,0x000007e3,0x00000816,0x00050085, +0x00000008,0x0000081c,0x000007fa,0x00000b29, +0x00050081,0x00000008,0x0000081d,0x00000817, +0x0000081c,0x00050085,0x00000008,0x00000822, +0x0000080d,0x00000b29,0x00050081,0x00000008, +0x00000823,0x0000081d,0x00000822,0x00050085, +0x00000008,0x0000082b,0x000007e3,0x00000b2a, +0x00050081,0x00000008,0x0000082c,0x000007fa, +0x0000082b,0x00050085,0x00000008,0x00000831, +0x000007fa,0x00000b2b,0x00050081,0x00000008, +0x00000832,0x0000082c,0x00000831,0x00050085, +0x00000008,0x00000837,0x0000080d,0x00000b2a, +0x00050081,0x00000008,0x00000838,0x00000832, +0x00000837,0x00050085,0x00000008,0x00000840, +0x000007e3,0x00000b29,0x00050081,0x00000008, +0x00000841,0x0000080d,0x00000840,0x00050081, +0x00000008,0x00000847,0x00000841,0x0000081c, +0x00050085,0x00000008,0x0000084c,0x0000080d, +0x00000b28,0x00050081,0x00000008,0x0000084d, +0x00000847,0x0000084c,0x00070050,0x0000000a, +0x00000b34,0x00000823,0x00000838,0x0000084d, +0x00000060,0x000200f9,0x000008f4,0x000200f8, +0x00000851,0x000500aa,0x00000024,0x00000853, +0x0000056c,0x0000043f,0x000300f7,0x000008ef, +0x00000000,0x000400fa,0x00000853,0x00000854, +0x000008ef,0x000200f8,0x00000854,0x00050085, +0x0000000a,0x00000860,0x000009f9,0x00000b2c, +0x0008000c,0x0000000a,0x00000863,0x00000001, +0x0000002b,0x00000860,0x00000b1d,0x00000b1e, +0x00050051,0x00000008,0x00000865,0x00000863, +0x00000000,0x00050051,0x00000008,0x00000867, +0x00000863,0x00000001,0x00050081,0x00000008, +0x00000868,0x00000865,0x00000867,0x00050051, +0x00000008,0x0000086a,0x00000863,0x00000002, +0x00050081,0x00000008,0x0000086b,0x00000868, +0x0000086a,0x0007000c,0x00000008,0x0000086c, +0x00000001,0x0000001a,0x0000086b,0x000003e5, +0x00050085,0x0000000a,0x00000873,0x000009f9, +0x00000b1f,0x0008000c,0x0000000a,0x00000876, +0x00000001,0x0000002b,0x00000873,0x00000b1d, +0x00000b1e,0x00050051,0x00000008,0x00000878, +0x00000876,0x00000000,0x00050051,0x00000008, +0x0000087a,0x00000876,0x00000001,0x00050081, +0x00000008,0x0000087b,0x00000878,0x0000087a, +0x00050051,0x00000008,0x0000087d,0x00000876, +0x00000002,0x00050081,0x00000008,0x0000087e, +0x0000087b,0x0000087d,0x0007000c,0x00000008, +0x0000087f,0x00000001,0x0000001a,0x0000087e, +0x00000474,0x00050085,0x0000000a,0x0000088a, +0x00000a13,0x00000b2d,0x0008000c,0x0000000a, +0x0000088d,0x00000001,0x0000002b,0x0000088a, +0x00000b1d,0x00000b1e,0x00050051,0x00000008, +0x0000088f,0x0000088d,0x00000000,0x00050051, +0x00000008,0x00000891,0x0000088d,0x00000001, +0x00050081,0x00000008,0x00000892,0x0000088f, +0x00000891,0x00050051,0x00000008,0x00000894, +0x0000088d,0x00000002,0x00050081,0x00000008, +0x00000895,0x00000892,0x00000894,0x0007000c, +0x00000008,0x00000896,0x00000001,0x0000001a, +0x00000895,0x00000060,0x0007000c,0x00000008, +0x0000089d,0x00000001,0x0000001a,0x00000896, +0x00000b2f,0x00050083,0x00000008,0x000008a0, +0x00000060,0x00000896,0x0006000c,0x00000008, +0x000008a1,0x00000001,0x00000004,0x000008a0, +0x0007000c,0x00000008,0x000008a5,0x00000001, +0x0000001a,0x000008a1,0x00000b2f,0x00050083, +0x00000008,0x000008a6,0x00000060,0x000008a5, +0x0008000c,0x00000008,0x000008a9,0x00000001, +0x0000002e,0x0000089d,0x000008a6,0x00000896, +0x00050085,0x00000008,0x000008b2,0x0000086c, +0x00000b30,0x00050081,0x00000008,0x000008b3, +0x0000086c,0x000008b2,0x00050085,0x00000008, +0x000008b8,0x0000087f,0x00000b31,0x00050081, +0x00000008,0x000008b9,0x000008b3,0x000008b8, +0x00050085,0x00000008,0x000008be,0x000008a9, +0x00000b31,0x00050081,0x00000008,0x000008bf, +0x000008b9,0x000008be,0x00060052,0x0000000a, +0x00000aed,0x000008bf,0x00000134,0x00000000, +0x00050085,0x00000008,0x000008c7,0x0000086c, +0x00000b31,0x00050081,0x00000008,0x000008c8, +0x0000087f,0x000008c7,0x00050085,0x00000008, +0x000008cd,0x0000087f,0x00000b30,0x00050081, +0x00000008,0x000008ce,0x000008c8,0x000008cd, +0x00050081,0x00000008,0x000008d4,0x000008ce, +0x000008be,0x00060052,0x0000000a,0x00000af3, +0x000008d4,0x00000aed,0x00000001,0x00050085, +0x00000008,0x000008dc,0x0000086c,0x00000b32, +0x00050081,0x00000008,0x000008dd,0x000008a9, +0x000008dc,0x00050085,0x00000008,0x000008e2, +0x0000087f,0x00000b32,0x00050081,0x00000008, +0x000008e3,0x000008dd,0x000008e2,0x00050085, +0x00000008,0x000008e8,0x000008a9,0x00000b33, +0x00050081,0x00000008,0x000008e9,0x000008e3, +0x000008e8,0x00060052,0x0000000a,0x00000af9, +0x000008e9,0x00000af3,0x00000002,0x0008000c, +0x0000000a,0x000008ee,0x00000001,0x0000002b, +0x00000af9,0x00000b1d,0x00000b1e,0x000200f9, +0x000008f4,0x000200f8,0x000008ef,0x0004003d, +0x0000003e,0x000008f0,0x000000b8,0x00050051, +0x00000008,0x000008f1,0x000008f0,0x00000000, +0x00050051,0x00000008,0x000008f2,0x000008f0, +0x00000001,0x00070050,0x0000000a,0x000008f3, +0x000008f1,0x000008f2,0x0000006e,0x00000060, +0x000200f9,0x000008f4,0x000200f8,0x000008f4, +0x002b00f5,0x0000000a,0x00000b12,0x0000090f, +0x00000903,0x00000929,0x000005be,0x00000943, +0x000005cd,0x00000134,0x000005da,0x0000095d, +0x00000951,0x00000977,0x0000096b,0x00000991, +0x00000985,0x00000134,0x0000061d,0x000009ab, +0x0000099f,0x000009c5,0x000009b9,0x000009df, +0x00000661,0x0000067b,0x00000674,0x000006cb, +0x0000067f,0x00000b35,0x000006cf,0x0000076b, +0x00000757,0x00000776,0x0000076f,0x000007c6, +0x0000077a,0x00000b34,0x000007ca,0x000008ee, +0x00000854,0x000008f3,0x000008ef,0x00050041, +0x00000020,0x000004f2,0x0000001e,0x00000163, +0x0004003d,0x00000006,0x000004f3,0x000004f2, +0x000500ab,0x00000024,0x000004f4,0x000004f3, +0x00000023,0x000300f7,0x000004f6,0x00000000, +0x000400fa,0x000004f4,0x000004f5,0x000004f6, +0x000200f8,0x000004f5,0x0008004f,0x00000011, +0x000004f9,0x00000b12,0x00000b12,0x00000000, +0x00000001,0x00000002,0x00060041,0x00000047, +0x00000a1c,0x0000001e,0x00000044,0x00000046, +0x0004003d,0x00000008,0x00000a1d,0x00000a1c, +0x00060041,0x00000047,0x00000a1e,0x0000001e, +0x00000044,0x0000004b,0x0004003d,0x00000008, +0x00000a1f,0x00000a1e,0x00060041,0x00000047, +0x00000a20,0x0000001e,0x00000044,0x0000004f, +0x0004003d,0x00000008,0x00000a21,0x00000a20, +0x0006000c,0x00000011,0x00000a23,0x00000001, +0x00000004,0x000004f9,0x00050041,0x00000047, +0x00000a24,0x0000001e,0x00000055,0x0004003d, +0x00000008,0x00000a25,0x00000a24,0x00060050, +0x00000011,0x00000a26,0x00000a25,0x00000a25, +0x00000a25,0x0007000c,0x00000011,0x00000a27, +0x00000001,0x0000001a,0x00000a23,0x00000a26, +0x00060050,0x00000011,0x00000a29,0x00000a1d, +0x00000a1d,0x00000a1d,0x00050081,0x00000011, +0x00000a2a,0x00000a27,0x00000a29,0x00050088, +0x00000008,0x00000a2f,0x00000060,0x00000a25, +0x00060050,0x00000011,0x00000a30,0x00000a2f, +0x00000a2f,0x00000a2f,0x0007000c,0x00000011, +0x00000a31,0x00000001,0x0000001a,0x00000a23, +0x00000a30,0x00060050,0x00000011,0x00000a33, +0x00000a1f,0x00000a1f,0x00000a1f,0x00050081, +0x00000011,0x00000a34,0x00000a31,0x00000a33, +0x00060050,0x00000011,0x00000a37,0x00000a21, +0x00000a21,0x00000a21,0x00050081,0x00000011, +0x00000a38,0x000004f9,0x00000a37,0x0008000c, +0x00000011,0x00000a3b,0x00000001,0x0000002b, +0x00000a38,0x00000b16,0x00000b17,0x0008000c, +0x00000011,0x00000a3f,0x00000001,0x0000002e, +0x00000a2a,0x00000a34,0x00000a3b,0x00050041, +0x00000079,0x00000a41,0x0000001e,0x00000078, +0x0004003d,0x0000000a,0x00000a42,0x00000a41, +0x0008004f,0x00000011,0x00000a43,0x00000a42, +0x00000a42,0x00000000,0x00000001,0x00000002, +0x0005008e,0x00000011,0x00000a44,0x00000a43, +0x00000030,0x00050083,0x00000011,0x00000a46, +0x00000b18,0x00000a44,0x00050085,0x00000011, +0x00000a47,0x00000a3f,0x00000a46,0x00050081, +0x00000011,0x00000a4c,0x00000a47,0x00000a44, +0x00050083,0x00000011,0x00000a4e,0x00000a4c, +0x00000b19,0x0008000c,0x00000011,0x00000a52, +0x00000001,0x0000002b,0x00000a4e,0x00000b16, +0x00000b17,0x00050041,0x00000079,0x00000a53, +0x0000001e,0x0000008c,0x0004003d,0x0000000a, +0x00000a54,0x00000a53,0x0008004f,0x00000011, +0x00000a55,0x00000a54,0x00000a54,0x00000000, +0x00000001,0x00000002,0x00050085,0x00000011, +0x00000a57,0x00000a52,0x00000a55,0x0006000c, +0x00000011,0x00000a59,0x00000001,0x00000004, +0x00000a57,0x00050041,0x00000079,0x00000a5a, +0x0000001e,0x00000094,0x0004003d,0x0000000a, +0x00000a5b,0x00000a5a,0x0008004f,0x00000011, +0x00000a5c,0x00000a5b,0x00000a5b,0x00000000, +0x00000001,0x00000002,0x00050088,0x00000011, +0x00000a5e,0x00000b17,0x00000a5c,0x0007000c, +0x00000011,0x00000a5f,0x00000001,0x0000001a, +0x00000a59,0x00000a5e,0x0008000c,0x00000011, +0x00000a63,0x00000001,0x0000002b,0x00000a5f, +0x00000b16,0x00000b17,0x00050051,0x00000008, +0x000004fc,0x00000a63,0x00000000,0x00060052, +0x0000000a,0x00000afb,0x000004fc,0x00000b12, +0x00000000,0x00050051,0x00000008,0x000004fe, +0x00000a63,0x00000001,0x00060052,0x0000000a, +0x00000afd,0x000004fe,0x00000afb,0x00000001, +0x00050051,0x00000008,0x00000500,0x00000a63, +0x00000002,0x00060052,0x0000000a,0x00000aff, +0x00000500,0x00000afd,0x00000002,0x000200f9, +0x000004f6,0x000200f8,0x000004f6,0x000700f5, +0x0000000a,0x00000b13,0x00000b12,0x000008f4, +0x00000aff,0x000004f5,0x0003003e,0x00000502, +0x00000b13,0x000100fd,0x00010038} +; diff --git a/vrto3d/shaders/repack.frag b/vrto3d/shaders/repack.frag new file mode 100644 index 00000000..9c9aed96 --- /dev/null +++ b/vrto3d/shaders/repack.frag @@ -0,0 +1,342 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#version 450 + +// GLSL port of the Windows presenter's repack pixel shader (kPsSource in +// vrto3d/src/presenter/window_presenter.cpp) plus the display-correction +// pass (kAdjustPsHlsl in vrto3d/src/dx11_renderer.cpp) folded in as an +// optional post-step gated by pc.correction_enabled. +// +// The input `sbs` is the canonical 2W x H side-by-side texture — left eye in +// the left half (before eye_swap). uv (0,0) is the top-left of the OUTPUT +// window (see fullscreen.vert), and v = 0 samples the top row of `sbs`. +// +// `pc.mode` carries the raw OutputMode enum value (vrto3dlib/stereo_config.h), +// NOT the Windows presenter's remapped shader enum. The FramePacked* variants +// derive their blank-gap row count from the mode itself (30 px for 720p60, +// 45 px for the 1080p variants — mirrors FramePackTimingSpec::gap_pixels, +// which is what the Windows presenter feeds as `framepack_offset`). +// +// C++ mirror of the push-constant block (std430 layout, 96 bytes total): +// +// struct RepackPushConstants { // offset +// int32_t out_size[2]; // 0 swapchain extent (w, h) +// int32_t eye_size[2]; // 8 per-eye pixel size of sbs +// int32_t mode; // 16 OutputMode int value +// int32_t eye_swap; // 20 0/1 +// int32_t correction_enabled; // 24 0/1 display-correction post-step +// float curve; // 28 shader_curve +// float lift[4]; // 32 rgb + pad +// float gamma[4]; // 48 rgb + pad +// float gain[4]; // 64 rgb + pad +// float curve_offsets[4]; // 80 x=off_low y=off_high z=off_both w=unused +// }; // 96 bytes (<= 128 push-constant min-spec) +// static_assert(sizeof(RepackPushConstants) == 96); + +layout(set = 0, binding = 0) uniform sampler2D sbs; + +layout(location = 0) in vec2 uv; +layout(location = 0) out vec4 out_color; + +layout(push_constant) uniform Pc { + ivec2 out_size; // swapchain extent + ivec2 eye_size; // per-eye pixel size of sbs (sbs is 2*eye_size.x wide); reserved + int mode; // OutputMode int value + int eye_swap; // 0/1 + int correction_enabled; // display-correction post-step + float curve; // shader_curve + vec4 lift; // rgb + pad + vec4 gamma_; // rgb + pad + vec4 gain; // rgb + pad + vec4 curve_offsets; // x=off_low y=off_high z=off_both w=unused +} pc; + +// OutputMode enum values — keep in sync with vrto3dlib/stereo_config.h. +const int kSbS = 0; +const int kTaB = 1; +const int kRowInterlaced = 2; +const int kColInterlaced = 3; +const int kCheckerboard = 4; +const int kLeiaSR = 5; // alternate presenter — SbS fallback +const int kNvidiaDX9 = 6; // alternate presenter — SbS fallback +const int kWibbleWobble = 7; // alternate presenter — SbS fallback +const int kVirtualDesktop = 8; +const int kFramePacked720p60 = 9; +const int kFramePacked1080p24 = 10; +const int kFramePacked1080p60 = 11; +const int kFramePacked1080p60CVT = 12; +const int kDualDisplay = 13; +const int kDualDisplayFlip = 14; +const int kAnaglyphRedCyan = 15; +const int kAnaglyphRedCyanDubois = 16; +const int kAnaglyphRedCyanDeghosted = 17; +const int kAnaglyphRedCyanCompromise = 18; +const int kAnaglyphGreenMagenta = 19; +const int kAnaglyphGreenMagentaDubois = 20; +const int kAnaglyphGreenMagentaDeghosted = 21; +const int kAnaglyphBlueAmber = 22; +const int kMono = 23; + +// Given a half (0=left, 1=right), sample the input SbS at (u_half, v). +// u_half in [0,1] = position within the eye's half of the input. +// eye_swap flips which half-source we read (mirrors HLSL SampleEye). +vec4 SampleEye(int half_idx, float u_half, float v) { + int eye = (pc.eye_swap != 0) ? (1 - half_idx) : half_idx; + float u_src = 0.5 * u_half + 0.5 * float(eye); + return texture(sbs, vec2(u_src, v)); +} + +// Display-correction post-process — exact port of kAdjustPsHlsl +// (dx11_renderer.cpp): extended SCurve.fx followed by LiftGammaGain.fx. +// Operates in the same byte space the swapchain stores (defaults are all +// pass-through, so black stays black in framepack gap rows). +vec3 ApplyCorrection(vec3 col) { + float off_low = pc.curve_offsets.x; + float off_high = pc.curve_offsets.y; + float off_both = pc.curve_offsets.z; + + // Extended SCurve.fx — at curve=1.0 the mix degenerates to pass-through. + vec3 low = pow(abs(col), vec3(pc.curve)) + off_low; + vec3 high = pow(abs(col), vec3(1.0 / pc.curve)) + off_high; + vec3 t = clamp(col + off_both, 0.0, 1.0); + col = mix(low, high, t); + + // LiftGammaGain.fx (operates in sRGB byte space — matches ReShade default). + col = col * (1.5 - 0.5 * pc.lift.rgb) + 0.5 * pc.lift.rgb - 0.5; + col = clamp(col, 0.0, 1.0); + col *= pc.gain.rgb; + col = pow(abs(col), 1.0 / pc.gamma_.rgb); + + return clamp(col, 0.0, 1.0); +} + +vec4 Repack() { + int m = pc.mode; + + // SbS / DualDisplay (+ alternate-presenter fallbacks): straight + // horizontal split. For DualDisplay the output window spans two + // monitors, so the left half (= left monitor) shows the left eye + // fullscreen and the right half the right eye. + if (m == kSbS || m == kDualDisplay + || m == kLeiaSR || m == kNvidiaDX9 || m == kWibbleWobble) { + int half_idx = (uv.x < 0.5) ? 0 : 1; + float u_half = (uv.x < 0.5) ? (uv.x * 2.0) : ((uv.x - 0.5) * 2.0); + return SampleEye(half_idx, u_half, uv.y); + } + + // TaB / FramePacked*: left eye on the top rows, blank gap rows black, + // right eye below. Plain TaB has no gap; the FramePacked variants derive + // the gap row count from the HDMI 1.4 timing spec. + if (m == kTaB || m == kFramePacked720p60 || m == kFramePacked1080p24 + || m == kFramePacked1080p60 || m == kFramePacked1080p60CVT) { + float gap_px = 0.0; + if (m == kFramePacked720p60) gap_px = 30.0; + else if (m != kTaB) gap_px = 45.0; // 1080p24/60/60CVT + float gap = (gap_px > 0.0) ? (gap_px / float(pc.out_size.y)) : 0.0; + float half_height = (1.0 - gap) * 0.5; + if (uv.y < half_height) { + // Top half -> left eye + return SampleEye(0, uv.x, uv.y / half_height); + } else if (uv.y >= half_height + gap) { + // Bottom half -> right eye + float v_rel = (uv.y - (half_height + gap)) / half_height; + return SampleEye(1, uv.x, v_rel); + } else { + return vec4(0.0, 0.0, 0.0, 1.0); // framepack gap + } + } + + // Row-interlaced: alternating rows; odd output rows = right eye. + if (m == kRowInterlaced) { + int row = int(gl_FragCoord.y); + int half_idx = row & 1; + return SampleEye(half_idx, uv.x, uv.y); + } + + // Col-interlaced + if (m == kColInterlaced) { + int col = int(gl_FragCoord.x); + int half_idx = col & 1; + return SampleEye(half_idx, uv.x, uv.y); + } + + // Checkerboard + if (m == kCheckerboard) { + int row = int(gl_FragCoord.y); + int col = int(gl_FragCoord.x); + int half_idx = (row + col) & 1; + return SampleEye(half_idx, uv.x, uv.y); + } + + // VirtualDesktop: SbS in center band of a 2W x 2H window (rows [0.25, 0.75)). + if (m == kVirtualDesktop) { + if (uv.y < 0.25 || uv.y >= 0.75) return vec4(0.0, 0.0, 0.0, 1.0); + float v = (uv.y - 0.25) * 2.0; + int half_idx = (uv.x < 0.5) ? 0 : 1; + float u_half = (uv.x < 0.5) ? (uv.x * 2.0) : ((uv.x - 0.5) * 2.0); + return SampleEye(half_idx, u_half, v); + } + + // DualDisplayFlip: same horizontal split as SbS/DualDisplay, but the LEFT + // half is sampled with v inverted (vertical flip). + if (m == kDualDisplayFlip) { + int half_idx = (uv.x < 0.5) ? 0 : 1; + float u_half = (uv.x < 0.5) ? (uv.x * 2.0) : ((uv.x - 0.5) * 2.0); + float v_src = (half_idx == 0) ? (1.0 - uv.y) : uv.y; + return SampleEye(half_idx, u_half, v_src); + } + + // Mono — single-eye view. eye_swap selects right (1) instead of left (0). + if (m == kMono) { + return SampleEye(0, uv.x, uv.y); + } + + // ----- Anaglyph variants (3DToElse / iaian7+vectorform formulas) ----- + // Sample full image of each eye; cA = left, cB = right. + vec4 cA = SampleEye(0, uv.x, uv.y); + vec4 cB = SampleEye(1, uv.x, uv.y); + + // Anaglyph red-cyan (simple) + if (m == kAnaglyphRedCyan) { + return vec4(cA.r, cB.g, cB.b, 1.0); + } + + // Anaglyph red-cyan Dubois + if (m == kAnaglyphRedCyanDubois) { + float r = clamp( 0.437 * cA.r + 0.449 * cA.g + 0.164 * cA.b + - 0.011 * cB.r - 0.032 * cB.g - 0.007 * cB.b, 0.0, 1.0); + float g = clamp(-0.062 * cA.r - 0.062 * cA.g - 0.024 * cA.b + + 0.377 * cB.r + 0.761 * cB.g + 0.009 * cB.b, 0.0, 1.0); + float b = clamp(-0.048 * cA.r - 0.050 * cA.g - 0.017 * cA.b + - 0.026 * cB.r - 0.093 * cB.g + 1.234 * cB.b, 0.0, 1.0); + return vec4(r, g, b, 1.0); + } + + // Anaglyph red-cyan deghosted (iaian7 / vectorform) + if (m == kAnaglyphRedCyanDeghosted) { + float Contrast = 1.0; + float DeGhost = 0.06 * 0.1; + float contrast = (Contrast * 0.5) + 0.5; + float LOne = contrast * 0.45; + float ROne = contrast; + + vec4 image = vec4(0.0, 0.0, 0.0, 1.0); + vec4 accum; + accum = clamp(cA * vec4(LOne, (1.0 - LOne) * 0.5, (1.0 - LOne) * 0.5, 1.0), 0.0, 1.0); + image.r = pow(accum.r + accum.g + accum.b, 1.00); + accum = clamp(cB * vec4(1.0 - ROne, ROne, 0.0, 1.0), 0.0, 1.0); + image.g = pow(accum.r + accum.g + accum.b, 1.15); + accum = clamp(cB * vec4(1.0 - ROne, 0.0, ROne, 1.0), 0.0, 1.0); + image.b = pow(accum.r + accum.g + accum.b, 1.15); + + accum = image; + image.r = accum.r + (accum.r * DeGhost) + (accum.g * (DeGhost * -0.5)) + (accum.b * (DeGhost * -0.5)); + image.g = accum.g + (accum.r * (DeGhost * -0.25)) + (accum.g * (DeGhost * 0.5)) + (accum.b * (DeGhost * -0.25)); + image.b = accum.b + (accum.r * (DeGhost * -0.25)) + (accum.g * (DeGhost * -0.25)) + (accum.b * (DeGhost * 0.5)); + image.a = 1.0; + return image; + } + + // Anaglyph red-cyan compromise. The HLSL uses mul(row_matrix, rgb) — + // written out here as explicit row dot products to avoid GLSL's + // column-major mat3 constructor changing the meaning. + if (m == kAnaglyphRedCyanCompromise) { + float r = dot(vec3( 0.439, 0.447, 0.148), cA.rgb); + float g = dot(vec3( 0.095, 0.934, -0.005), cB.rgb); + float b = dot(vec3(-0.018, -0.028, 1.057), cB.rgb); + return vec4(clamp(vec3(r, g, b), 0.0, 1.0), 1.0); + } + + // Anaglyph green-magenta (simple) + if (m == kAnaglyphGreenMagenta) { + return vec4(cB.r, cA.g, cB.b, 1.0); + } + + // Anaglyph green-magenta Dubois + if (m == kAnaglyphGreenMagentaDubois) { + float r = clamp(-0.062 * cA.r - 0.158 * cA.g - 0.039 * cA.b + + 0.529 * cB.r + 0.705 * cB.g + 0.024 * cB.b, 0.0, 1.0); + float g = clamp( 0.284 * cA.r + 0.668 * cA.g + 0.143 * cA.b + - 0.016 * cB.r - 0.015 * cB.g + 0.065 * cB.b, 0.0, 1.0); + float b = clamp(-0.015 * cA.r - 0.027 * cA.g + 0.021 * cA.b + + 0.009 * cB.r + 0.075 * cB.g + 0.937 * cB.b, 0.0, 1.0); + return vec4(r, g, b, 1.0); + } + + // Anaglyph green-magenta deghosted + if (m == kAnaglyphGreenMagentaDeghosted) { + float Contrast = 1.0; + float DeGhost = 0.06 * 0.275; + float contrast = (Contrast * 0.5) + 0.5; + float LOne = contrast * 0.45; + float ROne = contrast * 0.8; + + vec4 image = vec4(0.0, 0.0, 0.0, 1.0); + vec4 accum; + accum = clamp(cB * vec4(ROne, 1.0 - ROne, 0.0, 1.0), 0.0, 1.0); + image.r = pow(accum.r + accum.g + accum.b, 1.15); + accum = clamp(cA * vec4((1.0 - LOne) * 0.5, LOne, (1.0 - LOne) * 0.5, 1.0), 0.0, 1.0); + image.g = pow(accum.r + accum.g + accum.b, 1.05); + accum = clamp(cB * vec4(0.0, 1.0 - ROne, ROne, 1.0), 0.0, 1.0); + image.b = pow(accum.r + accum.g + accum.b, 1.15); + + accum = image; + image.r = accum.r + (accum.r * (DeGhost * 0.5)) + (accum.g * (DeGhost * -0.25)) + (accum.b * (DeGhost * -0.25)); + image.g = accum.g + (accum.r * (DeGhost * -0.5)) + (accum.g * (DeGhost * 0.25)) + (accum.b * (DeGhost * -0.5)); + image.b = accum.b + (accum.r * (DeGhost * -0.25)) + (accum.g * (DeGhost * -0.25)) + (accum.b * (DeGhost * 0.5)); + image.a = 1.0; + return image; + } + + // Anaglyph blue-amber (ColorCode style) + if (m == kAnaglyphBlueAmber) { + float Contrast = 1.0; + float DeGhost = 0.06 * 0.275; + float contrast = (Contrast * 0.5) + 0.5; + float LOne = contrast * 0.45; + float ROne = contrast; + + vec4 image = vec4(0.0, 0.0, 0.0, 1.0); + vec4 accum; + accum = clamp(cA * vec4(ROne, 0.0, 1.0 - ROne, 1.0), 0.0, 1.0); + image.r = pow(accum.r + accum.g + accum.b, 1.05); + accum = clamp(cA * vec4(0.0, ROne, 1.0 - ROne, 1.0), 0.0, 1.0); + image.g = pow(accum.r + accum.g + accum.b, 1.10); + accum = clamp(cB * vec4((1.0 - LOne) * 0.5, (1.0 - LOne) * 0.5, LOne, 1.0), 0.0, 1.0); + image.b = pow(accum.r + accum.g + accum.b, 1.0); + image.b = mix(pow(image.b, (DeGhost * 0.15) + 1.0), + 1.0 - pow(abs(1.0 - image.b), (DeGhost * 0.15) + 1.0), + image.b); + + accum = image; + image.r = accum.r + (accum.r * (DeGhost * 1.5)) + (accum.g * (DeGhost * -0.75)) + (accum.b * (DeGhost * -0.75)); + image.g = accum.g + (accum.r * (DeGhost * -0.75)) + (accum.g * (DeGhost * 1.5)) + (accum.b * (DeGhost * -0.75)); + image.b = accum.b + (accum.r * (DeGhost * -1.5)) + (accum.g * (DeGhost * -1.5)) + (accum.b * (DeGhost * 3.0)); + return clamp(image, 0.0, 1.0); + } + + // Fallback (mirrors the HLSL debug output) + return vec4(uv, 0.0, 1.0); +} + +void main() { + vec4 c = Repack(); + if (pc.correction_enabled != 0) { + c.rgb = ApplyCorrection(c.rgb); + } + out_color = c; +} diff --git a/vrto3d/src/auto_depth.cpp b/vrto3d/src/auto_depth.cpp new file mode 100644 index 00000000..eb4a99c9 --- /dev/null +++ b/vrto3d/src/auto_depth.cpp @@ -0,0 +1,455 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#include "auto_depth.h" + +#define WIN32_LEAN_AND_MEAN +#include + +#include +#include +#include + +#include + +#include "vrto3dlib/debug_log.hpp" + +#include "hmd_device_driver.h" // StereoDisplayComponent::FeedAutoDepthSample + + +// Disparity histogram — 128 buckets, each spanning `stride` source pixels. +// Stride is 4 up to ~2K-wide eyes and grows on wider targets so the buckets +// always cover the full search radius (see Run()). +// Layout: [0..127] = bucket counts, [128] = total committed matches. +#define NUM_BUCKETS 128 + +namespace { + +// Quarter-res block-matched left/right disparity search. Each thread picks a +// left-eye sample, sweeps `search_radius` candidate offsets in the right eye, +// records the best (lowest SAD) as that thread's disparity, and increments a +// bucket in the global disparity histogram. The CPU then walks the histogram +// for a percentile/peak-shape result. +constexpr const char* kAutoDepthCS = R"HLSL( +Texture2D g_sbs : register(t0); +RWByteAddressBuffer g_result : register(u0); + +cbuffer Params : register(b0) +{ + uint sbs_w; + uint sbs_h; + uint search_radius; // in source pixels + uint stride; // 4 = quarter-res +}; + +#define NUM_BUCKETS 128 + +// Block size for SAD matching. A 5x1 horizontal block is enough to make random +// single-pixel color collisions vanishingly rare, while staying cheap inside +// the inner search loop. +#define BLOCK_HALF 2 + +float3 LoadL(uint x, uint y) { return g_sbs.Load(int3(x, y, 0)).rgb; } +float3 LoadR(uint rx, uint y) { return g_sbs.Load(int3(rx, y, 0)).rgb; } + +[numthreads(8, 8, 1)] +void CSMain(uint3 DTid : SV_DispatchThreadID, uint GI : SV_GroupIndex) +{ + uint eye_w = sbs_w / 2; + uint x = DTid.x * stride; + uint y = DTid.y * stride; + + // Inner ROI: skip the periphery where SteamVR's hidden-area mask blacks + // out the lens corners. Black-vs-black matches at the eye edges + // otherwise dominate the disparity histogram. Keep the central ~84% wide + // by ~90% tall window (per eye). + uint x_lo = eye_w * 8u / 100u; + uint x_hi = eye_w * 92u / 100u; + uint y_lo = sbs_h * 5u / 100u; + uint y_hi = sbs_h * 95u / 100u; + + if (x >= x_lo && x < x_hi && y >= y_lo && y < y_hi) + { + // Local gradient — skip textureless regions. + float3 L = LoadL(x, y); + float3 Lr = LoadL(min(x + stride, eye_w - 1), y); + float grad = abs(L.r - Lr.r) + abs(L.g - Lr.g) + abs(L.b - Lr.b); + // Brightness floor — black hidden-area-mask pixels that survive the + // ROI gate (anti-aliased mask edges) are still ambiguous matches. + float Llum = L.r + L.g + L.b; + if (grad > 0.04 && Llum > 0.05) + { + // Cache the left block once. + float3 Lb[2 * BLOCK_HALF + 1]; + [unroll] for (int i = -BLOCK_HALF; i <= BLOCK_HALF; ++i) + Lb[i + BLOCK_HALF] = LoadL(x + i, y); + + // Search crossed (pop-out) disparity only: right-eye match is to + // the LEFT of the left-eye column. Under the converged off-axis + // projection d=0 == the screen plane (z = convergence), growing d + // == closer. Uncrossed (behind-screen) disparity is not searched + // — background divergence is not this loop's problem. + uint max_d = min(search_radius, x - BLOCK_HALF); + float bestCost = 1e9; + float secondBest = 1e9; + uint bestD = 0; + for (uint d = 0; d < max_d; d += stride) + { + uint rx = eye_w + x - d; + float cost = 0.0; + [unroll] for (int j = -BLOCK_HALF; j <= BLOCK_HALF; ++j) + { + float3 R = LoadR(rx + j, y); + float3 dC = abs(Lb[j + BLOCK_HALF] - R); + cost += dC.r + dC.g + dC.b; + } + // Runner-up tracking excludes candidates adjacent to the + // current best: on smooth textures the cost curve's own + // shoulder is nearly as cheap as the minimum, and counting it + // as "second best" made the uniqueness test below reject + // valid matches on gently-textured near surfaces. + if (cost < bestCost) + { + if (d - bestD > stride) { secondBest = bestCost; } + bestCost = cost; + bestD = d; + } + else if (cost < secondBest && d - bestD > stride) + { + secondBest = cost; + } + } + + // Lowe-style uniqueness test: only trust the match if it's + // meaningfully better than the runner-up (rejects ambiguous / + // repeating-pattern regions where a "best" disparity is random). + // Also require an absolute quality floor. + const float maxAbsCost = 0.30 * float(2 * BLOCK_HALF + 1); // ~0.3 per pixel * block size + if (bestCost < maxAbsCost && bestCost * 1.4 < secondBest) + { + // Bucket index: each bucket spans `stride` source pixels. + uint bucket = bestD / stride; + if (bucket >= NUM_BUCKETS) bucket = NUM_BUCKETS - 1; + uint orig0; + g_result.InterlockedAdd(bucket * 4, 1, orig0); + g_result.InterlockedAdd(NUM_BUCKETS * 4, 1, orig0); + } + } + } +} +)HLSL"; + +struct AutoDepthParams { + uint32_t sbs_w; + uint32_t sbs_h; + uint32_t search_radius; + uint32_t stride; +}; + +} // namespace + + +namespace vrto3d { + +void AutoDepthAnalyzer::Init(ID3D11Device* device) +{ + device_ = device; +} + + +bool AutoDepthAnalyzer::EnsureResources(ID3D11Texture2D* sbs) +{ + if (init_failed_) return false; + if (!device_ || !sbs) return false; + + // Rebuild the SRV whenever the source texture changes (e.g. window + // resize re-creates `out_sbs_` upstream). + if (sbs != srv_source_) { + sbs_srv_.Reset(); + srv_source_ = sbs; + } + + if (!sbs_srv_) { + D3D11_TEXTURE2D_DESC td{}; + sbs->GetDesc(&td); + D3D11_SHADER_RESOURCE_VIEW_DESC sd{}; + sd.Format = td.Format; + sd.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + sd.Texture2D.MipLevels = 1; + sd.Texture2D.MostDetailedMip = 0; + HRESULT hr = device_->CreateShaderResourceView(sbs, &sd, &sbs_srv_); + if (FAILED(hr)) { + LOG() << "AutoDepth: CreateShaderResourceView hr=0x" << std::hex << hr; + init_failed_ = true; + return false; + } + } + + if (cs_ && result_buf_ && result_uav_ && staging_[0] && staging_[1] && params_cb_) { + return sbs_srv_ != nullptr; + } + + // Compile the compute shader. + if (!cs_) { + Microsoft::WRL::ComPtr blob, err; + HRESULT hr = D3DCompile(kAutoDepthCS, std::strlen(kAutoDepthCS), + nullptr, nullptr, nullptr, + "CSMain", "cs_5_0", 0, 0, &blob, &err); + if (FAILED(hr)) { + if (err) LOG() << "AutoDepth: D3DCompile error: " << reinterpret_cast(err->GetBufferPointer()); + else LOG() << "AutoDepth: D3DCompile failed hr=0x" << std::hex << hr; + init_failed_ = true; + return false; + } + hr = device_->CreateComputeShader(blob->GetBufferPointer(), blob->GetBufferSize(), + nullptr, &cs_); + if (FAILED(hr)) { + LOG() << "AutoDepth: CreateComputeShader hr=0x" << std::hex << hr; + init_failed_ = true; + return false; + } + } + + constexpr UINT kResultUints = NUM_BUCKETS + 1; + constexpr UINT kResultBytes = kResultUints * 4; + if (!result_buf_) { + D3D11_BUFFER_DESC bd{}; + bd.ByteWidth = kResultBytes; + bd.Usage = D3D11_USAGE_DEFAULT; + bd.BindFlags = D3D11_BIND_UNORDERED_ACCESS; + bd.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS; + bd.StructureByteStride = 0; + HRESULT hr = device_->CreateBuffer(&bd, nullptr, &result_buf_); + if (FAILED(hr)) { + LOG() << "AutoDepth: CreateBuffer(result) hr=0x" << std::hex << hr; + init_failed_ = true; + return false; + } + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavd{}; + uavd.Format = DXGI_FORMAT_R32_TYPELESS; + uavd.ViewDimension = D3D11_UAV_DIMENSION_BUFFER; + uavd.Buffer.FirstElement = 0; + uavd.Buffer.NumElements = kResultUints; + uavd.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_RAW; + hr = device_->CreateUnorderedAccessView(result_buf_.Get(), &uavd, &result_uav_); + if (FAILED(hr)) { + LOG() << "AutoDepth: CreateUAV hr=0x" << std::hex << hr; + init_failed_ = true; + return false; + } + } + + for (int i = 0; i < 2; ++i) { + if (staging_[i]) continue; + D3D11_BUFFER_DESC bd{}; + bd.ByteWidth = kResultBytes; + bd.Usage = D3D11_USAGE_STAGING; + bd.BindFlags = 0; + bd.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + bd.MiscFlags = 0; + HRESULT hr = device_->CreateBuffer(&bd, nullptr, &staging_[i]); + if (FAILED(hr)) { + LOG() << "AutoDepth: CreateBuffer(staging[" << i << "]) hr=0x" << std::hex << hr; + init_failed_ = true; + return false; + } + } + + if (!params_cb_) { + D3D11_BUFFER_DESC bd{}; + bd.ByteWidth = sizeof(AutoDepthParams); + bd.Usage = D3D11_USAGE_DYNAMIC; + bd.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + HRESULT hr = device_->CreateBuffer(&bd, nullptr, ¶ms_cb_); + if (FAILED(hr)) { + LOG() << "AutoDepth: CreateBuffer(params) hr=0x" << std::hex << hr; + init_failed_ = true; + return false; + } + } + + return sbs_srv_ != nullptr; +} + + +void AutoDepthAnalyzer::Run(ID3D11DeviceContext* ctx, + ID3D11Texture2D* sbs, + uint32_t sbs_w, + uint32_t sbs_h, + uint64_t frame_counter, + StereoDisplayComponent* component) +{ + if (!ctx || !sbs || !component) return; + if (sbs_w == 0 || sbs_h == 0) return; + if (!EnsureResources(sbs)) return; + + const uint32_t eye_w = sbs_w / 2; + const uint32_t search_radius = (std::max)(eye_w / 4u, 16u); + // Disparity step / decimation stride: 4px (quarter-res) up to ~2K-wide + // eyes, growing with the search radius beyond that so the 128-bucket + // histogram always spans the full radius — previously anything past + // 508px of disparity silently piled into the top bucket and the loop + // under-corrected exactly when objects were closest. + const uint32_t kStride = (std::max)( + 4u, (search_radius + NUM_BUCKETS - 1u) / NUM_BUCKETS); + + // Update params CB. + { + D3D11_MAPPED_SUBRESOURCE m{}; + if (FAILED(ctx->Map(params_cb_.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &m))) return; + AutoDepthParams* p = reinterpret_cast(m.pData); + p->sbs_w = sbs_w; + p->sbs_h = sbs_h; + p->search_radius = search_radius; + p->stride = kStride; + ctx->Unmap(params_cb_.Get(), 0); + } + + // Bind + clear + dispatch. + const UINT clear_val[4] = {0, 0, 0, 0}; + ctx->ClearUnorderedAccessViewUint(result_uav_.Get(), clear_val); + + ID3D11ShaderResourceView* srvs[1] = { sbs_srv_.Get() }; + ID3D11UnorderedAccessView* uavs[1] = { result_uav_.Get() }; + ID3D11Buffer* cbs[1] = { params_cb_.Get() }; + + ctx->CSSetShader(cs_.Get(), nullptr, 0); + ctx->CSSetShaderResources(0, 1, srvs); + ctx->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + ctx->CSSetConstantBuffers(0, 1, cbs); + + const uint32_t tx = (eye_w / kStride + 7) / 8; + const uint32_t ty = (sbs_h / kStride + 7) / 8; + ctx->Dispatch((std::max)(tx, 1u), (std::max)(ty, 1u), 1); + + // Unbind to avoid hazards with the OSD composite / presenter that follow. + ID3D11ShaderResourceView* null_srv[1] = { nullptr }; + ID3D11UnorderedAccessView* null_uav[1] = { nullptr }; + ctx->CSSetShaderResources(0, 1, null_srv); + ctx->CSSetUnorderedAccessViews(0, 1, null_uav, nullptr); + + // Copy result to this frame's staging slot. + const uint32_t cur = ring_idx_; + const uint32_t other = ring_idx_ ^ 1u; + ctx->CopyResource(staging_[cur].Get(), result_buf_.Get()); + inflight_frame_[cur] = frame_counter; + + // Try to read the previous frame's result without stalling. + if (inflight_frame_[other] != UINT64_MAX) { + D3D11_MAPPED_SUBRESOURCE m{}; + HRESULT hr = ctx->Map(staging_[other].Get(), 0, + D3D11_MAP_READ, D3D11_MAP_FLAG_DO_NOT_WAIT, &m); + if (SUCCEEDED(hr)) { + uint32_t hist[NUM_BUCKETS + 1] = {}; + if (m.pData) { + std::memcpy(hist, m.pData, sizeof(hist)); + } + ctx->Unmap(staging_[other].Get(), 0); + inflight_frame_[other] = UINT64_MAX; + + const uint32_t match_count = hist[NUM_BUCKETS]; + uint32_t max_disp = 0; + uint32_t top_bucket = 0; + // Compute the structural-residue "tail floor": average count + // across the top 12 buckets. Loop-boundary residue piles ~30-50 + // hits/bucket here every frame regardless of scene; a real near- + // object peak will tower well above this floor. + uint32_t tail_sum = 0; + for (int b = NUM_BUCKETS - 12; b < NUM_BUCKETS; ++b) tail_sum += hist[b]; + const uint32_t tail_floor = tail_sum / 12u; + + // 5-bucket window must clear several thresholds simultaneously + // (per-bucket densities identical to the original 3-bucket tune, + // rescaled by 5/3 — the wider window lets a genuine near-object + // cluster spread over 5-6 buckets qualify where 3 adjacent + // buckets alone couldn't): + // (a) absolute floor (250 hits) + // (b) ~5.6% of total + // (c) 5x mean per bucket + // (d) 4x the tail-floor window (rejects flat boundary residue + // while still allowing genuine high-disparity clusters + // that produce a clear peak above the residue baseline) + const uint32_t mean_per_bucket = match_count / NUM_BUCKETS; + const uint32_t min_window = (std::max)({ + 250u, + match_count / 18u, + 5u * 5u * mean_per_bucket, + 4u * 5u * tail_floor}); + for (int b = NUM_BUCKETS - 3; b >= 2; --b) { + const uint32_t window = hist[b - 2] + hist[b - 1] + hist[b] + + hist[b + 1] + hist[b + 2]; + if (window >= min_window) { + top_bucket = static_cast(b); + max_disp = top_bucket * kStride; + break; + } + } + + // Peak at the top of the histogram means matches at/beyond the + // search radius piled into the last bucket — the true disparity + // may be larger than reported. + const bool saturated = top_bucket >= NUM_BUCKETS - 3; + + component->FeedAutoDepthSample(max_disp, eye_w, kStride); + + if (component->IsAutoDepthLoggingEnabled() && + (frame_counter <= 5 || (frame_counter % 120 == 0))) { + LOG() << "AutoDepth: max_disp=" << max_disp + << " px (eye_w=" << eye_w + << ", search=" << search_radius + << ", stride=" << kStride + << ", bucket=" << top_bucket + << ", frac=" << (eye_w ? float(max_disp) / float(eye_w) : 0.0f) + << ", matches=" << match_count + << (saturated ? ", SATURATED" : "") + << ")"; + std::ostringstream ss; + ss << "AutoDepth hist:"; + for (int b = 0; b < NUM_BUCKETS - 8; b += 4) { + ss << " [" << b << "]=" << hist[b]; + } + for (int b = NUM_BUCKETS - 8; b < NUM_BUCKETS; ++b) { + ss << " [" << b << "]=" << hist[b]; + } + LOG() << ss.str(); + } + } + // DXGI_ERROR_WAS_STILL_DRAWING -> just try again next frame. + } + + ring_idx_ ^= 1u; +} + + +void AutoDepthAnalyzer::Shutdown() +{ + cs_.Reset(); + sbs_srv_.Reset(); + result_uav_.Reset(); + result_buf_.Reset(); + staging_[0].Reset(); + staging_[1].Reset(); + params_cb_.Reset(); + device_.Reset(); + srv_source_ = nullptr; + inflight_frame_[0] = inflight_frame_[1] = UINT64_MAX; + ring_idx_ = 0; + init_failed_ = false; +} + +} // namespace vrto3d diff --git a/vrto3d/src/auto_depth.h b/vrto3d/src/auto_depth.h new file mode 100644 index 00000000..c2095d33 --- /dev/null +++ b/vrto3d/src/auto_depth.h @@ -0,0 +1,79 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include + +#include +#include + +class StereoDisplayComponent; + +namespace vrto3d { + +// Disparity-search compute pipeline driving auto-depth. +// +// Lifetime: created once, lazy-initialized on first Run() call (so the D3D +// device is already alive). Resources keyed to a specific source SbS texture +// are rebuilt automatically whenever Run() sees a new pointer. +// +// Thread-affinity: all methods must be called on the renderer's window +// thread (the same one that owns the immediate context). +class AutoDepthAnalyzer { +public: + AutoDepthAnalyzer() = default; + ~AutoDepthAnalyzer() = default; + + // Stash the device. Resources are not built until the first Run(). + void Init(ID3D11Device* device); + + // Dispatch the disparity search on `sbs` (2W x H side-by-side, full + // SbS dimensions in `sbs_w`/`sbs_h`), copy the histogram to a staging + // ring slot, and read back the previous frame's slot without stalling. + // Feeds the result into component->FeedAutoDepthSample. `frame_counter` + // is used solely for diagnostic logging cadence. + void Run(ID3D11DeviceContext* ctx, + ID3D11Texture2D* sbs, + uint32_t sbs_w, + uint32_t sbs_h, + uint64_t frame_counter, + StereoDisplayComponent* component); + + // Release every D3D resource. Safe to call multiple times. + void Shutdown(); + +private: + bool EnsureResources(ID3D11Texture2D* sbs); + + Microsoft::WRL::ComPtr device_; + Microsoft::WRL::ComPtr cs_; + Microsoft::WRL::ComPtr sbs_srv_; + Microsoft::WRL::ComPtr result_buf_; + Microsoft::WRL::ComPtr result_uav_; + Microsoft::WRL::ComPtr staging_[2]; + Microsoft::WRL::ComPtr params_cb_; + + // Source texture the SRV is bound to. When this changes (e.g. resize), + // the SRV is rebuilt. + ID3D11Texture2D* srv_source_ = nullptr; + + uint64_t inflight_frame_[2] = { UINT64_MAX, UINT64_MAX }; + uint32_t ring_idx_ = 0; + bool init_failed_ = false; +}; + +} // namespace vrto3d diff --git a/vrto3d/src/device_provider.cpp b/vrto3d/src/device_provider.cpp index 5a7e606e..1ecd0b7f 100644 --- a/vrto3d/src/device_provider.cpp +++ b/vrto3d/src/device_provider.cpp @@ -16,13 +16,53 @@ */ #define WIN32_LEAN_AND_MEAN -#include +#include +#include +#ifdef _WIN32 #include #include #include #include "vrto3dlib/win32_helper.hpp" #include "device_provider.h" +#include "dx11_renderer.h" +#include "direct_mode_component.h" +#include "presenter/nvstereo_dx9_presenter.h" +#else +#include +#include +#include +#include + +#include "vrto3dlib/linux_helper.hpp" +#include "device_provider.h" +#include "vk/vk_renderer.h" +#include "vk/direct_mode_component_vk.h" +#endif +#include "vr_recenter.h" + +namespace { + +// Sample the PID a few seconds after disconnect — most games take a moment +// to fully exit after their SteamVR connection drops. If the PID is gone, +// shut SteamVR down; otherwise the user still has the app running and we +// leave SteamVR alone. +void ScheduleAutoExitCheck(uint32_t pid) +{ + std::thread([pid]() { + std::this_thread::sleep_for(std::chrono::seconds(5)); + if (IsProcessRunning(pid)) { + LOG() << "auto_exit: pid " << pid + << " still running after 5s, leaving SteamVR alone"; + return; + } + LOG() << "auto_exit: pid " << pid + << " is gone, shutting down SteamVR"; + RequestSteamVRShutdown(); + }).detach(); +} + +} // namespace //----------------------------------------------------------------------------- // Purpose: This is called by vrserver after it receives a pointer back from HmdDriverFactory. @@ -30,8 +70,33 @@ //----------------------------------------------------------------------------- vr::EVRInitError MyDeviceProvider::Init( vr::IVRDriverContext *pDriverContext ) { +#ifdef _WIN32 global_mtx_ = CreateMutex(NULL, TRUE, kVRto3DMutexName); + // Best-effort process-level DPI elevation. Usually fails because vrserver + // already locked its awareness, in which case the per-thread elevation in + // LeiaSrPresenter::WindowThreadLoop and the WM_DPICHANGED handler in + // PresentWndProc cover us. + using SetProcessDpiAwarenessContextFn = BOOL (WINAPI*)(DPI_AWARENESS_CONTEXT); + if (HMODULE user32 = GetModuleHandleW(L"user32.dll")) { + if (auto set_proc_ctx = reinterpret_cast( + GetProcAddress(user32, "SetProcessDpiAwarenessContext"))) { + set_proc_ctx(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + } + } +#else + // Single-instance guard: flock'd file in XDG_RUNTIME_DIR (mirrors the + // Global\VRto3DDriver named mutex on Windows). + { + const char* run = getenv("XDG_RUNTIME_DIR"); + std::string lock_path = std::string(run ? run : "/tmp") + "/" + kVRto3DLockName; + int fd = open(lock_path.c_str(), O_CREAT | O_RDWR | O_CLOEXEC, 0600); + if (fd >= 0) + flock(fd, LOCK_EX | LOCK_NB); // advisory; keep fd for process lifetime + global_mtx_ = (void*)(intptr_t)fd; + } +#endif + // We need to initialise our driver context to make calls to the server. // OpenVR provides a macro to do this for us. VR_INIT_SERVER_DRIVER_CONTEXT( pDriverContext ); @@ -39,13 +104,19 @@ vr::EVRInitError MyDeviceProvider::Init( vr::IVRDriverContext *pDriverContext ) // First, initialize our hmd, which we'll later pass OpenVR a pointer to. my_hmd_device_ = std::make_unique< MockControllerDeviceDriver >(); + static const char kSerialNumber[] = "VRto3D-1234"; + // TrackedDeviceAdded returning true means we have had our device added to SteamVR. - if (!vr::VRServerDriverHost()->TrackedDeviceAdded("VRto3D-1234", vr::TrackedDeviceClass_HMD, my_hmd_device_.get())) + if (!vr::VRServerDriverHost()->TrackedDeviceAdded(kSerialNumber, vr::TrackedDeviceClass_HMD, my_hmd_device_.get())) { LOG() << "Failed to create hmd device!"; return vr::VRInitError_Driver_Unknown; } + // No DisplayRedirect sibling needed in direct mode — game eye textures + // arrive via IVRDriverDirectModeComponent::SubmitLayer / Present rather + // than IVRVirtualDisplay::Present. + return vr::VRInitError_None; } @@ -81,27 +152,54 @@ void MyDeviceProvider::RunFrame() vr::VREvent_t vrEvent; while (vr::VRServerDriverHost()->PollNextEvent(&vrEvent, sizeof(vrEvent))) { - if ((vrEvent.eventType == vr::VREvent_ProcessConnected || + // Connect-side events are NOT gated by wait_count_. When a mod (e.g. + // RealVR) re-inits VR after an FoV/resolution change, the reconnect + // event lands inside the post-disconnect cool-off window; dropping it + // would leave the renderer stuck in paused_for_disconnect_ with no + // further chance to resume. + if (vrEvent.eventType == vr::VREvent_ProcessConnected || vrEvent.eventType == vr::VREvent_ActionBindingReloaded || vrEvent.eventType == vr::VREvent_SceneApplicationChanged || - vrEvent.eventType == vr::VREvent_Input_BindingLoadFailed || + vrEvent.eventType == vr::VREvent_Input_BindingLoadFailed || vrEvent.eventType == vr::VREvent_Input_BindingLoadSuccessful || - vrEvent.eventType == vr::VREvent_Input_ActionManifestReloaded) && - wait_count_ == 0) + vrEvent.eventType == vr::VREvent_Input_ActionManifestReloaded) { auto appName = GetProcessName(vrEvent.data.process.pid); auto lowerAppName = appName; std::transform(lowerAppName.begin(), lowerAppName.end(), lowerAppName.begin(), ::tolower); - if (Skip_Processes.find(appName) == Skip_Processes.end() && - lowerAppName.find("exe") != std::string::npos) +#ifdef _WIN32 + const bool name_is_game = lowerAppName.find("exe") != std::string::npos; +#else + // Native Linux games have no .exe suffix; Proton games keep it. + const bool name_is_game = !lowerAppName.empty(); +#endif + const bool skipped = Skip_Processes.find(lowerAppName) != Skip_Processes.end(); + if (!skipped && name_is_game) { app_name_ = appName; app_pid_ = vrEvent.data.process.pid; - LOG() << "AppName = " << app_name_.c_str(); + g_current_app_pid.store(app_pid_); + LOG() << "AppName = " << app_name_.c_str() + << " (evt=" << (int)vrEvent.eventType + << " pid=" << vrEvent.data.process.pid << ")"; my_hmd_device_->LoadSettings(app_name_, app_pid_, vr::VREvent_ProcessConnected); + // Resume the renderer (cleared by the prior ProcessDisconnected). + if (auto* r = my_hmd_device_->GetRenderer()) r->OnAppConnect(); wait_count_ = 500; } + else if (vrEvent.eventType == vr::VREvent_ProcessConnected || + vrEvent.eventType == vr::VREvent_SceneApplicationChanged) + { + // Not treated as a game connect. Logged so we can see what + // becomes the scene app after a real game exits (e.g. SteamVR + // Home / environments) — a spurious auto-raise there is the + // suspected "stayed on top after quitting the game" cause. + LOG() << "connect ignored: name='" << appName.c_str() + << "' pid=" << vrEvent.data.process.pid + << " evt=" << (int)vrEvent.eventType + << " skip=" << skipped << " is_game=" << name_is_game; + } } else if ((vrEvent.eventType == vr::VREvent_ProcessDisconnected || vrEvent.eventType == vr::VREvent_Compositor_ApplicationNotResponding || @@ -109,12 +207,45 @@ void MyDeviceProvider::RunFrame() !app_name_.empty() && vrEvent.data.process.pid == app_pid_ && wait_count_ == 0) { LOG() << "Unload = " << app_name_.c_str(); + // Sample auto_exit + pid before LoadSettings / app_pid_ reset. + // The actual liveness check + shutdown happens on a delayed + // thread (ScheduleAutoExitCheck) so the game has time to fully + // terminate after the SteamVR disconnect fires. + const bool want_auto_exit = + my_hmd_device_->GetStereoComponent() && + my_hmd_device_->GetStereoComponent()->GetConfig().auto_exit; + const uint32_t pid_snapshot = app_pid_; my_hmd_device_->LoadSettings(app_name_, app_pid_, vr::VREvent_ProcessDisconnected); + + // Drop the departed game's swap-texture handles BEFORE we + // continue submitting frames. The compositor doesn't always call + // DestroySwapTextureSet on app exit, leaving stale entries in + // handle_map_ that the renderer would try to import — and that + // import was the trigger for the DEVICE_REMOVED cascade observed + // in crash logs. Pause the renderer so it stops trying until the + // next ProcessConnected. + if (auto* dmc = my_hmd_device_->GetDirectModeComponent()) { + dmc->DestroyAllSwapTextureSets(pid_snapshot); + } + if (auto* r = my_hmd_device_->GetRenderer()) r->OnAppDisconnect(); + app_name_ = ""; app_pid_ = 0; + g_current_app_pid.store(0); wait_count_ = 500; + if (want_auto_exit) { + LOG() << "auto_exit: scheduling check for pid " << pid_snapshot; + ScheduleAutoExitCheck(pid_snapshot); + } } } + + // VREvent_Quit isn't delivered via the driver-host event stream — it + // goes to *client* apps over their IPC pipe. Our TriggerOpenVRRecenter + // VR_Init registers us as a Background client, so we have to drain + // that client-side queue and ack quit, or vrserver waits 5s for us + // and force-kills the process. + vrto3d::PumpOpenVRClientEvents(); } //----------------------------------------------------------------------------- @@ -140,12 +271,41 @@ void MyDeviceProvider::LeaveStandby() //----------------------------------------------------------------------------- void MyDeviceProvider::Cleanup() { +#ifdef _WIN32 if (global_mtx_) { CloseHandle((HANDLE)global_mtx_); } +#else + if ((intptr_t)global_mtx_ >= 0) + { + close((int)(intptr_t)global_mtx_); + } +#endif // Our controller devices will have already deactivated. Let's now destroy them. my_hmd_device_ = nullptr; + + // Tear down the background OpenVR client session we used for chaperone + // recenters. vrserver flags an unclean exit if a VR_Init'd client never + // VR_Shutdown's. This is the last hook called before our DLL unloads. + vrto3d::ShutdownOpenVRClient(); + +#ifdef _WIN32 + // NV3D-Glass exit pattern: when the NVIDIA 3D Vision presenter ran this + // session, hard-kill the process now that our own teardown is complete. + // A hard display freeze (reboot required) has been observed AFTER a clean + // explicit teardown — inside process exit, where nvd3dumx.dll / + // nvapi64.dll / d3d9.dll run their DLL_PROCESS_DETACH against the driver + // stereo state the FSE teardown just strained. TerminateProcess skips CRT + // static destructors and all DLL_PROCESS_DETACH (ExitProcess would not) + // and never returns on success. Every other output mode keeps the normal + // exit path. + if (vrto3d::NvStereoWasActiveThisSession()) { + LOG() << "MyDeviceProvider::Cleanup: NV3D session — TerminateProcess " + "to skip NVIDIA DLL_PROCESS_DETACH"; + TerminateProcess(GetCurrentProcess(), 0); + } +#endif } diff --git a/vrto3d/src/direct_mode_component.cpp b/vrto3d/src/direct_mode_component.cpp new file mode 100644 index 00000000..a150aa8e --- /dev/null +++ b/vrto3d/src/direct_mode_component.cpp @@ -0,0 +1,417 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ + +#include "direct_mode_component.h" + +#include +#include +#include +#include + +#include + +#include "dx11_renderer.h" +#include "vrto3dlib/debug_log.hpp" +#include "vrto3dlib/win32_helper.hpp" + + +DirectModeComponent::DirectModeComponent(Dx11Renderer* renderer) + : renderer_(renderer) +{} + + +DirectModeComponent::~DirectModeComponent() +{ + std::lock_guard lk(sets_mutex_); + // Each TextureSet shows up three times in handle_map_; delete each unique + // pointer once. + std::set deleted; + for (auto& kv : handle_map_) { + if (deleted.insert(kv.second.first).second) { + delete kv.second.first; + } + } + handle_map_.clear(); +} + + +void DirectModeComponent::CreateSwapTextureSet(uint32_t unPid, + const SwapTextureSetDesc_t* pDesc, + SwapTextureSet_t* pOut) +{ + if (!pDesc || !pOut || !renderer_ || !renderer_->Device()) return; + + // Once the DX11 device is terminally dead, every CreateTexture2D below + // fails with DXGI_ERROR_DEVICE_REMOVED (0x887a0005). SteamVR retries + // CreateSwapTextureSet on each failure, which becomes a hot spam loop + // that has been observed to wedge vrcompositor and bring SteamVR down. + // Refuse early so the swap-texture set comes back empty and the caller + // eventually surfaces the failure instead of pounding the dead device. + if (renderer_->IsDeviceDead()) { + static std::atomic logged{false}; + bool e = false; + if (logged.compare_exchange_strong(e, true)) { + LOG() << "DirectModeComponent::CreateSwapTextureSet pid=" << unPid + << " refused — DX11 device is dead, SteamVR restart required"; + } + return; + } + + LOG() << "DirectModeComponent::CreateSwapTextureSet pid=" << unPid + << " " << pDesc->nWidth << "x" << pDesc->nHeight + << " fmt=" << pDesc->nFormat + << " samples=" << pDesc->nSampleCount; + + D3D11_TEXTURE2D_DESC td{}; + td.Width = pDesc->nWidth; + td.Height = pDesc->nHeight; + td.MipLevels = 1; + td.ArraySize = 1; + td.Format = static_cast(pDesc->nFormat); + td.SampleDesc.Count = pDesc->nSampleCount == 0 ? 1 : pDesc->nSampleCount; + td.Usage = D3D11_USAGE_DEFAULT; + td.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; + // Legacy shared handle (matches ALVR's working path; the NTHANDLE + + // keyed-mutex path is documented but not exercised here). + td.MiscFlags = D3D11_RESOURCE_MISC_SHARED; + + auto* set = new TextureSet(); + set->pid = unPid; + + bool ok = true; + for (int i = 0; i < 3; ++i) { + HRESULT hr = renderer_->Device()->CreateTexture2D(&td, nullptr, &set->textures[i]); + if (FAILED(hr) || !set->textures[i]) { + LOG() << "DirectModeComponent: CreateTexture2D[" << i << "] failed hr=0x" + << std::hex << hr; + // If the failure is a terminal DXGI device state, latch the + // renderer so subsequent CreateSwapTextureSet calls bail at the + // top instead of spam-failing. + (void)renderer_->MarkDeviceDeadIfTerminal(hr, "DirectModeComponent::CreateTexture2D"); + ok = false; + break; + } + + Microsoft::WRL::ComPtr dxgi_res; + hr = set->textures[i].As(&dxgi_res); + if (FAILED(hr) || !dxgi_res) { + LOG() << "DirectModeComponent: QueryInterface(IDXGIResource)[" << i + << "] failed hr=0x" << std::hex << hr; + ok = false; + break; + } + + hr = dxgi_res->GetSharedHandle(&set->handles[i]); + if (FAILED(hr) || !set->handles[i]) { + LOG() << "DirectModeComponent: GetSharedHandle[" << i << "] failed hr=0x" + << std::hex << hr; + ok = false; + break; + } + + pOut->rSharedTextureHandles[i] = + reinterpret_cast(set->handles[i]); + } + + if (!ok) { + delete set; + for (int i = 0; i < 3; ++i) pOut->rSharedTextureHandles[i] = 0; + pOut->unTextureFlags = 0; + return; + } + + // unTextureFlags = 0 — we use legacy shared handles, not NT handles. + pOut->unTextureFlags = 0; + + { + std::lock_guard lk(sets_mutex_); + for (int i = 0; i < 3; ++i) { + handle_map_.emplace(set->handles[i], std::make_pair(set, i)); + } + } +} + + +void DirectModeComponent::DestroySwapTextureSet(vr::SharedTextureHandle_t sharedTextureHandle) +{ + auto raw = reinterpret_cast(static_cast(sharedTextureHandle)); + std::lock_guard lk(sets_mutex_); + auto it = handle_map_.find(raw); + if (it == handle_map_.end()) return; + TextureSet* set = it->second.first; + for (HANDLE h : set->handles) handle_map_.erase(h); + delete set; +} + + +void DirectModeComponent::DestroyAllSwapTextureSets(uint32_t unPid) +{ + std::lock_guard lk(sets_mutex_); + // Collect unique sets owned by this pid, delete them, then prune the map. + std::set to_delete; + for (auto& kv : handle_map_) { + if (kv.second.first->pid == unPid) to_delete.insert(kv.second.first); + } + for (TextureSet* s : to_delete) { + for (HANDLE h : s->handles) handle_map_.erase(h); + delete s; + } +} + + +void DirectModeComponent::GetNextSwapTextureSetIndex(vr::SharedTextureHandle_t /*handles*/[2], + uint32_t (*pIndices)[2]) +{ + (*pIndices)[0] = ((*pIndices)[0] + 1) % 3; + (*pIndices)[1] = ((*pIndices)[1] + 1) % 3; +} + + +void DirectModeComponent::SubmitLayer(const SubmitLayerPerEye_t (&perEye)[2]) +{ + std::lock_guard lk(submit_mutex_); + if (submit_layer_count_ < kMaxLayers) { + submit_layers_[submit_layer_count_][0] = perEye[0]; + submit_layers_[submit_layer_count_][1] = perEye[1]; + } + // Always increment so the logged count reflects what the compositor + // submits even when it exceeds kMaxLayers. + ++submit_layer_count_; +} + + +void DirectModeComponent::Present(vr::SharedTextureHandle_t syncTexture) +{ + if (!renderer_) return; + + SubmitLayerPerEye_t local[kMaxLayers][2]{}; + int submitted_count = 0; + int kept_count = 0; + { + std::lock_guard lk(submit_mutex_); + submitted_count = submit_layer_count_; + submit_layer_count_ = 0; + if (submitted_count == 0) { + if (last_logged_submit_count_ != 0) { + LOG() << "DirectModeComponent: Present with no SubmitLayer " + "(was " << last_logged_submit_count_ << ")"; + last_logged_submit_count_ = 0; + } + return; + } + kept_count = submitted_count > kMaxLayers ? kMaxLayers : submitted_count; + for (int i = 0; i < kept_count; ++i) { + local[i][0] = submit_layers_[i][0]; + local[i][1] = submit_layers_[i][1]; + } + } + + if (submitted_count != last_logged_submit_count_) { + LOG() << "DirectModeComponent: SubmitLayer calls per Present = " + << submitted_count + << " (was " << last_logged_submit_count_ << ")"; + last_logged_submit_count_ = submitted_count; + } + + // Resolve each layer's eye handles back to the textures we allocated in + // CreateSwapTextureSet, dropping placeholder/marker quads as we go. The + // resolve loop also compacts local[] in lockstep with pairs[] so the + // sort below can permute them together by a single index. + // + // Placeholder filter: UEVR mods can submit a 2x4 or 16x16 marker on the + // game's pid alongside the real scene that flows through vrcompositor's + // full-eye target. If we kept the marker it would either (a) sort to + // index 0 under the pid-first rule and shrink out_sbs_ to ~4x4, breaking + // the picture, or (b) end up as a stretched overlay smearing whatever + // garbage the placeholder holds across the full eye. Anything below + // kMinSceneAreaPx effective pixels can't carry visual content; drop it. + constexpr uint64_t kMinSceneAreaPx = 64ull * 64ull; + Dx11Renderer::DirectModeLayerPair pairs[kMaxLayers]{}; + int resolved_count = 0; + int filtered_tiny = 0; + { + std::lock_guard lk(sets_mutex_); + auto find_one = [&](vr::SharedTextureHandle_t h, uint32_t* out_pid) + -> Microsoft::WRL::ComPtr + { + auto raw = reinterpret_cast(static_cast(h)); + auto it = handle_map_.find(raw); + if (it == handle_map_.end()) return {}; + if (out_pid) *out_pid = it->second.first->pid; + return it->second.first->textures[it->second.second]; + }; + for (int i = 0; i < kept_count; ++i) { + uint32_t pid_l = 0, pid_r = 0; + Microsoft::WRL::ComPtr left = find_one(local[i][0].hTexture, &pid_l); + Microsoft::WRL::ComPtr right = find_one(local[i][1].hTexture, &pid_r); + if (!left || !right) { + ++handle_miss_count_; + continue; + } + D3D11_TEXTURE2D_DESC ld{}; + left->GetDesc(&ld); + const float du = std::abs(local[i][0].bounds.uMax - local[i][0].bounds.uMin); + const float dv = std::abs(local[i][0].bounds.vMax - local[i][0].bounds.vMin); + const uint64_t eff_px = + static_cast(static_cast(ld.Width) * du + 0.5) * + static_cast(static_cast(ld.Height) * dv + 0.5); + if (eff_px < kMinSceneAreaPx) { + ++filtered_tiny; + continue; + } + pairs[resolved_count].left = std::move(left); + pairs[resolved_count].right = std::move(right); + pairs[resolved_count].bounds_left = local[i][0].bounds; + pairs[resolved_count].bounds_right = local[i][1].bounds; + pairs[resolved_count].pid = pid_l; + if (i != resolved_count) { + local[resolved_count][0] = local[i][0]; + local[resolved_count][1] = local[i][1]; + } + ++resolved_count; + } + } + if (filtered_tiny > 0 && filtered_tiny != last_logged_filtered_tiny_) { + LOG() << "DirectModeComponent: filtered " << filtered_tiny + << " sub-threshold layer(s) (<" << kMinSceneAreaPx << "px)"; + last_logged_filtered_tiny_ = filtered_tiny; + } else if (filtered_tiny == 0 && last_logged_filtered_tiny_ != 0) { + last_logged_filtered_tiny_ = 0; + } + + // Reorder layers so OnDirectModeFrame's "layers[0] is the opaque base, + // layers[1+] composite on top" assumption holds across mods. + // + // The resolve loop above already dropped sub-threshold placeholder + // quads, so every remaining layer is real, scene-or-overlay-sized + // content. Two-tier sort over what's left: + // 1. Layers owned by the game process come before layers owned by + // vrcompositor.exe. This catches Luke Ross R.E.A.L., which submits + // the actual 3D scene from the game's pid (a smaller texture) but + // routes its in-game UI / mod menu through a larger vrcompositor- + // owned overlay layer. Without this, the area heuristic alone + // picked the UI layer as base and the game scene (alpha=1 across + // the surface) drew over it, hiding the UI completely. + // 2. Within the same group, larger effective area (texture dims + // scaled by submitted bounds rect) wins. This handles UEVR mods + // that submit both scene and a same-pid HUD quad: the full scene + // beats the smaller HUD. It also handles bounds-shrunk + // allocations (e.g. a 2592x1458 texture with bounds=(0,0,0.741, + // 0.741) rendered into the top-left 1920x1080) — raw + // width*height would mis-prefer a smaller full-bounds layer + // like SteamVR Home. + if (resolved_count > 1) { + auto effective_area = [](const Dx11Renderer::DirectModeLayerPair& p) -> uint64_t { + D3D11_TEXTURE2D_DESC d{}; + p.left->GetDesc(&d); + const float du = std::abs(p.bounds_left.uMax - p.bounds_left.uMin); + const float dv = std::abs(p.bounds_left.vMax - p.bounds_left.vMin); + const double eff_w = static_cast(d.Width) * du; + const double eff_h = static_cast(d.Height) * dv; + if (eff_w <= 0.0 || eff_h <= 0.0) return 0; + return static_cast(eff_w * eff_h); + }; + + struct LayerKey { + int original_index; + bool is_compositor; + uint64_t eff_area; + }; + LayerKey keys[kMaxLayers]; + for (int i = 0; i < resolved_count; ++i) { + auto it = pid_name_cache_.find(pairs[i].pid); + if (it == pid_name_cache_.end()) { + it = pid_name_cache_.emplace(pairs[i].pid, GetProcessName(pairs[i].pid)).first; + } + keys[i].original_index = i; + keys[i].is_compositor = _stricmp(it->second.c_str(), "vrcompositor.exe") == 0; + keys[i].eff_area = effective_area(pairs[i]); + } + + int order[kMaxLayers]; + for (int i = 0; i < resolved_count; ++i) order[i] = i; + std::stable_sort(order, order + resolved_count, + [&](int a, int b) { + if (keys[a].is_compositor != keys[b].is_compositor) { + return !keys[a].is_compositor; + } + return keys[a].eff_area > keys[b].eff_area; + }); + + bool needs_reorder = false; + for (int i = 0; i < resolved_count; ++i) { + if (order[i] != i) { needs_reorder = true; break; } + } + if (needs_reorder) { + Dx11Renderer::DirectModeLayerPair sorted_pairs[kMaxLayers]{}; + SubmitLayerPerEye_t sorted_local[kMaxLayers][2]{}; + for (int i = 0; i < resolved_count; ++i) { + sorted_pairs[i] = std::move(pairs[order[i]]); + sorted_local[i][0] = local[order[i]][0]; + sorted_local[i][1] = local[order[i]][1]; + } + for (int i = 0; i < resolved_count; ++i) { + pairs[i] = std::move(sorted_pairs[i]); + local[i][0] = sorted_local[i][0]; + local[i][1] = sorted_local[i][1]; + } + } + } + + // Per-layer log: emitted once per change in resolved layer count. + // Useful when adding a new mod/game combo — pid + process name lets + // us sanity-check the post-reorder order (game-pid layer should be + // at index 0, vrcompositor-pid layers should follow). + if (resolved_count != last_logged_layer_dim_count_) { + last_logged_layer_dim_count_ = resolved_count; + for (int i = 0; i < resolved_count; ++i) { + D3D11_TEXTURE2D_DESC ld{}, rd{}; + pairs[i].left->GetDesc(&ld); + pairs[i].right->GetDesc(&rd); + + const uint32_t pid = pairs[i].pid; + auto it = pid_name_cache_.find(pid); + if (it == pid_name_cache_.end()) { + it = pid_name_cache_.emplace(pid, GetProcessName(pid)).first; + } + const std::string& pname = it->second; + + LOG() << "DirectModeComponent: layer " << i + << " pid=" << pid << "(" << pname.c_str() << ")" + << " dims L=" << ld.Width << "x" << ld.Height + << " R=" << rd.Width << "x" << rd.Height + << " fmt=" << ld.Format + << " bounds L=(" << local[i][0].bounds.uMin << "," << local[i][0].bounds.vMin + << "," << local[i][0].bounds.uMax << "," << local[i][0].bounds.vMax << ")"; + } + } + + if (resolved_count == 0) { + if (handle_miss_count_ == 1 || (handle_miss_count_ % 60) == 0) { + LOG() << "DirectModeComponent::Present: no submitted layers resolved " + "to handle_map_ entries (misses=" << handle_miss_count_ << ")"; + } + return; + } + if (handle_miss_count_ > 0) { + LOG() << "DirectModeComponent::Present: handle lookups recovered after " + << handle_miss_count_ << " misses"; + handle_miss_count_ = 0; + } + + renderer_->OnDirectModeFrame(pairs, resolved_count, syncTexture); +} diff --git a/vrto3d/src/direct_mode_component.h b/vrto3d/src/direct_mode_component.h new file mode 100644 index 00000000..a020c2d9 --- /dev/null +++ b/vrto3d/src/direct_mode_component.h @@ -0,0 +1,108 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include "openvr_driver.h" + +class Dx11Renderer; + + +// IVRDriverDirectModeComponent implementation. In direct mode SteamVR's +// compositor never runs its lens-distortion / panel-mask render pass — game +// eye textures flow into us through SubmitLayer as shared DXGI handles to +// textures we ourselves allocated in CreateSwapTextureSet. Present hands the +// pair off to the Dx11Renderer's window thread, which composes them into the +// existing SbS scratch buffer and feeds the rest of the pipeline (OSD, +// auto-depth, presenter, screenshots) unchanged. +class DirectModeComponent : public vr::IVRDriverDirectModeComponent { +public: + explicit DirectModeComponent(Dx11Renderer* renderer); + ~DirectModeComponent(); + + // ----- vr::IVRDriverDirectModeComponent overrides ----- + + void CreateSwapTextureSet(uint32_t unPid, + const SwapTextureSetDesc_t* pSwapTextureSetDesc, + SwapTextureSet_t* pOutSwapTextureSet) override; + + void DestroySwapTextureSet(vr::SharedTextureHandle_t sharedTextureHandle) override; + + void DestroyAllSwapTextureSets(uint32_t unPid) override; + + void GetNextSwapTextureSetIndex(vr::SharedTextureHandle_t sharedTextureHandles[2], + uint32_t (*pIndices)[2]) override; + + void SubmitLayer(const SubmitLayerPerEye_t (&perEye)[2]) override; + + void Present(vr::SharedTextureHandle_t syncTexture) override; + +private: + Dx11Renderer* renderer_; + + // Each application gets three textures per eye allocated on our device + // and shared via legacy DXGI shared handles (matches ALVR's pattern; the + // NT-handle path is not exercised here). We map every handle back to its + // owning TextureSet so SubmitLayer / Present can resolve the texture + // pointer and so DestroySwapTextureSet can wipe all three handles when + // any one of them is destroyed. + struct TextureSet { + Microsoft::WRL::ComPtr textures[3]; + HANDLE handles[3]{}; + uint32_t pid = 0; + }; + std::mutex sets_mutex_; + std::map> handle_map_; + + // Per-frame layer accumulation. Apps (especially UEVR-driven games) + // submit multiple layers per frame — typically layer 0 is the base scene + // and layers 1+ are HUD / overlay quads that alpha-blend on top. We keep + // up to kMaxLayers of them and the renderer composites in order. + static constexpr int kMaxLayers = 8; + std::mutex submit_mutex_; + SubmitLayerPerEye_t submit_layers_[kMaxLayers][2]{}; + int submit_layer_count_ = 0; + + // SubmitLayer-call counter — logged once per change. Tracked separately + // from submit_layer_count_ because the count we observe may exceed + // kMaxLayers; we want to know when that happens. + int last_logged_submit_count_ = -1; + + // Running count of handle-map misses (submitted hTexture not in + // handle_map_). Logged on first miss + every 60th to detect persistence. + uint64_t handle_miss_count_ = 0; + + // Track which submit-count we last logged per-layer dimensions for, so + // dimension lines aren't spammed every frame. + int last_logged_layer_dim_count_ = -1; + + // Last-logged count of sub-threshold layers dropped by the placeholder + // filter — emitted once per change so we know UEVR's marker quads are + // being suppressed correctly. + int last_logged_filtered_tiny_ = 0; + + // pid → executable name cache for the per-layer diagnostic log. Avoids + // an OpenProcess call every time the layer-count changes. + std::unordered_map pid_name_cache_; +}; diff --git a/vrto3d/src/dx11_renderer.cpp b/vrto3d/src/dx11_renderer.cpp new file mode 100644 index 00000000..ed5d7132 --- /dev/null +++ b/vrto3d/src/dx11_renderer.cpp @@ -0,0 +1,1408 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ + +#include "dx11_renderer.h" + +#define WIN32_LEAN_AND_MEAN +#include +#include +#pragma comment(lib, "winmm.lib") + +#include + +#include +#include +#include +#include + +#include +#pragma comment(lib, "d3dcompiler.lib") + +#include "vrto3dlib/debug_log.hpp" + +#include "auto_depth.h" +#include "hmd_device_driver.h" // StereoDisplayComponent::GetConfig() +#include "osd/osd_renderer.h" +#include "osd/osd_menu.h" +#include "screenshot.h" + + +Dx11Renderer::Dx11Renderer() +{ + LARGE_INTEGER f{}; + if (QueryPerformanceFrequency(&f) && f.QuadPart > 0) { + qpc_freq_sec_ = 1.0 / static_cast(f.QuadPart); + } +} + +Dx11Renderer::~Dx11Renderer() { Shutdown(); } + + +namespace { + +// Fullscreen-triangle vertex shader. The pixel shader samples its SRV and +// outputs the texel as-is (re-encoded to sRGB to match layer 0's byte- +// passthrough convention). Blend state is straight alpha (SRC_ALPHA / +// INV_SRC_ALPHA) — matches ALVR's overlay-layer convention. +const char* kCompositeVsHlsl = R"hlsl( +struct VsOut { + float4 pos : SV_Position; + float2 uv : TEXCOORD0; +}; +VsOut main(uint id : SV_VertexID) { + VsOut o; + float2 uv = float2((id << 1) & 2, id & 2); + o.uv = uv; + o.pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); + return o; +} +)hlsl"; + +const char* kCompositePsHlsl = R"hlsl( +Texture2D layer_tex : register(t0); +SamplerState layer_smp : register(s0); +struct VsOut { + float4 pos : SV_Position; + float2 uv : TEXCOORD0; +}; +// Sampler auto-decodes the source's sRGB texels to linear; out_sbs_ is +// UNORM-typed (no encode on write), so we re-apply the sRGB curve here to +// keep the byte convention consistent with layer 0's CopySubresourceRegion. +// Piecewise sRGB encode rather than pow(1/2.2) so dark-near-zero pixels stay +// on the linear portion of the curve. +float3 linear_to_srgb(float3 c) { + float3 lo = 12.92f * c; + float3 hi = 1.055f * pow(max(c, 0.0f), 1.0f / 2.4f) - 0.055f; + // step(edge, x) = (x >= edge) ? 1 : 0 — picks hi when c >= threshold. + float3 mask = step(0.0031308f, c); + return lerp(lo, hi, mask); +} +float4 main(VsOut i) : SV_Target { + float4 c = layer_tex.Sample(layer_smp, i.uv); + c.rgb = linear_to_srgb(c.rgb); + return c; +} +)hlsl"; + +// --------------------------------------------------------------------------- +// Display-correction post-process pass — runs once on out_sbs_ (post-OSD, +// pre-presenter) and applies user-tunable Lift / Gamma / Gain + extended +// SCurve. Defaults all pass-through; the pass is skipped entirely when +// disabled. Operates in sRGB byte space (matches ReShade's default workflow +// and the LiftGammaGain.fx / SCurve.fx references the user supplied). +// --------------------------------------------------------------------------- +const char* kAdjustVsHlsl = R"hlsl( +struct VsOut { + float4 pos : SV_Position; + float2 uv : TEXCOORD0; +}; +VsOut main(uint id : SV_VertexID) { + VsOut o; + float2 uv = float2((id << 1) & 2, id & 2); + o.uv = uv; + o.pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); + return o; +} +)hlsl"; + +const char* kAdjustPsHlsl = R"hlsl( +Texture2D g_src : register(t0); +SamplerState g_smp : register(s0); +cbuffer Params : register(b0) { + float4 lift; // .rgb used + float4 gamma; // .rgb used + float4 gain; // .rgb used + float curve; // 1.0 = pass-through; <1 reduces, >1 increases midtone contrast + float off_low; + float off_high; + float off_both; +}; +struct VsOut { + float4 pos : SV_Position; + float2 uv : TEXCOORD0; +}; +float4 main(VsOut i) : SV_Target { + float3 col = g_src.Sample(g_smp, i.uv).rgb; + + // Extended SCurve.fx — at curve=1.0 the lerp degenerates to pass-through. + float3 low = pow(abs(col), curve) + off_low; + float3 high = pow(abs(col), 1.0 / curve) + off_high; + float3 t = saturate(col + off_both); + col = lerp(low, high, t); + + // LiftGammaGain.fx (operates in sRGB byte space — matches ReShade default). + col = col * (1.5 - 0.5 * lift.rgb) + 0.5 * lift.rgb - 0.5; + col = saturate(col); + col *= gain.rgb; + col = pow(abs(col), 1.0 / gamma.rgb); + + return float4(saturate(col), 1.0); +} +)hlsl"; + +// Constant buffer pushed to the shader each frame. 16-byte aligned to match +// the cbuffer layout (float4 + float4 + float4 + 4*float = 64 bytes). +struct AdjustCB { + float lift[4]; + float gamma[4]; + float gain[4]; + float curve; + float off_low; + float off_high; + float off_both; +}; +static_assert(sizeof(AdjustCB) == 64, "AdjustCB must stay 16-byte-aligned"); + + +bool CompileShaderBlob(const char* src, const char* entry, const char* profile, + Microsoft::WRL::ComPtr& out_blob) { + Microsoft::WRL::ComPtr err; + HRESULT hr = D3DCompile(src, std::strlen(src), nullptr, nullptr, nullptr, + entry, profile, 0, 0, &out_blob, &err); + if (FAILED(hr)) { + LOG() << "Dx11Renderer: composite shader compile (" << entry << "/" << profile + << ") failed hr=0x" << std::hex << hr; + if (err) LOG() << " err: " << static_cast(err->GetBufferPointer()); + return false; + } + return true; +} + +} // namespace + + +bool Dx11Renderer::Init(LUID adapter_luid, + const StereoDisplayDriverConfiguration& cfg, + const vrto3d::FocusContext& focus) +{ + if (!platform::CreateD3D11Device(adapter_luid, device_, context_, adapter_)) { + LOG() << "Dx11Renderer: CreateD3D11Device failed"; + return false; + } + + // Cap GPU frame latency to 1 so Present blocks the CPU as soon as the + // GPU falls a single frame behind. The default is 3, which lets us + // happily queue 3 frames of work ahead — fine when frame cost is + // uniform, but in direct-mode multi-layer composite the cost varies + // with content and the queue piles up over a session. Without this + // clamp the frame-interval histogram drifts from a tight 8ms cluster + // to a peak at 20-26ms, which the eye reads as "vibrating back." + { + Microsoft::WRL::ComPtr dxgi_dev1; + if (SUCCEEDED(device_.As(&dxgi_dev1)) && dxgi_dev1) { + HRESULT hr = dxgi_dev1->SetMaximumFrameLatency(1); + if (FAILED(hr)) { + LOG() << "Dx11Renderer: SetMaximumFrameLatency(1) hr=0x" + << std::hex << hr; + } + } + } + + DXGI_ADAPTER_DESC1 desc{}; + if (adapter_) adapter_->GetDesc1(&desc); + char narrow[256] = {}; + WideCharToMultiByte(CP_UTF8, 0, desc.Description, -1, narrow, sizeof(narrow), nullptr, nullptr); + LOG() << "Dx11Renderer: adapter='" << narrow << "' LUID=" << desc.AdapterLuid.HighPart + << ":" << desc.AdapterLuid.LowPart; + + output_mode_ = cfg.output_mode; + // For NvidiaDX9, resolve the panel dims so EnsureOutputTexture can pin + // out_sbs_ to fixed dims (panel_w*2 × panel_h) — stable shared handle. + if (output_mode_ == OutputMode::NvidiaDX9) { + platform::MonitorInfo p{}, s{}; + if (platform::ResolveTargetMonitors(cfg.display_index, false, p, s)) { + nv_panel_w_ = p.width; + nv_panel_h_ = p.height; + LOG() << "Dx11Renderer: NvidiaDX9 panel dims pinned to " + << nv_panel_w_ << "x" << nv_panel_h_; + } else { + LOG() << "Dx11Renderer: NvidiaDX9 ResolveTargetMonitors failed; " + "shared handle will recreate on dim change (stereo routing may break)"; + } + } + presenter_ = vrto3d::MakePresenter(cfg.output_mode); + if (!presenter_) { + LOG() << "Dx11Renderer: MakePresenter returned null for mode=" << OutputModeToString(cfg.output_mode); + return false; + } + if (!presenter_->Init(*this, cfg, focus)) { + LOG() << "Dx11Renderer: presenter Init failed"; + return false; + } + + // Build the multi-layer composite pipeline. If this fails we still run — + // overlay layers just won't blend (only layer 0's CopySubresourceRegion + // will produce visible output). + do { + Microsoft::WRL::ComPtr vsb, psb; + if (!CompileShaderBlob(kCompositeVsHlsl, "main", "vs_5_0", vsb)) break; + if (!CompileShaderBlob(kCompositePsHlsl, "main", "ps_5_0", psb)) break; + if (FAILED(device_->CreateVertexShader(vsb->GetBufferPointer(), vsb->GetBufferSize(), + nullptr, &composite_vs_))) break; + if (FAILED(device_->CreatePixelShader (psb->GetBufferPointer(), psb->GetBufferSize(), + nullptr, &composite_ps_))) break; + + D3D11_SAMPLER_DESC sd{}; + sd.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + sd.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.MaxLOD = D3D11_FLOAT32_MAX; + if (FAILED(device_->CreateSamplerState(&sd, &composite_sampler_))) break; + + // Straight-alpha blend (SRC_ALPHA / INV_SRC_ALPHA). Matches ALVR's + // convention for OpenVR direct-mode overlay layers. DirectModeComponent + // reorders layers so the game-pid scene is at index 0 (base, copied + // byte-pass-through) and vrcompositor-pid overlays follow at 1+, so + // this straight-alpha math is what they expect. + D3D11_BLEND_DESC bd{}; + bd.RenderTarget[0].BlendEnable = TRUE; + bd.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; + bd.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; + bd.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; + bd.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; + bd.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO; + bd.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + bd.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + if (FAILED(device_->CreateBlendState(&bd, &composite_blend_))) break; + + // Opaque (no blend) — used by the layer-0 shader fallback when the + // per-eye source format doesn't match out_sbs_ (e.g. RGBA8 source + // into BGRA-forced out_sbs_ for NvidiaDX9 mode). The sampler handles + // the byte-order conversion; we just want a straight replace. + D3D11_BLEND_DESC bdo{}; + bdo.RenderTarget[0].BlendEnable = FALSE; + bdo.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + if (FAILED(device_->CreateBlendState(&bdo, &composite_blend_opaque_))) break; + + D3D11_RASTERIZER_DESC rd{}; + rd.FillMode = D3D11_FILL_SOLID; + rd.CullMode = D3D11_CULL_NONE; + if (FAILED(device_->CreateRasterizerState(&rd, &composite_raster_))) break; + + // Disable depth/stencil entirely for the overlay composite. We bind a + // null DSV when drawing layer 1+, but D3D11's default depth-stencil + // state has DepthEnable=TRUE. Without an explicit no-test state, some + // drivers (and any leftover state from a prior pass) can cause every + // overlay pixel to be discarded — manifesting as a fully black overlay + // and a black main menu in games that submit the menu as a layer. + D3D11_DEPTH_STENCIL_DESC dsd{}; + dsd.DepthEnable = FALSE; + dsd.StencilEnable = FALSE; + if (FAILED(device_->CreateDepthStencilState(&dsd, &composite_depth_))) break; + + composite_pipeline_ready_ = true; + } while (false); + + if (!composite_pipeline_ready_) { + LOG() << "Dx11Renderer: composite pipeline init failed — overlay layers will be dropped"; + } + + // Derive the per-eye frame interval from cfg.display_frequency. That + // value has already been halved for frame-sequential output modes + // (see hmd_device_driver.cpp Activate path), so this number is what + // the OpenVR compositor expects as the per-eye refresh rate. + if (cfg.display_frequency > 1.0f) { + const double ns_per_frame = 1.0e9 / static_cast(cfg.display_frequency); + frame_interval_ns_ = std::chrono::nanoseconds(static_cast(ns_per_frame)); + LOG() << "Dx11Renderer: per-eye frame interval = " + << (ns_per_frame / 1.0e6) << "ms (" + << cfg.display_frequency << "Hz)"; + } + + // Dedicated VsyncEvent ticker. The OpenVR compositor uses VsyncEvent to + // pace itself in direct mode; firing it from the window thread inherits + // all our render jitter and feeds it back into the compositor's submit + // cadence (visible as bimodal frame-interval histograms under load). + // Running it from a high-precision timer thread gives the compositor a + // stable per-eye tick regardless of what the GPU is doing. + initialized_ = true; + vsync_stop_.store(false, std::memory_order_relaxed); + vsync_thread_ = std::thread(&Dx11Renderer::VsyncTickThread, this); + return true; +} + + +void Dx11Renderer::VsyncTickThread() +{ + using clock = std::chrono::steady_clock; + const auto interval = frame_interval_ns_; + // High-resolution timing: bump system timer to 1ms for sleep_until accuracy. + timeBeginPeriod(1); + auto next = clock::now() + interval; + while (!vsync_stop_.load(std::memory_order_relaxed)) { + std::this_thread::sleep_until(next); + vr::VRServerDriverHost()->VsyncEvent(0.0); + next += interval; + // If we've fallen behind (e.g. process throttled), don't try to + // catch up by firing back-to-back — re-anchor to "now + interval" + // so the compositor's clock stays smooth. + const auto now = clock::now(); + if (next < now) next = now + interval; + } + timeEndPeriod(1); +} + + +// Detect a terminal DXGI device state from any HRESULT and latch it. On the +// first observation we log GetDeviceRemovedReason and set device_dead_; all +// future calls return immediately. This stops the EnsureOutputTexture spam +// loop that was hammering a dead device thousands of times per second. +bool Dx11Renderer::CheckAndMarkDeviceDead(HRESULT hr, const char* origin) +{ + const bool is_lost = + hr == DXGI_ERROR_DEVICE_REMOVED || + hr == DXGI_ERROR_DEVICE_RESET || + hr == DXGI_ERROR_DEVICE_HUNG || + hr == DXGI_ERROR_DRIVER_INTERNAL_ERROR || + hr == 0x887A0005 /* legacy DXGI_ERROR_DEVICE_REMOVED on some SDKs */; + if (!is_lost) return false; + bool expected = false; + if (device_dead_.compare_exchange_strong(expected, true)) { + HRESULT reason = device_ ? device_->GetDeviceRemovedReason() : S_OK; + device_removed_reason_ = reason; + LOG() << "Dx11Renderer: device terminal state in " << (origin ? origin : "?") + << " hr=0x" << std::hex << hr + << " GetDeviceRemovedReason=0x" << std::hex << reason + << " — stopping renderer. SteamVR restart required."; + // Drop everything that might be holding a reference to the dying + // device's resources. This unblocks SteamVR shutdown cleanly. + shared_texture_cache_.clear(); + composite_srv_cache_.clear(); + out_sbs_.Reset(); + out_sbs_rtv_.Reset(); + } + return true; +} + + +void Dx11Renderer::OnAppDisconnect() +{ + // Called from MyDeviceProvider when SteamVR fires VREvent_ProcessDisconnected. + // The departed game's swap textures are still referenced in our caches via + // shared-texture handles, but the source ID3D11Device just went away — any + // attempt to import / use them now will fault or trigger DEVICE_REMOVED. + // Drop everything and pause until the next ProcessConnected. + paused_for_disconnect_.store(true, std::memory_order_release); + + std::lock_guard ctx_lk(context_mutex_); + shared_texture_cache_.clear(); + composite_srv_cache_.clear(); + // out_sbs_ is sized for the previous game's resolution; the next game may + // submit a different size. Drop it so it gets recreated cleanly. + // + // EXCEPT in NvidiaDX9 mode where out_sbs_ is pinned to fixed panel dims + // (and is its own composite target — game source dims don't change its + // size). NvStereoDx9Presenter sizes its shared staging ring from + // out_sbs_'s desc and NV3D-Lib identity-caches the D3D9 imports of the + // ring slots; recreating out_sbs_ here would churn the ring + imports at + // every transition (historically NV3D's per-eye routing got stuck when + // the imported resource was recreated). Keep out_sbs_ alive so the whole + // chain stays stable across disconnect/connect. + if (output_mode_ != OutputMode::NvidiaDX9 || + !nv_panel_w_ || !nv_panel_h_) { + out_sbs_.Reset(); + out_sbs_rtv_.Reset(); + sbs_width_ = sbs_height_ = 0; + sbs_format_ = DXGI_FORMAT_UNKNOWN; + } + LOG() << "Dx11Renderer: app-disconnect — caches cleared, renderer paused" + << ((output_mode_ == OutputMode::NvidiaDX9 && nv_panel_w_ && nv_panel_h_) + ? " (out_sbs_ preserved for stable NvidiaDX9 shared handle)" + : ""); +} + + +void Dx11Renderer::OnAppConnect() +{ + paused_for_disconnect_.store(false, std::memory_order_release); + LOG() << "Dx11Renderer: app-connect — renderer resumed"; +} + + +void Dx11Renderer::EnsureOutputTexture(const D3D11_TEXTURE2D_DESC& incoming) +{ + // NvidiaDX9 forces BGRA byte order on out_sbs_ because the D3D9 import + // path (IDXGIResource::GetSharedHandle → IDirect3DDevice9Ex::CreateTexture + // with D3DFMT_A8R8G8B8) only matches BGRA. If the per-eye source is RGBA, + // OnDirectModeFrame routes layer 0 through the composite-shader path so + // the sampler converts byte order on read. Other output modes preserve + // the source format so layer 0's CopySubresourceRegion stays byte-exact. + DXGI_FORMAT effective_fmt = incoming.Format; + if (output_mode_ == OutputMode::NvidiaDX9) { + if (effective_fmt == DXGI_FORMAT_R8G8B8A8_UNORM || + effective_fmt == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB) { + effective_fmt = DXGI_FORMAT_B8G8R8A8_UNORM; + } + } + + // NvidiaDX9 also pins out_sbs_ to fixed panel-derived dims so the + // cross-device shared handle stays stable across landscape↔game + // transitions. Without this, NV3D's per-eye routing gets stuck on the + // left eye when shared_input_sfc_ is recreated. The layer composite + // shader path scales source pixels to fill the panel-sized halves. + UINT effective_w = incoming.Width; + UINT effective_h = incoming.Height; + if (output_mode_ == OutputMode::NvidiaDX9 && nv_panel_w_ && nv_panel_h_) { + effective_w = nv_panel_w_ * 2u; + effective_h = nv_panel_h_; + } + + if (out_sbs_ && + sbs_width_ == effective_w && + sbs_height_ == effective_h && + sbs_format_ == effective_fmt) { + return; + } + + D3D11_TEXTURE2D_DESC d{}; + d.Width = effective_w; + d.Height = effective_h; + d.MipLevels = 1; + d.ArraySize = 1; + d.Format = effective_fmt; + d.SampleDesc.Count = 1; + d.Usage = D3D11_USAGE_DEFAULT; + d.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; + d.CPUAccessFlags = 0; + // SHARED so cross-device presenters can call OpenSharedResource on this + // texture's GetSharedHandle(): + // - WibbleWobble's in-process client uses its own internal D3D11 device. + // - NvStereoDX9 imports the handle into D3D9Ex via CreateTexture + // (pSharedHandle) for a GPU-side StretchRect into its packed surface. + // Other on-device presenters (Window/LeiaSR) ignore the flag. + d.MiscFlags = D3D11_RESOURCE_MISC_SHARED; + + out_sbs_.Reset(); + out_sbs_rtv_.Reset(); + composite_srv_cache_.clear(); // texture lifetimes change at this boundary + HRESULT hr = device_->CreateTexture2D(&d, nullptr, &out_sbs_); + if (FAILED(hr)) { + LOG() << "Dx11Renderer: CreateTexture2D(out_sbs) failed hr=" << std::hex << hr; + (void)CheckAndMarkDeviceDead(hr, "EnsureOutputTexture/CreateTexture2D"); + return; + } + sbs_width_ = effective_w; + sbs_height_ = effective_h; + sbs_format_ = effective_fmt; + + // Build the RTV used by the multi-layer composite path. Failure here just + // disables overlay blending; layer 0 still flows through CopySubresourceRegion. + if (composite_pipeline_ready_) { + D3D11_RENDER_TARGET_VIEW_DESC rd{}; + rd.Format = d.Format; + rd.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; + HRESULT rhr = device_->CreateRenderTargetView(out_sbs_.Get(), &rd, &out_sbs_rtv_); + if (FAILED(rhr)) { + LOG() << "Dx11Renderer: CreateRenderTargetView(out_sbs) failed hr=0x" + << std::hex << rhr; + } + } + LOG() << "Dx11Renderer: out_sbs_ (re)created " << d.Width << "x" << d.Height + << " fmt=" << d.Format; +} + + +// --------------------------------------------------------------------------- +// Display-correction pipeline init. One-shot: builds shaders + static states +// the first time ApplyDisplayCorrection() is called with the feature enabled. +// Machines that never enable the shader pay no init cost. Returns true if +// (already) ready, false on failure (caller skips the pass). +// --------------------------------------------------------------------------- +bool Dx11Renderer::EnsureDisplayCorrectionPipeline() +{ + if (adj_pipeline_ready_) return true; + if (adj_pipeline_tried_) return false; // failed previously — don't retry every frame + adj_pipeline_tried_ = true; + + if (!device_) return false; + + Microsoft::WRL::ComPtr vsb, psb; + if (!CompileShaderBlob(kAdjustVsHlsl, "main", "vs_5_0", vsb)) return false; + if (!CompileShaderBlob(kAdjustPsHlsl, "main", "ps_5_0", psb)) return false; + if (FAILED(device_->CreateVertexShader(vsb->GetBufferPointer(), vsb->GetBufferSize(), + nullptr, &adj_vs_))) return false; + if (FAILED(device_->CreatePixelShader (psb->GetBufferPointer(), psb->GetBufferSize(), + nullptr, &adj_ps_))) return false; + + D3D11_SAMPLER_DESC sd{}; + sd.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + sd.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.MaxLOD = D3D11_FLOAT32_MAX; + if (FAILED(device_->CreateSamplerState(&sd, &adj_sampler_))) return false; + + D3D11_BUFFER_DESC cbd{}; + cbd.ByteWidth = sizeof(AdjustCB); + cbd.Usage = D3D11_USAGE_DYNAMIC; + cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + if (FAILED(device_->CreateBuffer(&cbd, nullptr, &adj_cb_))) return false; + + // Opaque blend — straight write, no alpha math. + D3D11_BLEND_DESC bd{}; + bd.RenderTarget[0].BlendEnable = FALSE; + bd.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + if (FAILED(device_->CreateBlendState(&bd, &adj_blend_))) return false; + + D3D11_RASTERIZER_DESC rd{}; + rd.FillMode = D3D11_FILL_SOLID; + rd.CullMode = D3D11_CULL_NONE; + if (FAILED(device_->CreateRasterizerState(&rd, &adj_raster_))) return false; + + D3D11_DEPTH_STENCIL_DESC dsd{}; + dsd.DepthEnable = FALSE; + dsd.StencilEnable = FALSE; + if (FAILED(device_->CreateDepthStencilState(&dsd, &adj_depth_))) return false; + + adj_pipeline_ready_ = true; + LOG() << "Dx11Renderer: display-correction pipeline initialized"; + return true; +} + + +// --------------------------------------------------------------------------- +// In-place display-correction pass on out_sbs_. Skipped when shader_enabled +// is false (zero cost). Otherwise: lazy-init the pipeline, lazy-create the +// scratch texture to match out_sbs_'s dims/format, copy out_sbs_ -> scratch, +// then draw a fullscreen triangle reading scratch through the adjustment +// shader and writing back to out_sbs_. +// --------------------------------------------------------------------------- +void Dx11Renderer::ApplyDisplayCorrection() +{ + if (!osd_component_) return; + StereoDisplayDriverConfiguration cfg = osd_component_->GetConfig(); + if (!cfg.shader_enabled) return; + if (!out_sbs_ || !out_sbs_rtv_ || !context_ || !device_) return; + if (sbs_width_ == 0 || sbs_height_ == 0) return; + + if (!EnsureDisplayCorrectionPipeline()) return; + + // (Re)create scratch when dims/format change. Local-only, no SHARED. + if (!adj_scratch_ + || adj_scratch_w_ != sbs_width_ + || adj_scratch_h_ != sbs_height_ + || adj_scratch_format_ != sbs_format_) { + adj_scratch_.Reset(); + adj_scratch_srv_.Reset(); + + D3D11_TEXTURE2D_DESC td{}; + td.Width = sbs_width_; + td.Height = sbs_height_; + td.MipLevels = 1; + td.ArraySize = 1; + td.Format = sbs_format_; + td.SampleDesc = { 1, 0 }; + td.Usage = D3D11_USAGE_DEFAULT; + td.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; + HRESULT hr = device_->CreateTexture2D(&td, nullptr, &adj_scratch_); + if (FAILED(hr)) { + LOG() << "Dx11Renderer: ApplyDisplayCorrection scratch CreateTexture2D failed hr=0x" + << std::hex << hr; + return; + } + D3D11_SHADER_RESOURCE_VIEW_DESC srvd{}; + srvd.Format = sbs_format_; + srvd.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvd.Texture2D.MipLevels = 1; + hr = device_->CreateShaderResourceView(adj_scratch_.Get(), &srvd, &adj_scratch_srv_); + if (FAILED(hr)) { + LOG() << "Dx11Renderer: ApplyDisplayCorrection scratch SRV failed hr=0x" + << std::hex << hr; + adj_scratch_.Reset(); + return; + } + adj_scratch_w_ = sbs_width_; + adj_scratch_h_ = sbs_height_; + adj_scratch_format_ = sbs_format_; + } + + // Push the live config into the constant buffer. + D3D11_MAPPED_SUBRESOURCE m{}; + if (FAILED(context_->Map(adj_cb_.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &m))) return; + { + AdjustCB cb{}; + cb.lift[0] = cfg.shader_lift[0]; + cb.lift[1] = cfg.shader_lift[1]; + cb.lift[2] = cfg.shader_lift[2]; + cb.lift[3] = 0.0f; + cb.gamma[0] = cfg.shader_gamma[0]; + cb.gamma[1] = cfg.shader_gamma[1]; + cb.gamma[2] = cfg.shader_gamma[2]; + cb.gamma[3] = 0.0f; + cb.gain[0] = cfg.shader_gain[0]; + cb.gain[1] = cfg.shader_gain[1]; + cb.gain[2] = cfg.shader_gain[2]; + cb.gain[3] = 0.0f; + cb.curve = cfg.shader_curve; + cb.off_low = cfg.shader_curve_off_low; + cb.off_high = cfg.shader_curve_off_high; + cb.off_both = cfg.shader_curve_off_both; + std::memcpy(m.pData, &cb, sizeof(cb)); + } + context_->Unmap(adj_cb_.Get(), 0); + + // Snapshot the current out_sbs_ contents into the scratch. + context_->CopyResource(adj_scratch_.Get(), out_sbs_.Get()); + + // Draw scratch -> out_sbs_ through the adjustment shader. + D3D11_VIEWPORT vp{}; + vp.Width = static_cast(sbs_width_); + vp.Height = static_cast(sbs_height_); + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + + ID3D11RenderTargetView* rtv = out_sbs_rtv_.Get(); + context_->OMSetRenderTargets(1, &rtv, nullptr); + context_->OMSetDepthStencilState(adj_depth_.Get(), 0); + const float blend_factor[4] = { 1.f, 1.f, 1.f, 1.f }; + context_->OMSetBlendState(adj_blend_.Get(), blend_factor, 0xFFFFFFFFu); + context_->RSSetState(adj_raster_.Get()); + context_->RSSetViewports(1, &vp); + context_->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + context_->IASetInputLayout(nullptr); + context_->VSSetShader(adj_vs_.Get(), nullptr, 0); + context_->PSSetShader(adj_ps_.Get(), nullptr, 0); + ID3D11Buffer* cbs[] = { adj_cb_.Get() }; + context_->PSSetConstantBuffers(0, 1, cbs); + ID3D11ShaderResourceView* srvs[] = { adj_scratch_srv_.Get() }; + context_->PSSetShaderResources(0, 1, srvs); + ID3D11SamplerState* samps[] = { adj_sampler_.Get() }; + context_->PSSetSamplers(0, 1, samps); + + context_->Draw(3, 0); + + // Unbind the SRV so the same texture can be bound as RTV next frame + // (D3D11 forbids simultaneous SRV+RTV on the same resource). + ID3D11ShaderResourceView* null_srv[] = { nullptr }; + context_->PSSetShaderResources(0, 1, null_srv); + ID3D11RenderTargetView* null_rtv[] = { nullptr }; + context_->OMSetRenderTargets(1, null_rtv, nullptr); +} + + +void Dx11Renderer::OnDirectModeFrame(const DirectModeLayerPair* layers, + int layer_count, + vr::SharedTextureHandle_t sync_handle) +{ + if (!initialized_ || !device_ || !context_ || !layers || layer_count <= 0) return; + // Circuit-breaker: once the device has hit DEVICE_REMOVED / RESET / HUNG + // we stop submitting work. Continuing to hammer the device was the cause + // of the original "thousands of CreateTexture2D failures per second" log + // pattern that contributed to the GPU driver freeze. + if (device_dead_.load(std::memory_order_acquire)) return; + // If the presenter has died (e.g. NvStereoDx9Presenter latched dead on a + // device-removed check or a present-failure streak), stop composing too. + // Continuing to push shader work to a wedged GPU prevents NVIDIA's TDR + // from recovering and has been observed to cause full-PC freezes. + if (presenter_ && !presenter_->IsAlive()) return; + // Pause-on-disconnect: skip frames between VR apps so we don't try to + // import stale shared-texture handles from a process that just exited. + if (paused_for_disconnect_.load(std::memory_order_acquire)) return; + if (layer_count > kMaxLayers) layer_count = kMaxLayers; + if (!layers[0].left || !layers[0].right) return; + + // Compose synchronously on the compositor thread (matches ALVR's + // pattern). The AcquireSync gate forces UEVR's GPU work for both eyes + // to be committed before we read, eliminating the read-mid-render race + // that caused the "frame jumping back" stutter. + + // Open + cache the syncTexture (compositor reuses the handle). + Microsoft::WRL::ComPtr sync_tex; + Microsoft::WRL::ComPtr sync_mutex; + if (sync_handle) { + auto it = shared_texture_cache_.find(sync_handle); + if (it != shared_texture_cache_.end()) { + sync_tex = it->second; + } else if (platform::ImportSharedTexture(device_.Get(), sync_handle, sync_tex) && sync_tex) { + shared_texture_cache_.emplace(sync_handle, sync_tex); + LOG() << "Dx11Renderer: cached sync texture handle=0x" << std::hex << sync_handle; + } + if (sync_tex) sync_tex.As(&sync_mutex); + } + + ID3D11Texture2D* const left = layers[0].left.Get(); + ID3D11Texture2D* const right = layers[0].right.Get(); + + D3D11_TEXTURE2D_DESC eye_desc{}; + left->GetDesc(&eye_desc); + D3D11_TEXTURE2D_DESC right_desc{}; + right->GetDesc(&right_desc); + + // Resolve bounds → integer source sub-rect for each eye. Games may + // allocate a texture larger than the rendered region (e.g. sized for max + // FFR/supersample) and submit bounds < (0,0,1,1) to mark the valid + // portion; copying the whole texture would compress the valid portion + // into the top-left of out_sbs_ (picture-in-picture). + auto sub_rect = [](const vr::VRTextureBounds_t& b, UINT tex_w, UINT tex_h, + UINT& sx, UINT& sy, UINT& sw, UINT& sh) -> bool { + // V-flip / U-flip bounds (vMin > vMax etc.) would require shader + // sampling to mirror, which CopySubresourceRegion can't do. Detect + // and fall through to "full texture" below. + if (!(b.uMax > b.uMin) || !(b.vMax > b.vMin)) return false; + const float du = b.uMax - b.uMin; + const float dv = b.vMax - b.vMin; + if (du <= 0.f || du > 1.f || dv <= 0.f || dv > 1.f) return false; + sx = static_cast(b.uMin * tex_w + 0.5f); + sy = static_cast(b.vMin * tex_h + 0.5f); + sw = static_cast(du * tex_w + 0.5f); + sh = static_cast(dv * tex_h + 0.5f); + if (sw == 0 || sh == 0) return false; + if (sx + sw > tex_w || sy + sh > tex_h) return false; + return true; + }; + + UINT lsx = 0, lsy = 0, lsw = eye_desc.Width, lsh = eye_desc.Height; + UINT rsx = 0, rsy = 0, rsw = right_desc.Width, rsh = right_desc.Height; + const bool left_sub = sub_rect(layers[0].bounds_left, eye_desc.Width, eye_desc.Height, lsx, lsy, lsw, lsh); + const bool right_sub = sub_rect(layers[0].bounds_right, right_desc.Width, right_desc.Height, rsx, rsy, rsw, rsh); + if (!left_sub) { + lsx = lsy = 0; lsw = eye_desc.Width; lsh = eye_desc.Height; + } + if (!right_sub) { + rsx = rsy = 0; rsw = right_desc.Width; rsh = right_desc.Height; + } + // out_sbs_ is sized to the per-eye effective dims (post-bounds). Both + // eyes are normally identical; if they diverge, use the left as the + // canonical eye size — sampling a sub-rect of the right that ends up a + // pixel off in width is far less visible than a stretched composite. + // + // NvidiaDX9 mode overrides this and pins per-eye to panel dims so + // out_sbs_ stays at fixed (panel_w*2 × panel_h) across all transitions — + // the cross-device shared handle remains stable and NV3D's per-eye + // routing doesn't get stuck. The shader path scales the source + // (which may be a different aspect / resolution) to fill the panel. + UINT eff_eye_w = lsw; + UINT eff_eye_h = lsh; + if (output_mode_ == OutputMode::NvidiaDX9 && nv_panel_w_ && nv_panel_h_) { + eff_eye_w = nv_panel_w_; + eff_eye_h = nv_panel_h_; + } + + // One-line-per-change diagnostic. Effective dims and "(bounds applied)" + // are only reported when the submitted bounds actually shrink the source + // sub-rect below the raw texture size, since that's the case worth + // calling out in a debug log. + const bool bounds_shrank = eff_eye_w != eye_desc.Width || + eff_eye_h != eye_desc.Height; + if (eye_desc.Width != last_left_w_ || eye_desc.Height != last_left_h_ || + right_desc.Width != last_right_w_ || right_desc.Height != last_right_h_ || + eff_eye_w != last_eff_w_ || eff_eye_h != last_eff_h_ || + eye_desc.Format != last_eye_format_) { + if (bounds_shrank) { + LOG() << "Dx11Renderer: per-eye dims L=" << eye_desc.Width << "x" << eye_desc.Height + << " R=" << right_desc.Width << "x" << right_desc.Height + << " fmt=" << eye_desc.Format + << " eff=" << eff_eye_w << "x" << eff_eye_h << " (bounds applied)"; + } else { + LOG() << "Dx11Renderer: per-eye dims L=" << eye_desc.Width << "x" << eye_desc.Height + << " R=" << right_desc.Width << "x" << right_desc.Height + << " fmt=" << eye_desc.Format; + } + last_left_w_ = eye_desc.Width; + last_left_h_ = eye_desc.Height; + last_right_w_ = right_desc.Width; + last_right_h_ = right_desc.Height; + last_eff_w_ = eff_eye_w; + last_eff_h_ = eff_eye_h; + last_eye_format_ = eye_desc.Format; + } + + auto strip_srgb = [](DXGI_FORMAT f) -> DXGI_FORMAT { + switch (f) { + case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM; + case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM; + case DXGI_FORMAT_B8G8R8X8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8X8_UNORM; + case DXGI_FORMAT_BC1_UNORM_SRGB: return DXGI_FORMAT_BC1_UNORM; + case DXGI_FORMAT_BC2_UNORM_SRGB: return DXGI_FORMAT_BC2_UNORM; + case DXGI_FORMAT_BC3_UNORM_SRGB: return DXGI_FORMAT_BC3_UNORM; + case DXGI_FORMAT_BC7_UNORM_SRGB: return DXGI_FORMAT_BC7_UNORM; + default: return f; + } + }; + + // Lock the immediate context for the duration of our GPU submits so + // the window thread doesn't issue Present-side commands concurrently. + std::lock_guard ctx_lk(context_mutex_); + + D3D11_TEXTURE2D_DESC sbs_desc = eye_desc; + sbs_desc.Width = eff_eye_w * 2; + sbs_desc.Height = eff_eye_h; + sbs_desc.Format = strip_srgb(eye_desc.Format); + EnsureOutputTexture(sbs_desc); + if (!out_sbs_) return; + + // Hold AcquireSync only across the read of the per-eye textures. + // Failure (timeout) means the compositor's work isn't yet committed — + // skip this frame rather than read stale/torn content. + bool acquired = false; + if (sync_mutex) { + HRESULT ahr = sync_mutex->AcquireSync(0, 10); + if (ahr == S_OK) { + acquired = true; + } else { + // Route through the circuit-breaker — if the source device is + // dead, AcquireSync can start returning DEVICE_REMOVED and we + // should latch+stop rather than spin retrying. + (void)CheckAndMarkDeviceDead(ahr, "AcquireSync"); + static std::atomic logged{false}; + bool expected = false; + if (logged.compare_exchange_strong(expected, true)) { + LOG() << "Dx11Renderer: AcquireSync(0,10) returned 0x" + << std::hex << ahr << " — skipping frame"; + } + return; + } + } + + // Format-mismatch detection: when out_sbs_ was forced to BGRA (NvidiaDX9 + // mode) but the per-eye source is RGBA, CopySubresourceRegion will refuse + // the copy (different byte-order families). Route layer 0 through the + // shader instead — the sampler converts byte order on read. Bounds in + // this fallback are best-effort: full-texture sampling rather than the + // box-based sub-rect that the Copy path supports. + // + // NvidiaDX9 mode also pins out_sbs_ to panel dims (not source dims), + // which means CopySubresourceRegion would fail on the size mismatch + // even when formats match — always force the shader path for that mode. + const bool fmt_mismatch = + sbs_format_ != strip_srgb(eye_desc.Format) || + sbs_format_ != strip_srgb(right_desc.Format) || + (output_mode_ == OutputMode::NvidiaDX9 && nv_panel_w_ && nv_panel_h_); + + if (!fmt_mismatch) { + // Layer 0 — base scene, byte-pass-through via CopySubresourceRegion. + // Source rect honors the submitted bounds (when applicable) so an + // oversized eye texture with bounds=(0,0,<1,<1) doesn't degenerate + // into picture-in-picture. + D3D11_BOX lbox{ lsx, lsy, 0, lsx + lsw, lsy + lsh, 1 }; + D3D11_BOX rbox{ rsx, rsy, 0, rsx + rsw, rsy + rsh, 1 }; + context_->CopySubresourceRegion(out_sbs_.Get(), 0, + 0, 0, 0, + left, 0, + &lbox); + context_->CopySubresourceRegion(out_sbs_.Get(), 0, + eff_eye_w, 0, 0, + right, 0, + &rbox); + } + + // Shader composite path. Entered when either: + // - There are overlay layers (1+) that need alpha blending onto the base. + // - The per-eye source format doesn't match out_sbs_ (NvidiaDX9 BGRA + // forcing + RGBA source) — layer 0 also routes through here with the + // opaque blend state. + const bool need_shader = (layer_count > 1) || fmt_mismatch; + if (need_shader && composite_pipeline_ready_ && out_sbs_rtv_) { + ID3D11RenderTargetView* rtv = out_sbs_rtv_.Get(); + const float blend_factor[4] = { 1, 1, 1, 1 }; + + context_->OMSetRenderTargets(1, &rtv, nullptr); + context_->OMSetDepthStencilState(composite_depth_.Get(), 0); + context_->RSSetState(composite_raster_.Get()); + context_->IASetInputLayout(nullptr); + context_->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + context_->VSSetShader(composite_vs_.Get(), nullptr, 0); + context_->PSSetShader(composite_ps_.Get(), nullptr, 0); + ID3D11SamplerState* samps[] = { composite_sampler_.Get() }; + context_->PSSetSamplers(0, 1, samps); + + // Match the per-eye region we wrote layer 0 into. Using texture dims + // here would place overlay halves outside out_sbs_ whenever bounds < + // (0,0,1,1) shrank the scene below the raw texture size. + const UINT half_w = eff_eye_w; + const UINT full_h = eff_eye_h; + + auto blit_half = [&](ID3D11Texture2D* tex, UINT viewport_x) { + if (!tex) return; + ID3D11ShaderResourceView* srv_raw = nullptr; + auto it = composite_srv_cache_.find(tex); + if (it != composite_srv_cache_.end()) { + srv_raw = it->second.Get(); + } else { + D3D11_TEXTURE2D_DESC td{}; + tex->GetDesc(&td); + D3D11_SHADER_RESOURCE_VIEW_DESC sv{}; + sv.Format = td.Format; + sv.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + sv.Texture2D.MipLevels = 1; + sv.Texture2D.MostDetailedMip = 0; + Microsoft::WRL::ComPtr srv; + if (FAILED(device_->CreateShaderResourceView(tex, &sv, &srv)) || !srv) return; + srv_raw = srv.Get(); + composite_srv_cache_.emplace(tex, std::move(srv)); + } + ID3D11ShaderResourceView* srvs[] = { srv_raw }; + context_->PSSetShaderResources(0, 1, srvs); + + D3D11_VIEWPORT vp{}; + vp.TopLeftX = static_cast(viewport_x); + vp.TopLeftY = 0.0f; + vp.Width = static_cast(half_w); + vp.Height = static_cast(full_h); + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + context_->RSSetViewports(1, &vp); + context_->Draw(3, 0); + + ID3D11ShaderResourceView* null_srv[] = { nullptr }; + context_->PSSetShaderResources(0, 1, null_srv); + }; + + // Layer 0 fallback (opaque replace) — only when format mismatch + // skipped the CopySubresourceRegion path. + if (fmt_mismatch) { + context_->OMSetBlendState(composite_blend_opaque_.Get(), blend_factor, 0xFFFFFFFF); + blit_half(left, 0); + blit_half(right, half_w); + } + + // Layers 1+ — alpha-blended overlay. + if (layer_count > 1) { + context_->OMSetBlendState(composite_blend_.Get(), blend_factor, 0xFFFFFFFF); + for (int li = 1; li < layer_count; ++li) { + blit_half(layers[li].left.Get(), 0); + blit_half(layers[li].right.Get(), half_w); + } + } + + ID3D11RenderTargetView* null_rtv = nullptr; + context_->OMSetRenderTargets(1, &null_rtv, nullptr); + } + + // Auto-depth: dispatch the disparity compute pass on the freshly-copied + // out_sbs_, before OSD composite so the analysis runs on the clean frame. + if (osd_component_ && osd_component_->IsAutoDepthEnabled()) { + if (!auto_depth_) { + auto_depth_ = std::make_unique(); + auto_depth_->Init(device_.Get()); + } + auto_depth_->Run(context_.Get(), out_sbs_.Get(), + sbs_width_, sbs_height_, + frame_counter_.load(std::memory_order_relaxed), + osd_component_); + } + + // Drain a pending screenshot request before OSD compositing so the saved + // image is the clean stereo frame, not one with menu/toast text baked in. + { + bool want = false; + std::string name; + { + std::lock_guard lk(screenshot_mutex_); + if (screenshot_pending_) { + want = true; + name = std::move(pending_screenshot_app_); + pending_screenshot_app_.clear(); + screenshot_pending_ = false; + } + } + if (want) CaptureScreenshot(name); + } + + // Optional display-correction shader pass — applied on the compositor + // thread (here, before Flush) rather than the window thread so the GPU + // writes are committed by the existing Flush() barrier. Cross-device + // consumers (NvidiaDX9 D3D9Ex shared handle, WibbleWobble's own DX11 + // device, LeiaSR weaver) all rely on that barrier; running the pass on + // the window thread races them and produces a per-frame brightness + // flicker as some frames are read pre-shader and others post-shader. + // Runs after the screenshot drain so saved screenshots stay clean. + ApplyDisplayCorrection(); + + // Force compositor's writes to commit before we release the sync mutex + // and before the window thread observes a fresh out_sbs_. + context_->Flush(); + + if (acquired) sync_mutex->ReleaseSync(0); + + // Signal the window thread that out_sbs_ holds a fresh composite so it + // can wake from cv.wait_for and present immediately — drives the + // window-thread pace off the compositor's actual submit rate rather + // than an independent timer that would introduce phase-offset latency. + { + std::lock_guard lk(composite_ready_mutex_); + composite_ready_ = true; + } + composite_cv_.notify_one(); +} + + +bool Dx11Renderer::WaitAndDrawPending(int timeout_ms) +{ + if (!initialized_ || !device_) return false; + if (device_dead_.load(std::memory_order_acquire)) return false; + // Presenter terminal state (e.g. NvStereoDx9Presenter latched dead after + // a device-removed check or 40-frame present-failure streak): skip OSD + // render + RecordComposite + Present so we stop adding GPU work. Sleep + // instead of busy-looping so the dead state doesn't burn CPU. + if (presenter_ && !presenter_->IsAlive()) { + std::this_thread::sleep_for(std::chrono::milliseconds(timeout_ms)); + return false; + } + + // Wait for the compositor thread to signal "fresh composite ready" + // (matches the old IVRVirtualDisplay path: window thread paced by + // compositor's submit rate, so each present happens immediately after + // a new out_sbs_ is ready — no phase-offset latency from a separate + // internal timer). If nothing arrives within `timeout_ms` we still + // fall through and re-present whatever out_sbs_ holds, so the display + // can't stall on an idle compositor. + { + std::unique_lock lk(composite_ready_mutex_); + composite_cv_.wait_for(lk, std::chrono::milliseconds(timeout_ms), + [&]{ return composite_ready_; }); + composite_ready_ = false; + } + + if (!out_sbs_) return false; + + // Serialize against OnDirectModeFrame's GPU work on the immediate + // context. Lock is held only across DrainPendingShot + OSD + the + // presenter's RecordComposite. The swap-chain Present (presenter_-> + // Present()) runs OUTSIDE this lock — Present can block on display + // pacing (FLIP waitable / vsync interval), and holding context_mutex_ + // across it would stall the compositor thread's next OnDirectModeFrame. + { + std::lock_guard ctx_lk(context_mutex_); + + // Drain any previously-queued staging readback (screenshot encode). + DrainPendingShot(); + + // If a background screenshot encode completed since our last tick, + // surface its result toast now — done on the window thread so the + // OSD lifetime is well-defined. + { + std::string toast; + { + std::lock_guard lk(screenshot_result_mutex_); + if (!pending_screenshot_toast_.empty()) { + toast = std::move(pending_screenshot_toast_); + pending_screenshot_toast_.clear(); + } + } + if (!toast.empty() && osd_renderer_) { + osd_renderer_->SetText(toast); + } + } + + // Lazy-init the OSD now that we know per-eye dimensions. + if (!osd_initialized_ && osd_pending_callbacks_ && device_ && context_) { + osd_renderer_ = std::make_unique(); + const UINT eye_w = sbs_width_ / 2; + if (osd_renderer_->Init(device_.Get(), context_.Get(), + eye_w, sbs_height_, + osd_headset_hwnd_, + osd_component_, + std::move(*osd_pending_callbacks_))) { + osd_initialized_ = true; + LOG() << "Dx11Renderer: OSD initialized eye=" << eye_w << "x" << sbs_height_; + } else { + LOG() << "Dx11Renderer: OSD init failed"; + osd_renderer_.reset(); + } + osd_pending_callbacks_.reset(); + } + + // Composite OSD into out_sbs_ before the presenter sees it. The OSD + // is intentionally drawn AFTER the display-correction shader pass (run + // upstream on the compositor thread) so the menu text stays at full + // fidelity and isn't dimmed by the user's crosstalk-reduction curves. + if (osd_initialized_ && osd_renderer_) { + const UINT eye_w = sbs_width_ / 2; + osd_renderer_->OnResize(eye_w, sbs_height_); + osd_renderer_->RenderFrame(out_sbs_.Get()); + } + + if (presenter_) presenter_->RecordComposite(out_sbs_.Get()); + + // Flush so all GPU commands recorded under context_mutex_ are in the + // driver queue before we release the lock. Any work the compositor + // thread queues next will land after ours in driver-submission order, + // and the GPU's automatic resource-hazard tracking serializes the + // out_sbs_ read/write between this frame's compose and the next + // frame's CopySubresourceRegion. + if (context_) context_->Flush(); + } + + // Present outside the lock. Pacing comes from the presenter's display + // hook (FLIP waitable handle / vsync interval), not from blocking here. + if (presenter_) presenter_->Present(); + + // VsyncEvent is driven from a dedicated 120Hz timer thread (see + // VsyncTickThread), not here. Firing it from the window thread inherits + // all of our jitter, which the compositor then feeds back into its + // submit cadence — bimodal frame intervals being the visible result. + + frame_counter_.fetch_add(1, std::memory_order_relaxed); + LARGE_INTEGER q{}; + if (QueryPerformanceCounter(&q)) { + last_vsync_qpc_sec_.store(static_cast(q.QuadPart) * qpc_freq_sec_, + std::memory_order_relaxed); + } + + return true; +} + + +void Dx11Renderer::Shutdown() +{ + if (!initialized_) return; + LOG() << "Dx11Renderer: Shutdown (frames=" << frame_counter_.load() << ")"; + + // Stop the VsyncEvent ticker first so the compositor doesn't keep + // submitting frames into a half-torn-down pipeline. + vsync_stop_.store(true, std::memory_order_relaxed); + if (vsync_thread_.joinable()) vsync_thread_.join(); + + // Wake the window thread so it can drop out of WaitAndDrawPending. + { + std::lock_guard lk(composite_ready_mutex_); + composite_ready_ = true; + } + composite_cv_.notify_all(); + { + std::lock_guard lk(pending_mutex_); + pending_ready_ = true; + } + pending_cv_.notify_all(); + + if (osd_renderer_) { + osd_renderer_->Shutdown(); + osd_renderer_.reset(); + } + osd_initialized_ = false; + osd_pending_callbacks_.reset(); + + if (presenter_) { + presenter_->Shutdown(); + presenter_.reset(); + } + + if (auto_depth_) { + auto_depth_->Shutdown(); + auto_depth_.reset(); + } + + shared_texture_cache_.clear(); + composite_srv_cache_.clear(); + for (auto& pl : pending_layers_) { pl.left.Reset(); pl.right.Reset(); } + pending_layer_count_ = 0; + pending_shot_.staging.Reset(); + has_pending_shot_ = false; + out_sbs_rtv_.Reset(); + out_sbs_.Reset(); + composite_vs_.Reset(); + composite_ps_.Reset(); + composite_sampler_.Reset(); + composite_blend_.Reset(); + composite_raster_.Reset(); + composite_depth_.Reset(); + composite_pipeline_ready_ = false; + adj_vs_.Reset(); + adj_ps_.Reset(); + adj_sampler_.Reset(); + adj_cb_.Reset(); + adj_blend_.Reset(); + adj_raster_.Reset(); + adj_depth_.Reset(); + adj_scratch_srv_.Reset(); + adj_scratch_.Reset(); + adj_scratch_w_ = adj_scratch_h_ = 0; + adj_scratch_format_ = DXGI_FORMAT_UNKNOWN; + adj_pipeline_ready_ = false; + adj_pipeline_tried_ = false; + context_.Reset(); + device_.Reset(); + adapter_.Reset(); + initialized_ = false; +} + +void Dx11Renderer::ConfigureOsd(StereoDisplayComponent* component, + vrto3d::osd::MenuCallbacks callbacks, + void* headset_hwnd) +{ + osd_component_ = component; + osd_headset_hwnd_ = headset_hwnd; + osd_pending_callbacks_ = std::make_unique(std::move(callbacks)); + // Real OsdRenderer construction happens on the window thread on the next + // WaitAndDrawPending so all D3D11 work stays on the right thread. +} + +void Dx11Renderer::SetOsdHeadsetHwnd(void* hwnd) +{ + osd_headset_hwnd_ = hwnd; + if (osd_renderer_) osd_renderer_->SetHeadsetHwnd(hwnd); +} + + +void Dx11Renderer::RequestScreenshot(std::string app_name) +{ + std::lock_guard lk(screenshot_mutex_); + pending_screenshot_app_ = std::move(app_name); + screenshot_pending_ = true; +} + + +void Dx11Renderer::CaptureScreenshot(const std::string& app_name) +{ + if (!device_ || !context_ || !out_sbs_) return; + if (has_pending_shot_) { + // Previous shot still draining (staging not yet readable). Drop the + // new request rather than blow it away mid-encode. + LOG() << "Screenshot: previous capture still in flight; dropping"; + return; + } + + D3D11_TEXTURE2D_DESC desc{}; + out_sbs_->GetDesc(&desc); + + // Stage the texture for CPU read; everything pixel-side lives in screenshot.cpp. + D3D11_TEXTURE2D_DESC sd = desc; + sd.Usage = D3D11_USAGE_STAGING; + sd.BindFlags = 0; + sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + sd.MiscFlags = 0; + sd.MipLevels = 1; + sd.ArraySize = 1; + sd.SampleDesc = {1, 0}; + + Microsoft::WRL::ComPtr staging; + HRESULT hr = device_->CreateTexture2D(&sd, nullptr, &staging); + if (FAILED(hr)) { + LOG() << "Screenshot: CreateTexture2D(staging) hr=0x" << std::hex << hr; + return; + } + // Queue the GPU->staging copy. We deliberately DO NOT Map here — that + // would block the immediate context until the GPU caught up, and on a + // 120Hz panel that's enough delay for SteamVR's compositor to partially + // desync us (visible as the right eye going black and the HUD overlay + // layer dropping for a moment when the live display resumes). + context_->CopyResource(staging.Get(), out_sbs_.Get()); + + float target_eye_aspect = 0.0f; + if (osd_component_) { + target_eye_aspect = osd_component_->GetConfig().aspect_ratio; + } + + pending_shot_.staging = std::move(staging); + pending_shot_.app_name = app_name; + pending_shot_.width = desc.Width; + pending_shot_.height = desc.Height; + pending_shot_.format = desc.Format; + pending_shot_.target_eye_aspect = target_eye_aspect; + pending_shot_.age_frames = 0; + has_pending_shot_ = true; + + // Toast is set once by the background encode thread when it completes — + // see DrainPendingShot, which hands the "Screenshot: _NNNN.png" + // text to the window thread for OsdRenderer::SetText. +} + + +void Dx11Renderer::DrainPendingShot() +{ + if (!has_pending_shot_ || !context_) return; + if (!pending_shot_.staging) { + has_pending_shot_ = false; + return; + } + ++pending_shot_.age_frames; + + // Give the GPU at least one frame to finish the queued CopyResource + // before we even try to Map — there's no point spending the Map syscall + // on a guaranteed-still-drawing texture. + if (pending_shot_.age_frames < 2) return; + + D3D11_MAPPED_SUBRESOURCE mapped{}; + HRESULT hr = context_->Map(pending_shot_.staging.Get(), 0, D3D11_MAP_READ, + D3D11_MAP_FLAG_DO_NOT_WAIT, &mapped); + if (hr == DXGI_ERROR_WAS_STILL_DRAWING) { + // GPU hasn't finished the CopyResource yet. Try again next frame. + // Bail out after ~120 frames (~1 second at 120Hz) so a wedged staging + // doesn't lock the slot indefinitely. + if (pending_shot_.age_frames > 120) { + LOG() << "Screenshot: staging never became readable after " + << pending_shot_.age_frames << " frames — abandoning"; + pending_shot_.staging.Reset(); + has_pending_shot_ = false; + } + return; + } + if (FAILED(hr)) { + LOG() << "Screenshot: Map(staging,DO_NOT_WAIT) hr=0x" << std::hex << hr; + pending_shot_.staging.Reset(); + has_pending_shot_ = false; + return; + } + + const UINT row_pitch = mapped.RowPitch; + const UINT width = pending_shot_.width; + const UINT height = pending_shot_.height; + const DXGI_FORMAT fmt = pending_shot_.format; + const float target_eye_aspect = pending_shot_.target_eye_aspect; + const std::string app_name = std::move(pending_shot_.app_name); + + // Copy the staging bytes into a writable buffer with alpha forced to + // 0xFF. Per-eye textures in direct mode arrive with whatever alpha the + // game wrote (often 0 — real HMDs don't sample alpha), and WIC's + // cubic-interpolation scaler would zero the RGB contribution of alpha-0 + // pixels during aspect-ratio scaling. Forcing alpha=0xFF preserves RGB. + std::vector opaque_buf(static_cast(row_pitch) * height); + { + const BYTE* src_row = static_cast(mapped.pData); + BYTE* dst_row = opaque_buf.data(); + for (UINT y = 0; y < height; ++y) { + memcpy(dst_row, src_row, row_pitch); + for (UINT x = 0; x < width; ++x) { + dst_row[x * 4 + 3] = 0xFF; + } + src_row += row_pitch; + dst_row += row_pitch; + } + } + context_->Unmap(pending_shot_.staging.Get(), 0); + + pending_shot_.staging.Reset(); + has_pending_shot_ = false; + + std::thread([this, app_name, width, height, fmt, row_pitch, target_eye_aspect, + buf = std::move(opaque_buf)]() mutable { + // SaveStereoPair logs its own "Screenshot: saved index..." line. + auto r = vrto3d::screenshot::SaveStereoPair(app_name, + width, height, + fmt, + buf.data(), row_pitch, + target_eye_aspect); + // Hand the result toast text back to the window thread to display + // via SetText. Doing it here on the background thread would race + // OSD lifetime at driver shutdown; the window thread picks this up + // on its next WaitAndDrawPending tick under the mutex. + char msg[256]; + if (r.ok) { + _snprintf_s(msg, _TRUNCATE, + "Screenshot: %s_%04d.png", + app_name.empty() ? "vrto3d" : app_name.c_str(), r.index); + } else { + _snprintf_s(msg, _TRUNCATE, "Screenshot failed (see log)"); + } + std::lock_guard lk(screenshot_result_mutex_); + pending_screenshot_toast_ = msg; + }).detach(); +} + diff --git a/vrto3d/src/dx11_renderer.h b/vrto3d/src/dx11_renderer.h new file mode 100644 index 00000000..0eb843c2 --- /dev/null +++ b/vrto3d/src/dx11_renderer.h @@ -0,0 +1,385 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "openvr_driver.h" +#include "platform.h" +#include "presenter/output_presenter.h" +#include "vrto3dlib/stereo_config.h" + +class StereoDisplayComponent; +namespace vrto3d { +class AutoDepthAnalyzer; +namespace osd { +class OsdRenderer; +struct MenuCallbacks; +} // namespace osd +} // namespace vrto3d + + +// Owns the DX11 device used to import the SteamVR-composited texture, maintains +// a persistent copy (out_sbs_), and forwards it to the selected presenter. +class Dx11Renderer { +public: + Dx11Renderer(); + ~Dx11Renderer(); + + // Picks the adapter matching `adapter_luid`, creates the D3D11 device, and + // constructs+initializes the presenter selected by cfg.output_mode. + bool Init(LUID adapter_luid, + const StereoDisplayDriverConfiguration& cfg, + const vrto3d::FocusContext& focus); + + // Per-layer eye-texture pair, passed from DirectModeComponent::Present. + // The textures are owned by the DirectModeComponent's swap-texture pool; + // the ComPtr here keeps them alive until the window thread is done with + // the frame. + struct DirectModeLayerPair { + Microsoft::WRL::ComPtr left; + Microsoft::WRL::ComPtr right; + // Per-eye UV sub-rect within the source texture that holds the + // actually-rendered pixels. Games can allocate a larger texture (e.g. + // sized for max FFR / supersample) and submit bounds < (0,0,1,1) to + // tell the compositor which portion is valid. Ignoring this turns the + // valid region into a picture-in-picture inside an oversized eye. + vr::VRTextureBounds_t bounds_left { 0.f, 0.f, 1.f, 1.f }; + vr::VRTextureBounds_t bounds_right { 0.f, 0.f, 1.f, 1.f }; + // Pid that allocated the underlying swap texture set. Used only by + // the Present-time diagnostic log so we can correlate a misbehaving + // overlay layer with the process responsible for filling it. + uint32_t pid = 0; + }; + static constexpr int kMaxLayers = 8; + + // Called from IVRDriverDirectModeComponent::Present (compositor thread). + // Composes the per-layer eye textures into out_sbs_ synchronously on the + // compositor thread, gated by an AcquireSync/ReleaseSync pair around the + // syncTexture's keyed mutex (matches ALVR's pattern). Doing the read+ + // composite here while the syncTexture mutex is held guarantees we never + // sample mid-render from UEVR's eye textures, which is the root cause of + // the "frame jumping back" stutter we saw with the deferred-window-thread + // design. The immediate context is shared with the window thread via + // context_mutex_, so only one side at a time issues D3D11 commands. + // Layer 0 is the base scene (CopySubresourceRegion); layers 1+ are + // alpha-blended via the composite shader pipeline. + void OnDirectModeFrame(const DirectModeLayerPair* layers, + int layer_count, + vr::SharedTextureHandle_t sync_handle); + + // Called from the presenter's window thread. Blocks up to timeout_ms for a + // pending frame, then opens the shared texture, copies to out_sbs_, and + // invokes presenter->PresentFrame. Returns true if a frame was drained. + // All immediate-context work happens here so the window thread (which owns + // the swapchain via CreateSwapChainForHwnd) is the one issuing Present — + // DXGI/DWM drops cross-thread Presents silently with DISCARD swap effect. + bool WaitAndDrawPending(int timeout_ms); + + void Shutdown(); + + // Stash OSD configuration. The OsdRenderer is lazy-initialized on the + // window thread the first time WaitAndDrawPending sees a frame (so that + // per-eye dimensions are known). May be called before or after Init. + void ConfigureOsd(StereoDisplayComponent* component, + vrto3d::osd::MenuCallbacks callbacks, + void* headset_hwnd); + + // Update the headset_hwnd used by the OSD for cursor coord mapping. + // Used by presenters whose display surface is owned by an external + // process (WibbleWobble) and only becomes available some time after + // ConfigureOsd was called. + void SetOsdHeadsetHwnd(void* hwnd); + + // Request that the next composited stereo frame be saved to disk as two + // PNG images under \steamapps\common\SteamVR\screenshots: + // one as-is (parallel-view) and one with eyes swapped (cross-view). The + // capture happens on the window thread inside WaitAndDrawPending, after + // the SBS copy completes but before the OSD is composited in. + void RequestScreenshot(std::string app_name); + + // Returns the OSD renderer pointer (may be null until first frame). + // Used by hmd_device_driver to push toast text and toggle the menu. + vrto3d::osd::OsdRenderer* Osd() { return osd_renderer_.get(); } + + // Live access to the active StereoDisplayComponent. Used by presenters + // to poll mid-session-tunable fields (e.g. eye_swap) each frame instead + // of caching the value at Init. + StereoDisplayComponent* Component() { return osd_component_; } + + // Live access to the active presenter — used by OSD callbacks that need + // to poke presenter-specific behavior (e.g. LeiaSR head-pose calibrate). + vrto3d::IOutputPresenter* Presenter() { return presenter_.get(); } + + // VR application lifecycle hooks. Called from MyDeviceProvider::RunFrame + // when SteamVR posts VREvent_ProcessConnected / ProcessDisconnected. The + // disconnect path drops the per-app swap-texture pool entries from the + // DirectModeComponent and pauses the renderer so we don't keep submitting + // work using stale shared-texture handles while no app is active. + void OnAppDisconnect(); + void OnAppConnect(); + + // Accessors for the presenter. + ID3D11Device* Device() const { return device_.Get(); } + ID3D11DeviceContext* Context() const { return context_.Get(); } + ID3D11Texture2D* CurrentSbsTexture() const { return out_sbs_.Get(); } + uint32_t SbsWidth() const { return sbs_width_; } + uint32_t SbsHeight() const { return sbs_height_; } + + // Latched once a DXGI_ERROR_DEVICE_REMOVED/RESET/HUNG is observed. + // Callers (e.g. DirectModeComponent::CreateSwapTextureSet) must check + // this and stop submitting work — otherwise they spam-fail on the dead + // device, which has been observed to take down vrcompositor. + bool IsDeviceDead() const { + return device_dead_.load(std::memory_order_acquire); + } + + // Public mark-dead entry point for callers outside this class that get + // DEVICE_REMOVED/RESET/HUNG from their own D3D11 calls (e.g. + // DirectModeComponent::CreateTexture2D). Returns true if the HRESULT + // was a terminal device state. + bool MarkDeviceDeadIfTerminal(HRESULT hr, const char* origin) { + return CheckAndMarkDeviceDead(hr, origin); + } + + // Frame counter + last-vsync timestamp (QPC seconds), updated each time + // the window thread completes a present. + uint64_t FrameCounter() const { return frame_counter_.load(std::memory_order_relaxed); } + double LastVsyncQpcSec() const { return last_vsync_qpc_sec_.load(std::memory_order_relaxed); } + +private: + void EnsureOutputTexture(const D3D11_TEXTURE2D_DESC& incoming); + void VsyncTickThread(); + + // Circuit-breaker: once set, OnDirectModeFrame and WaitAndDrawPending + // become no-ops. Set by CheckAndMarkDeviceDead on the first observation + // of a device-removed/reset/hung HRESULT. Cleared only by re-Init (not + // exposed here), so SteamVR restart is required to recover. + std::atomic device_dead_{false}; + HRESULT device_removed_reason_ = S_OK; + bool CheckAndMarkDeviceDead(HRESULT hr, const char* origin); + + // Pause-on-disconnect: set by OnAppDisconnect, cleared by OnAppConnect. + // The renderer skips OnDirectModeFrame while paused so we don't try to + // import the just-departed game's stale shared-texture handles. + std::atomic paused_for_disconnect_{false}; + + Microsoft::WRL::ComPtr device_; + Microsoft::WRL::ComPtr context_; + Microsoft::WRL::ComPtr adapter_; + + Microsoft::WRL::ComPtr out_sbs_; + uint32_t sbs_width_ = 0; + uint32_t sbs_height_ = 0; + DXGI_FORMAT sbs_format_ = DXGI_FORMAT_UNKNOWN; + + std::unique_ptr presenter_; + + std::atomic frame_counter_ {0}; + std::atomic last_vsync_qpc_sec_ {0.0}; + double qpc_freq_sec_ {0.0}; + + // Last-seen per-eye dimensions, for one-line-per-change diagnostic. + UINT last_left_w_ {0}; + UINT last_left_h_ {0}; + UINT last_right_w_ {0}; + UINT last_right_h_ {0}; + UINT last_eff_w_ {0}; + UINT last_eff_h_ {0}; + DXGI_FORMAT last_eye_format_ {DXGI_FORMAT_UNKNOWN}; + + // Dedicated VsyncEvent timer thread. Drives the compositor's submit + // cadence at a stable per-eye-frequency tick (matches HMD properties + // wired up in MockControllerDeviceDriver::Activate — half the panel + // rate for frame-sequential output modes like WibbleWobble/NvStereoDX9). + std::thread vsync_thread_; + std::atomic vsync_stop_{false}; + std::chrono::nanoseconds frame_interval_ns_{8'333'333}; // refined from cfg in Init + + // Compositor → window thread handoff. OnDirectModeFrame sets ready=true + // after composite + Flush so the window thread wakes immediately and + // presents the freshly-composed out_sbs_. Matches the smooth pacing of + // the old IVRVirtualDisplay path — the window thread runs at the + // compositor's submit rate rather than a phase-offset internal timer, + // which minimizes the latency variance between compositor frame arrival + // and display update. + std::condition_variable composite_cv_; + std::mutex composite_ready_mutex_; + bool composite_ready_ = false; + + // Serializes immediate-context access between the compositor thread + // (OnDirectModeFrame: layer copies + composite + Flush) and the window + // thread (WaitAndDrawPending: OSD render + presenter PresentFrame). + // Held briefly on each side — ~1ms typical — so the compositor never + // waits significantly on us, and vice versa. + std::mutex context_mutex_; + + // Compositor -> window-thread frame handoff. Only the latest pending + // layer batch is kept; if the window thread can't keep up, older frames + // are dropped (matches the compositor's expected pacing). The per-layer + // ComPtrs keep the swap textures alive even if DestroySwapTextureSet + // fires between the stash and the drain. + std::mutex pending_mutex_; + std::condition_variable pending_cv_; + DirectModeLayerPair pending_layers_[kMaxLayers]{}; + int pending_layer_count_ = 0; + vr::SharedTextureHandle_t pending_sync_handle_ = 0; + bool pending_ready_ = false; + + // Cache of shared textures opened from the compositor, keyed by handle. + // The compositor reuses sync texture handles across frames; reopening a + // fresh COM wrapper every frame loses DXGI's internal keyed-mutex state. + std::unordered_map> + shared_texture_cache_; + + bool initialized_ = false; + + // Composite pipeline state — used to alpha-blend overlay layers (layer + // 1+) onto out_sbs_. Layer 0 is normally a straight CopySubresourceRegion, + // but when the per-eye source format differs from out_sbs_'s format + // (e.g. RGBA8 source and BGRA-forced out_sbs_ in NvidiaDX9 mode) layer 0 + // also routes through this shader path so the sampler does the byte-order + // conversion. The opaque blend state is used for that layer-0 pass so the + // base scene fully overwrites the target. + Microsoft::WRL::ComPtr composite_vs_; + Microsoft::WRL::ComPtr composite_ps_; + Microsoft::WRL::ComPtr composite_sampler_; + Microsoft::WRL::ComPtr composite_blend_; // straight-alpha (layers 1+) + Microsoft::WRL::ComPtr composite_blend_opaque_; // replace (layer 0 fallback) + Microsoft::WRL::ComPtr composite_raster_; + Microsoft::WRL::ComPtr composite_depth_; + + // NvidiaDX9 mode forces out_sbs_ to DXGI_FORMAT_B8G8R8A8_UNORM regardless + // of the per-eye source format: NV3D-Lib rejects non-BGRA input textures + // (its D3D9 side opens the shared handle as D3DFMT_A8R8G8B8, which + // requires BGRA byte order), and NvStereoDx9Presenter's staging ring + // copies out_sbs_'s exact desc. Other output modes keep out_sbs_ at the + // source format. + OutputMode output_mode_ = OutputMode::SbS; + + // NvidiaDX9 mode also forces out_sbs_ to FIXED panel-derived dimensions + // (panel_w*2 × panel_h) regardless of source per-eye size. The presenter + // sizes its shared staging ring from out_sbs_'s desc and NV3D-Lib + // identity-caches the D3D9 imports of the ring slots — a fixed-size + // out_sbs_ means the ring is created once and the imports stay stable + // across landscape↔game transitions (empirically NV3D's per-eye routing + // got stuck on the left eye when the imported resource was recreated). + // Layer composite scales per-eye source to fill the panel-sized halves. + uint32_t nv_panel_w_ = 0; + uint32_t nv_panel_h_ = 0; + + // Cached RTV on out_sbs_, recreated whenever out_sbs_ is recreated. + Microsoft::WRL::ComPtr out_sbs_rtv_; + bool composite_pipeline_ready_ = false; + + // Per-source-texture SRV cache for the composite blit. UEVR and the + // compositor reuse a small set of swap textures across frames, so we + // hash by ID3D11Texture2D* and keep the SRV alive across the session. + // Cleared whenever out_sbs_ is (re)created (covers game-start/-exit + // boundaries when the underlying textures get destroyed). + std::unordered_map> + composite_srv_cache_; + + // OSD overlay (lazy-initialized on first frame so eye dims are known). + std::unique_ptr osd_renderer_; + StereoDisplayComponent* osd_component_ = nullptr; + void* osd_headset_hwnd_ = nullptr; + std::unique_ptr osd_pending_callbacks_; + bool osd_initialized_ = false; + + // Pending screenshot request — drained by WaitAndDrawPending. + std::mutex screenshot_mutex_; + std::string pending_screenshot_app_; + bool screenshot_pending_ = false; + + // Background-encode result handoff: the encode thread writes the toast + // text here, the window thread picks it up on its next tick and calls + // OsdRenderer::SetText. Separated from screenshot_mutex_ since the + // encode thread runs much later than the capture request. + std::mutex screenshot_result_mutex_; + std::string pending_screenshot_toast_; + + // Deferred staging readback. Synchronous Map(MAP_READ) blocks the + // immediate context for as long as it takes the GPU to flush the + // CopyResource — at 120Hz that's enough delay for SteamVR's compositor + // to partially desync us (we'd briefly drop a layer when it caught back + // up). Instead we queue the CopyResource on the screenshot frame and + // poll Map(DO_NOT_WAIT) on subsequent frames until it succeeds. + struct PendingShot { + Microsoft::WRL::ComPtr staging; + std::string app_name; + uint32_t width = 0; + uint32_t height = 0; + DXGI_FORMAT format = DXGI_FORMAT_UNKNOWN; + float target_eye_aspect = 0.0f; + // Number of WaitAndDrawPending iterations since the CopyResource was + // queued — used both for an early-frame guard (give the GPU at least + // one frame to finish) and a timeout (give up if the staging is + // never readable, so the slot doesn't stay stuck). + int age_frames = 0; + }; + PendingShot pending_shot_; + bool has_pending_shot_ = false; + + void CaptureScreenshot(const std::string& app_name); + void DrainPendingShot(); + + // Auto-depth disparity analyzer. Owns the compute pipeline + readback + // ring; lazy-initialized on first frame where auto-depth is enabled. + std::unique_ptr auto_depth_; + + // Display-correction shader pass — fullscreen post-process applied to + // out_sbs_ (post-OSD, pre-presenter) to reduce visible crosstalk on + // displays prone to it (e.g. passive row-interlaced 3D monitors). Lazy- + // initialized on first enable; entirely skipped when cfg.shader_enabled + // is false (zero perf cost when off). Scratch texture is local-only + // (no MISC_SHARED) since presenters consume out_sbs_, not the scratch. + Microsoft::WRL::ComPtr adj_vs_; + Microsoft::WRL::ComPtr adj_ps_; + Microsoft::WRL::ComPtr adj_sampler_; + Microsoft::WRL::ComPtr adj_cb_; + Microsoft::WRL::ComPtr adj_blend_; + Microsoft::WRL::ComPtr adj_raster_; + Microsoft::WRL::ComPtr adj_depth_; + Microsoft::WRL::ComPtr adj_scratch_; + Microsoft::WRL::ComPtr adj_scratch_srv_; + uint32_t adj_scratch_w_ = 0; + uint32_t adj_scratch_h_ = 0; + DXGI_FORMAT adj_scratch_format_ = DXGI_FORMAT_UNKNOWN; + bool adj_pipeline_ready_ = false; + bool adj_pipeline_tried_ = false; + + // One-shot init for the display-correction pipeline (shaders + states). + // Called the first time ApplyDisplayCorrection() runs with the shader + // enabled, so machines that never enable it pay no init cost. + bool EnsureDisplayCorrectionPipeline(); + // Runs the pass in-place on out_sbs_. Reads live config via + // osd_component_->GetConfig(); no-op when disabled or when out_sbs_/RTV + // aren't ready. Recreates the scratch texture on dim/format change. + void ApplyDisplayCorrection(); +}; diff --git a/vrto3d/src/focus_context.h b/vrto3d/src/focus_context.h new file mode 100644 index 00000000..0683072a --- /dev/null +++ b/vrto3d/src/focus_context.h @@ -0,0 +1,38 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include +#include + +namespace vrto3d { + +// Flags observed by the presenter's window z-order thread. Owned by +// MockControllerDeviceDriver; passed by non-owning pointers to avoid +// ref-counting concerns. +struct FocusContext { + std::atomic* is_on_top = nullptr; + std::atomic* man_on_top = nullptr; + std::atomic* app_pid = nullptr; + // Live mirror of config.auto_focus. The driver re-writes this whenever + // a profile is (re)loaded or the OSD checkbox toggles, so presenter + // focus loops see the change on the next iteration instead of being + // stuck with the value cached at Init(). + std::atomic* auto_focus = nullptr; +}; + +} // namespace vrto3d diff --git a/vrto3d/src/focus_policy.h b/vrto3d/src/focus_policy.h new file mode 100644 index 00000000..254ed62f --- /dev/null +++ b/vrto3d/src/focus_policy.h @@ -0,0 +1,79 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include + +namespace vrto3d { + +// Shared "should the overlay be on top?" decision, used by every presenter +// focus loop — Windows WindowPresenter / LeiaSrPresenter / +// WibbleWobblePresenter / NvStereoDx9Presenter (where "on top" maps to the +// NV3D-Lib popup's SetVisible) and the Linux VkRenderer present-thread focus +// block — so they can't drift. Pure logic over the FocusContext atoms (read +// into these fields by the caller) plus a per-caller latch. +// +// Rule: start lowered; man_on_top (Ctrl+F8 / "Always on Top") forces on top; +// a still-running already-focused app stays on top; and a newly-connected app +// auto-raises exactly once per PID (latching is_on_top/man_on_top via the +// out-params so the user can Ctrl+F8 it back down). The caller supplies any +// extra force-on-top signal (e.g. an open OSD) via force_on_top. +struct FocusInputs { + bool is_on_top = false; // FocusContext::is_on_top + bool man_on_top = false; // FocusContext::man_on_top + bool auto_focus = true; // FocusContext::auto_focus + uint32_t app_pid = 0; // FocusContext::app_pid + bool app_running = false; // platform IsProcessRunning(app_pid) + bool force_on_top = false; // caller override (OSD open, etc.) +}; + +struct FocusLatchState { + uint32_t last_auto_focused_pid = 0; +}; + +// Returns whether the overlay should be on top this tick. May set +// *set_is_on_top / *set_man_on_top true (the auto-focus latch) — the caller +// writes those back into its atomics. Mirrors WindowPresenter::FocusThreadLoop. +inline bool ComputeWantOnTop(const FocusInputs& in, FocusLatchState& latch, + bool* set_is_on_top, bool* set_man_on_top) +{ + *set_is_on_top = false; + *set_man_on_top = false; + + // Reset the latch when the tracked app is gone so a relaunch re-triggers. + if (in.app_pid == 0 || !in.app_running) + latch.last_auto_focused_pid = 0; + + if (in.force_on_top) + return true; + if (in.man_on_top) + return true; + if (in.is_on_top && in.app_running) + return true; + if (in.auto_focus && !in.is_on_top && in.app_running && in.app_pid != 0 && + in.app_pid != latch.last_auto_focused_pid) { + // Auto-raise once per new app PID; latch is_on_top+man_on_top so the + // user can still Ctrl+F8 it back down for this app. + *set_is_on_top = true; + *set_man_on_top = true; + latch.last_auto_focused_pid = in.app_pid; + return true; + } + return false; +} + +} // namespace vrto3d diff --git a/vrto3d/src/hmd_device_driver.cpp b/vrto3d/src/hmd_device_driver.cpp index 9d55a78f..97706247 100644 --- a/vrto3d/src/hmd_device_driver.cpp +++ b/vrto3d/src/hmd_device_driver.cpp @@ -17,11 +17,33 @@ #define WIN32_LEAN_AND_MEAN #include "hmd_device_driver.h" -#include "vrto3dlib/key_mappings.h" +#ifdef _WIN32 +#include "direct_mode_component.h" +#include "dx11_renderer.h" +#include "vrto3dlib/key_codes.h" +#else +#include "vk/direct_mode_component_vk.h" +#include "vk/vk_renderer.h" +#include "vrto3dlib/key_codes.h" +#endif +#include "platform.h" #include "vrto3dlib/json_manager.h" +#include "osd/osd_renderer.h" +#include "osd/osd_menu.h" +#include "vr_recenter.h" + +#ifdef _WIN32 +#include +#include +#include +#pragma comment(lib, "urlmon.lib") +#endif #include "vrto3dlib/app_id_mgr.h" -#include "vrto3dlib/overlay_mgr.h" +#ifdef _WIN32 #include "vrto3dlib/win32_helper.hpp" +#else +#include "vrto3dlib/linux_helper.hpp" +#endif #include "vrmath.h" #include @@ -30,12 +52,62 @@ #include #include #include +#include +#ifdef _WIN32 #include #include #pragma comment (lib, "WSock32.Lib") #include #include +#else +#include +#include +#include +#include +#include +#include +#include +#include +#include "vrto3dlib/input_state.h" +// Socket-compat shims so the shared OpenTrack UDP thread body compiles +// unchanged against BSD sockets. +using SOCKET = int; +using DWORD = uint32_t; +static constexpr int INVALID_SOCKET = -1; +static constexpr int SOCKET_ERROR = -1; +static inline int closesocket(int s) { return close(s); } +// XINPUT_STATE-shaped snapshot filled from the evdev gamepad backend. +struct PortableGamepad { + uint16_t wButtons = 0; + uint8_t bLeftTrigger = 0, bRightTrigger = 0; + int16_t sThumbLX = 0, sThumbLY = 0, sThumbRX = 0, sThumbRY = 0; +}; +struct _XINPUT_STATE { + uint32_t dwPacketNumber = 0; + PortableGamepad Gamepad; +}; +static inline uint32_t XInputGetStateShim(uint32_t /*idx*/, _XINPUT_STATE* out) +{ + const auto pad = vrto3d::input::GetGamepadState(); + out->Gamepad.wButtons = pad.wButtons; + out->Gamepad.bLeftTrigger = pad.bLeftTrigger; + out->Gamepad.bRightTrigger = pad.bRightTrigger; + out->Gamepad.sThumbLX = pad.sThumbLX; + out->Gamepad.sThumbLY = pad.sThumbLY; + out->Gamepad.sThumbRX = pad.sThumbRX; + out->Gamepad.sThumbRY = pad.sThumbRY; + return pad.connected ? 0u : 1167u; // ERROR_DEVICE_NOT_CONNECTED +} +#define _XInputGetState XInputGetStateShim +#ifndef ERROR_SUCCESS +#define ERROR_SUCCESS 0u +#endif +static inline void SwitchToXinpuGetStateEx() {} +#ifndef ZeroMemory +#define ZeroMemory(p, s) memset((p), 0, (s)) +#endif +#endif // Load settings from default.vrsettings @@ -91,7 +163,6 @@ MockControllerDeviceDriver::MockControllerDeviceDriver() // Display settings StereoDisplayDriverConfiguration display_configuration{}; display_configuration.display_index = 0; - display_configuration.multi_display = false; display_configuration.window_x = 0; display_configuration.window_y = 0; display_configuration.window_width = 1920; @@ -102,7 +173,13 @@ MockControllerDeviceDriver::MockControllerDeviceDriver() json_manager.LoadProfileFromJson(DEF_CFG, display_configuration); // Resolve display-index-driven window bounds from the active desktop layout +#ifdef _WIN32 const bool monitor_bounds_applied = ApplyDisplaySelectionToWindowConfig(display_configuration); +#else + // Presenters fullscreen onto the selected output themselves on Linux — + // window_x/y bounds are not used for placement. + const bool monitor_bounds_applied = false; +#endif LOG() << "Pre-init window bounds before StereoDisplayComponent: resolved=" << (monitor_bounds_applied ? "true" : "false") @@ -122,12 +199,15 @@ MockControllerDeviceDriver::MockControllerDeviceDriver() //----------------------------------------------------------------------------- vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId ) { + // Direct mode registers the HMD once; no sibling DisplayRedirect object + // needs activating. + is_active_.store(true); device_index_ = unObjectId; - is_active_ = true; + // Start NOT on top (both platforms): the overlay lowers to reveal the + // desktop/game until an app connects to SteamVR, at which point the + // auto-focus path raises it (mirrored on Linux in VkRenderer's focus loop). is_on_top_ = false; man_on_top_ = false; - ue3d_on_top_ = false; - take_screenshot_ = false; app_updated_ = false; no_profile_ = false; @@ -146,6 +226,57 @@ vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId ) vrp->SetStringProperty( container, vr::Prop_TrackingFirmwareVersion_String, "1.0"); vrp->SetStringProperty( container, vr::Prop_HardwareRevision_String, "1.0"); + // Query the target output monitor (resolved by display_index) for + // display frequency + approximate vsync-to-photons latency, then write + // the computed values back into the config so downstream consumers + // (pose thread cadence, hotkey sleep counts) pick them up. + { + auto cfg = stereo_display_component_->GetConfig(); + platform::MonitorInfo primary{}, secondary{}; + // JSON-supplied display_frequency overrides auto-detection. 0 (the + // default) means "ask the monitor". + if (cfg.display_frequency <= 0.0f) { + if (platform::ResolveTargetMonitors(cfg.display_index, false, primary, secondary)) { + cfg.display_frequency = platform::QueryRefreshHz(primary, 60.0f); + } else { + cfg.display_frequency = 60.0f; + } + } else { + // Still resolve the monitor so the log line reports the target. + platform::ResolveTargetMonitors(cfg.display_index, false, primary, secondary); + } + + // Frame-sequential modes (NVIDIA 3D Vision, WibbleWobble lightfield) + // alternate L/R eyes at the panel's refresh rate, so the actual + // stereo-pair rate is panel_rate / 2. Producing SBS pairs faster than + // that just wastes GPU bandwidth (every other pair gets dropped) and + // adds latency through the consumer's frame queue. + const bool frame_sequential = + cfg.output_mode == OutputMode::NvidiaDX9 + || cfg.output_mode == OutputMode::WibbleWobble; + if (frame_sequential && cfg.display_frequency > 1.0f) { + const float panel_hz = cfg.display_frequency; + cfg.display_frequency = panel_hz * 0.5f; + LOG() << "Display: frame-sequential mode — halving reported " + "display freq to compositor (" << panel_hz << "Hz panel -> " + << cfg.display_frequency << "Hz stereo-pair rate)"; + } + + cfg.display_latency = (cfg.display_frequency > 1.0f) + ? (0.5f / cfg.display_frequency) : 0.011f; + cfg.sleep_count_max = (int)(floor(1600.0 / (1000.0 / cfg.display_frequency))); + stereo_display_component_->LoadSettings(cfg); + auto_focus_.store(cfg.auto_focus); + hide_cursor_.store(cfg.hide_cursor); + lock_cursor_.store(cfg.lock_cursor); + stereo_cursor_.store(cfg.stereo_cursor); + cursor_depth_.store(cfg.cursor_depth); + cursor_size_.store(cfg.cursor_size); + LOG() << "Display: target=" << primary.device_name + << " freq=" << cfg.display_frequency << "Hz" + << " latency=" << cfg.display_latency << "s"; + } + // Display settings vrp->SetFloatProperty( container, vr::Prop_UserIpdMeters_Float, stereo_display_component_->GetConfig().depth); vrp->SetFloatProperty( container, vr::Prop_UserHeadToEyeDepthMeters_Float, 0.f); @@ -155,7 +286,21 @@ vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId ) vrp->SetBoolProperty( container, vr::Prop_ReportsTimeSinceVSync_Bool, false); vrp->SetBoolProperty( container, vr::Prop_IsOnDesktop_Bool, false); vrp->SetBoolProperty( container, vr::Prop_DisplayDebugMode_Bool, true); - vrp->SetBoolProperty( container, vr::Prop_HasDriverDirectModeComponent_Bool, false); + vrp->SetBoolProperty( container, vr::Prop_HasDriverDirectModeComponent_Bool, true); + + // Direct-mode virtual-display integration: advertise our adapter LUID so + // the compositor composites on the same GPU the renderer will use, and + // signal that we fire VsyncEvent ourselves from the renderer thread. + { +#ifdef _WIN32 + LUID luid = platform::PrimaryAdapterLuid(); + uint64_t luid_u64 = (static_cast(luid.HighPart) << 32) | luid.LowPart; + vrp->SetUint64Property(container, vr::Prop_GraphicsAdapterLuid_Uint64, luid_u64); +#endif + // No LUID handshake on Linux — the compositor and driver agree on the + // GPU implicitly. + vrp->SetBoolProperty (container, vr::Prop_DriverDirectModeSendsVsyncEvents_Bool, true); + } if (stereo_display_component_->GetConfig().dash_enable) { vrp->SetFloatProperty(container, vr::Prop_DashboardScale_Float, 1.0f); @@ -169,7 +314,11 @@ vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId ) // Get the current time std::time_t t = std::time(nullptr); std::tm tm; +#ifdef _WIN32 localtime_s(&tm, &t); +#else + localtime_r(&t, &tm); +#endif // Construct the JSON string with variables std::stringstream ss; ss << R"( @@ -268,7 +417,11 @@ vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId ) { std::thread([launch_script]() { LOG() << "Executing launch_script: " << launch_script.c_str(); +#ifdef _WIN32 const std::string command = "cmd.exe /C " + launch_script; +#else + const std::string command = launch_script; // /bin/sh via system() +#endif const int result = std::system(command.c_str()); if (result == 0) { LOG() << "launch_script completed successfully"; @@ -279,18 +432,23 @@ vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId ) }).detach(); } - // Thread setup + // Thread setup. FocusUpdateThread is gone — its responsibilities (window + // placement + always-on-top) now live inside WindowPresenter, which is + // owned by VirtualDisplayDevice and observes the focus atomics below via + // GetFocusContext(). xinput_thread_ = std::thread(&MockControllerDeviceDriver::XInputUpdateThread, this); pose_thread_ = std::thread(&MockControllerDeviceDriver::PoseUpdateThread, this); hotkey_thread_ = std::thread(&MockControllerDeviceDriver::PollHotkeysThread, this); - focus_thread_ = std::thread(&MockControllerDeviceDriver::FocusUpdateThread, this); - depth_thread_ = std::thread(&MockControllerDeviceDriver::AutoDepthThread, this); - if (stereo_display_component_->GetConfig().use_open_track) { - open_track_att_ = HmdQuaternion_Identity; - open_track_pos_ = { 0.0, 0.0, 0.0 }; - track_thread_ = std::thread(&MockControllerDeviceDriver::OpenTrackThread, this); - } - + monitor_thread_ = std::thread(&MockControllerDeviceDriver::MonitorModeThread, this); + // Always start OpenTrack listener; the use_open_track flag is checked at + // consumption time (PoseUpdateThread / Stereo component) so it can be + // toggled live from the OSD without restarting the driver. + open_track_att_ = HmdQuaternion_Identity; + open_track_pos_ = { 0.0, 0.0, 0.0 }; + track_thread_ = std::thread(&MockControllerDeviceDriver::OpenTrackThread, this); + cursor_thread_ = std::thread(&MockControllerDeviceDriver::CursorControlThread, this); + +#ifdef _WIN32 HANDLE thread_handle = pose_thread_.native_handle(); // Set the thread priority @@ -298,6 +456,383 @@ vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId ) // Handle error if setting priority fails LOG() << "Failed to set thread priority: " << GetLastError(); } +#else + { + sched_param sch{}; + sch.sched_priority = 0; + pthread_setschedparam(pose_thread_.native_handle(), SCHED_OTHER, &sch); + // Global input needs /dev/input access (user in the `input` group). + if (!vrto3d::input::Start()) { + LOG() << "evdev input unavailable — hotkeys/gamepad dead. " + "Add your user to the `input` group: sudo usermod -aG input $USER"; + } + } +#endif + + // Direct mode: build the platform renderer + selected presenter, then + // stand up the IVRDriverDirectModeComponent that hands per-eye game + // textures through to the renderer. + { + renderer_ = std::make_unique(); +#ifdef _WIN32 + LUID luid = platform::PrimaryAdapterLuid(); + const bool renderer_ok = + renderer_->Init(luid, stereo_display_component_->GetConfig(), GetFocusContext()); +#else + const bool renderer_ok = + renderer_->Init(stereo_display_component_->GetConfig(), GetFocusContext()); +#endif + if (!renderer_ok) { + LOG() << "Renderer Init failed; direct-mode path will be inactive"; + renderer_.reset(); + } else { + direct_mode_component_ = std::make_unique(renderer_.get()); + // Wire OSD callbacks. The OsdRenderer is lazy-initialized on the + // window thread when the first frame arrives. + vrto3d::osd::MenuCallbacks cb; + cb.save_game_profile = [this](std::string toast) { + if (prev_name_.empty()) return; + auto cfg = stereo_display_component_->GetConfig(); + JsonManager().SaveProfileToJson(prev_name_ + "_config.json", cfg); + if (renderer_ && renderer_->Osd()) renderer_->Osd()->SetText(toast); + }; + cb.save_default_profile = [this](std::string toast) { + auto cfg = stereo_display_component_->GetConfig(); + // SaveFullConfigToJson writes every key (display_index, + // output_mode, render dims, OpenTrack, track filter, LeiaSR, + // launch_script, etc.) — required so System-tab edits persist. + JsonManager().SaveFullConfigToJson(DEF_CFG, cfg); + if (renderer_ && renderer_->Osd()) renderer_->Osd()->SetText(toast); + }; + cb.reload_game_profile = [this](std::string toast) { + if (prev_name_.empty()) return; + auto cfg = stereo_display_component_->GetConfig(); + if (JsonManager().LoadProfileFromJson(prev_name_ + "_config.json", cfg)) { + stereo_display_component_->LoadSettings(cfg); + SetAsync(cfg.async_enable); + auto_focus_.store(cfg.auto_focus); + hide_cursor_.store(cfg.hide_cursor); + lock_cursor_.store(cfg.lock_cursor); + stereo_cursor_.store(cfg.stereo_cursor); + cursor_depth_.store(cfg.cursor_depth); + cursor_size_.store(cfg.cursor_size); + app_name_ = prev_name_; + if (renderer_ && renderer_->Osd()) { + renderer_->Osd()->SetAppName(app_name_); + renderer_->Osd()->SetText(toast); + } + } + }; + cb.reload_default_profile = [this](std::string toast) { + auto cfg = stereo_display_component_->GetConfig(); + // LoadProfileFromJson only reads per-profile fields; for the + // default config we also need to refresh global driver fields + // (display_index, output_mode, render dims, hmd_x/y/yaw, + // OpenTrack, track filter, LeiaSR sens, async_enable, etc). + // LoadParamsFromJson reads exactly that superset. + JsonManager().LoadParamsFromJson(cfg); + if (JsonManager().LoadProfileFromJson(DEF_CFG, cfg)) { + stereo_display_component_->LoadSettings(cfg); + SetAsync(cfg.async_enable); + auto_focus_.store(cfg.auto_focus); + hide_cursor_.store(cfg.hide_cursor); + lock_cursor_.store(cfg.lock_cursor); + stereo_cursor_.store(cfg.stereo_cursor); + cursor_depth_.store(cfg.cursor_depth); + cursor_size_.store(cfg.cursor_size); + app_name_ = ""; + if (renderer_ && renderer_->Osd()) { + renderer_->Osd()->SetAppName(app_name_); + renderer_->Osd()->SetText(toast); + } + } + }; + cb.reset_defaults = [this](std::string toast) { + // Factory reset: start from a fresh (in-code default) config, + // but keep the display/output hardware fields from the live + // config so a reset restores the stereo/shader/tracking + // tunables without scrambling the user's monitor selection, + // render dimensions, or refresh — those are the + // "Requires Restart" fields and are hardware-specific. + auto cur = stereo_display_component_->GetConfig(); + StereoDisplayDriverConfiguration def{}; + def.display_index = cur.display_index; + def.output_mode = cur.output_mode; + def.window_x = cur.window_x; + def.window_y = cur.window_y; + def.window_width = cur.window_width; + def.window_height = cur.window_height; + def.render_width = cur.render_width; + def.render_height = cur.render_height; + def.aspect_ratio = cur.aspect_ratio; + def.display_frequency = cur.display_frequency; + def.display_latency = cur.display_latency; + def.sleep_count_max = cur.sleep_count_max; + stereo_display_component_->LoadSettings(def); + SetAsync(def.async_enable); + auto_focus_.store(def.auto_focus); + hide_cursor_.store(def.hide_cursor); + lock_cursor_.store(def.lock_cursor); + stereo_cursor_.store(def.stereo_cursor); + cursor_depth_.store(def.cursor_depth); + cursor_size_.store(def.cursor_size); + if (renderer_ && renderer_->Osd()) renderer_->Osd()->SetText(toast); + }; + cb.reset_projection = [this]() { + stereo_display_component_->ResetProjection(); + }; + cb.get_auto_depth_enabled = [this]() { + return stereo_display_component_->IsAutoDepthEnabled(); + }; + cb.set_auto_depth_enabled = [this](bool on) { + stereo_display_component_->SetAutoDepthEnabled(on); + }; + cb.get_auto_depth_target = [this]() { + return stereo_display_component_->GetAutoDepthTargetDisparity(); + }; + cb.set_auto_depth_target = [this](float v) { + stereo_display_component_->SetAutoDepthTargetDisparity(v); + }; + cb.get_auto_depth_smoothing = [this]() { + return stereo_display_component_->GetAutoDepthSmoothing(); + }; + cb.set_auto_depth_smoothing = [this](float v) { + stereo_display_component_->SetAutoDepthSmoothing(v); + }; + cb.get_auto_depth_logging = [this]() { + return stereo_display_component_->IsAutoDepthLoggingEnabled(); + }; + cb.set_auto_depth_logging = [this](bool on) { + stereo_display_component_->SetAutoDepthLoggingEnabled(on); + }; + cb.calibrate_leiasr_head = [this]() { +#ifdef _WIN32 + if (renderer_ && renderer_->Presenter()) { + renderer_->Presenter()->RequestCalibrate(); + if (renderer_->Osd()) + renderer_->Osd()->SetText("LeiaSR head pose calibrated"); + } +#endif // LeiaSR is a Windows-only presenter + }; + cb.recenter_pose = [this]() { + // Raise the flag; XInputUpdateThread picks it up on its next + // 8ms tick and ConsumePoseReset zeros pitch/yaw + OT state + // and dispatches the deferred OpenVR ResetZeroPose. + stereo_display_component_->RequestPoseReset(); + if (renderer_ && renderer_->Osd()) + renderer_->Osd()->SetText("Recentered"); + }; + cb.toggle_always_on_top = [this]() { + is_on_top_ = !is_on_top_; + man_on_top_ = is_on_top_.load(); + }; + cb.always_on_top = [this]() { return man_on_top_.load(); }; + cb.request_game_focus = [this]() { + uint32_t pid = app_pid_.load(); + LOG() << "request_game_focus fired pid=" << pid; + if (pid == 0 || !IsProcessRunning(pid)) return; +#ifndef _WIN32 + // No cross-client focus stealing on Wayland; X11 raise lives + // in the presenter's topmost handling. + return; +#else + std::thread([this, pid]() { + // Let any held hotkey modifiers (Ctrl+Home was likely + // just used to close the menu) settle before we try + // to take foreground — the ALT-key trick inside + // ForceFocus misbehaves when Ctrl is still held. + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + if (!man_on_top_.load()) return; + HWND game_hwnd = GetHWNDFromPID(pid); + if (!game_hwnd) return; + // Skip if the game already has foreground — every + // ForceFocus call briefly raises the game window over our + // topmost VR window before the focus loop re-asserts it, + // which the user sees as a flicker. + if (GetForegroundWindow() == game_hwnd) return; + ForceFocus(game_hwnd, + GetCurrentThreadId(), + GetWindowThreadProcessId(game_hwnd, nullptr)); + LOG() << "request_game_focus fg_match=" + << (GetForegroundWindow() == game_hwnd); + }).detach(); +#endif + }; +#ifdef _WIN32 + cb.open_config_folder = [this]() { + std::string steam = GetSteamInstallPath(); + if (steam.empty()) return; + std::string path = steam + "\\config\\vrto3d"; + ShellExecuteA(nullptr, "open", path.c_str(), nullptr, nullptr, SW_SHOWNORMAL); + }; + cb.open_screenshot_folder = [this]() { + std::string steam = GetSteamInstallPath(); + if (steam.empty()) return; + std::string path = steam + "\\steamapps\\common\\SteamVR\\screenshots"; + SHCreateDirectoryExA(nullptr, path.c_str(), nullptr); + ShellExecuteA(nullptr, "open", path.c_str(), nullptr, nullptr, SW_SHOWNORMAL); + }; +#else + cb.open_config_folder = [this]() { + std::string steam = GetSteamInstallPath(); + if (steam.empty()) return; + std::string cmd = "xdg-open '" + steam + "/config/vrto3d' >/dev/null 2>&1 &"; + [[maybe_unused]] int rc = std::system(cmd.c_str()); + }; + cb.open_screenshot_folder = [this]() { + std::string steam = GetSteamInstallPath(); + if (steam.empty()) return; + std::string path = steam + "/steamapps/common/SteamVR/screenshots"; + std::error_code ec; + std::filesystem::create_directories(path, ec); + std::string cmd = "xdg-open '" + path + "' >/dev/null 2>&1 &"; + [[maybe_unused]] int rc = std::system(cmd.c_str()); + }; +#endif + cb.take_screenshot = [this]() { + if (!renderer_) return; + std::string name = !app_name_.empty() ? app_name_ + : !prev_name_.empty() ? prev_name_ + : std::string("vrto3d"); + renderer_->RequestScreenshot(name); + }; + cb.set_async = [this](bool on) { + SetAsync(on); + }; + cb.set_auto_focus = [this](bool on) { + auto_focus_.store(on); + // Disabling auto focus also drops any current always-on-top + // hold so the user's choice takes effect immediately rather + // than only blocking future auto-focus actions. + if (!on) { + is_on_top_ = false; + man_on_top_ = false; + } + }; + cb.set_hide_cursor = [this](bool on) { hide_cursor_.store(on); }; + cb.set_lock_cursor = [this](bool on) { lock_cursor_.store(on); }; + cb.set_stereo_cursor = [this](bool on) { stereo_cursor_.store(on); }; + cb.set_cursor_depth = [this](float px) { cursor_depth_.store(px); }; + cb.set_cursor_size = [this](int px) { cursor_size_.store(px); }; + cb.download_latest_profiles = [this]() { + // Re-entrancy guard — ignore clicks while a previous download + // is still in flight. + static std::atomic in_flight{false}; + bool expected = false; + if (!in_flight.compare_exchange_strong(expected, true)) { + if (renderer_ && renderer_->Osd()) + renderer_->Osd()->SetText("Profile download already running"); + return; + } + std::thread([this]{ + auto toast = [this](const std::string& msg, + std::chrono::milliseconds ttl = + std::chrono::milliseconds(3000)) { + if (renderer_ && renderer_->Osd()) + renderer_->Osd()->SetText(msg, ttl); + }; + static std::atomic& done = in_flight; + struct ResetOnExit { std::atomic& f; ~ResetOnExit(){ f.store(false); } } _r{done}; + + std::string steam = GetSteamInstallPath(); + if (steam.empty()) { + toast("Profile download failed: Steam path not found"); + return; + } +#ifndef _WIN32 + const std::string folder = steam + "/config/vrto3d"; + const std::string zip = folder + "/vrto3d_profiles.zip"; + toast("Downloading latest profiles…", std::chrono::milliseconds(60000)); + std::error_code ec; + std::filesystem::create_directories(folder, ec); + const std::string dl = + "curl -fsSL -o '" + zip + "' " + "https://github.com/oneup03/VRto3D/releases/download/latest/vrto3d_profiles.zip"; + if (std::system(dl.c_str()) != 0) { + toast("Profile download failed (curl)"); + return; + } + toast("Extracting profiles", std::chrono::milliseconds(30000)); + const std::string ex = "bsdtar -xf '" + zip + "' -C '" + folder + "'"; + const int rc = std::system(ex.c_str()); + ::remove(zip.c_str()); + if (rc != 0) { + toast("Extract failed (bsdtar)"); + } else { + toast("Profiles installed to config/vrto3d"); + } +#else + const std::string folder = steam + "\\config\\vrto3d"; + const std::string zip = folder + "\\vrto3d_profiles.zip"; + const wchar_t* url = + L"https://github.com/oneup03/VRto3D/releases/download/latest/vrto3d_profiles.zip"; + + toast("Downloading latest profiles…", std::chrono::milliseconds(60000)); + + // Make sure the destination folder exists. + CreateDirectoryA(folder.c_str(), nullptr); + + std::wstring wzip(zip.begin(), zip.end()); + HRESULT hr = URLDownloadToFileW(nullptr, url, wzip.c_str(), 0, nullptr); + if (FAILED(hr)) { + char buf[160]; + std::snprintf(buf, sizeof(buf), + "Profile download failed (hr=0x%08lX)", + static_cast(hr)); + toast(buf); + return; + } + + toast("Extracting profiles", std::chrono::milliseconds(30000)); + + // Use PowerShell's Expand-Archive to unpack — no extra + // dependency, available on every supported Windows. + std::string cmd = + "powershell.exe -NoProfile -ExecutionPolicy Bypass " + "-Command \"Expand-Archive -LiteralPath '" + zip + + "' -DestinationPath '" + folder + "' -Force\""; + + STARTUPINFOA si{}; + si.cb = sizeof(si); + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + PROCESS_INFORMATION pi{}; + std::vector cmdline(cmd.begin(), cmd.end()); + cmdline.push_back('\0'); + BOOL ok = CreateProcessA(nullptr, cmdline.data(), nullptr, nullptr, + FALSE, CREATE_NO_WINDOW, nullptr, nullptr, + &si, &pi); + DWORD exit_code = 1; + if (ok) { + WaitForSingleObject(pi.hProcess, 60000); + GetExitCodeProcess(pi.hProcess, &exit_code); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + } + DeleteFileA(zip.c_str()); + + if (!ok) { + toast("Extract failed: PowerShell unavailable"); + } else if (exit_code != 0) { + char buf[160]; + std::snprintf(buf, sizeof(buf), + "Extract failed (exit %lu)", + static_cast(exit_code)); + toast(buf); + } else { + toast("Profiles installed to config\\vrto3d"); + } +#endif + }).detach(); + }; + + // The headset HWND is created by the presenter on its window + // thread; FindWindow is the only way to discover it from here. + // OsdRenderer handles null gracefully — input mouse mapping just + // skips until the HWND becomes available. + renderer_->ConfigureOsd(stereo_display_component_.get(), std::move(cb), nullptr); + } + } LOG() << "Activation Complete"; @@ -313,6 +848,10 @@ void *MockControllerDeviceDriver::GetComponent( const char *pchComponentNameAndV { return stereo_display_component_.get(); } + if ( strcmp( pchComponentNameAndVersion, vr::IVRDriverDirectModeComponent_Version ) == 0 ) + { + return direct_mode_component_.get(); + } return nullptr; } @@ -335,7 +874,11 @@ void MockControllerDeviceDriver::OpenTrackThread() { SOCKET socket_s; struct sockaddr_in from = {}; +#ifdef _WIN32 int from_len = sizeof(from); +#else + socklen_t from_len = sizeof(from); +#endif struct TOpenTrack { double X; double Y; @@ -347,6 +890,7 @@ void MockControllerDeviceDriver::OpenTrackThread() TOpenTrack open_track; auto ot_port = stereo_display_component_->GetConfig().open_track_port; +#ifdef _WIN32 WSADATA wsaData; int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); if (iResult != 0) { @@ -378,8 +922,36 @@ void MockControllerDeviceDriver::OpenTrackThread() } } } +#else + { + struct sockaddr_in local = {}; + local.sin_family = AF_INET; + local.sin_port = htons(ot_port); + local.sin_addr.s_addr = INADDR_ANY; + + socket_s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (socket_s == INVALID_SOCKET) { + LOG() << "OpenTrack socket creation failed: " << errno; + } else { + fcntl(socket_s, F_SETFL, fcntl(socket_s, F_GETFL, 0) | O_NONBLOCK); + if (bind(socket_s, (struct sockaddr*)&local, sizeof(local)) == SOCKET_ERROR) { + LOG() << "OpenTrack bind failed: " << errno; + closesocket(socket_s); + socket_s = INVALID_SOCKET; + } + } + } +#endif while (is_active_) { + // Skip the recv pump entirely when OpenTrack is off; the OSD can flip + // use_open_track live, so we keep the thread (and bound socket) alive + // and just sleep a coarser tick while disabled. + if (!stereo_display_component_->GetConfig().use_open_track) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } + //Read UDP socket with OpenTrack data memset(&open_track, 0, sizeof(open_track)); auto bytes_read = recvfrom(socket_s, (char*)(&open_track), sizeof(open_track), 0, (sockaddr*)&from, &from_len); @@ -400,7 +972,9 @@ void MockControllerDeviceDriver::OpenTrackThread() else std::this_thread::sleep_for(std::chrono::milliseconds(1)); } closesocket(socket_s); +#ifdef _WIN32 WSACleanup(); +#endif } @@ -411,12 +985,35 @@ void MockControllerDeviceDriver::XInputUpdateThread() { float current_pitch = 0.0f; vr::HmdQuaternion_t current_yaw_quat = HmdQuaternion_Identity; + bool was_idle = false; while (is_active_) { const auto loop_start = std::chrono::steady_clock::now(); const auto config = stereo_display_component_->GetConfig(); + // When neither stick is consumed and no reset is pending, skip the + // XInput poll, math, and mutex publish. On the live→idle edge, clear + // the published controller pose once so PoseUpdateThread doesn't keep + // applying the last non-zero offset. + if (!config.pitch_enable && !config.yaw_enable && !config.pose_reset) + { + if (!was_idle) + { + current_pitch = 0.0f; + current_yaw_quat = HmdQuaternion_Identity; + { + std::lock_guard lock(controller_pose_mutex_); + controller_rotation_ = HmdQuaternion_Identity; + controller_pos_offset_ = { 0.0, 0.0, 0.0 }; + } + was_idle = true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(8)); + continue; + } + was_idle = false; + XINPUT_STATE state; ZeroMemory(&state, sizeof(XINPUT_STATE)); const bool got_xinput = (_XInputGetState(0, &state) == ERROR_SUCCESS); @@ -446,7 +1043,7 @@ void MockControllerDeviceDriver::XInputUpdateThread() { current_pitch = 0.0f; current_yaw_quat = HmdQuaternion_Identity; - stereo_display_component_->SetReset(); + ConsumePoseReset(); } const float pitch_radians = DEG_TO_RAD(current_pitch); @@ -577,6 +1174,14 @@ void MockControllerDeviceDriver::PoseUpdateThread() if (config.use_track_filter) { + // Reset on the disabled→enabled edge so the filter doesn't carry + // stale state (potentially minutes old) into its first FilterPose + // call when the OSD toggles use_track_filter on. Without this the + // initial samples produce noisy output until the filter resettles. + if (!track_filter_was_enabled_) + { + track_filter_.Reset(); + } double filtered_position[3] = { pose.vecPosition[0], pose.vecPosition[1], @@ -645,21 +1250,20 @@ vr::DriverPose_t MockControllerDeviceDriver::GetPose() void MockControllerDeviceDriver::PollHotkeysThread() { struct { int shot = 0; - int hmd = 0; int top = 0; int save = 0; - int overlay = 0; + int menu = 0; + int auto_depth = 0; } sleep; const int sleep_time = static_cast(floor(1000.0 / stereo_display_component_->GetConfig().display_frequency)); - std::string overlay_msg; - HWND overlay_hwnd = nullptr; - InitGDIPlus(); + auto getOsd = [this]() -> vrto3d::osd::OsdRenderer* { + return renderer_ ? renderer_->Osd() : nullptr; + }; auto setOverlay = [&](const std::string& msg) { - overlay_msg = msg; - sleep.overlay = stereo_display_component_->GetConfig().sleep_count_max * 3; + if (auto* osd = getOsd()) osd->SetText(msg); }; auto fmt = [&](const std::string& label, float value, int precision = 3) { std::ostringstream ss; @@ -675,39 +1279,79 @@ void MockControllerDeviceDriver::PollHotkeysThread() { setOverlay("VRto3D: " + stereo_version_number_); + // Deferred projection re-sync for the depth/convergence hotkeys. + // ResetProjection() posts SetDisplayProjectionRaw + a LensDistortionChanged + // event that most VR mods answer by rebuilding their projection; firing it + // at poll rate while a key autorepeats makes adjustment feel laggy. The + // flush at the bottom of the loop resyncs immediately on the first press, + // at most every 150ms while held, and once more on release so the final + // value always lands. + bool resync_pending = false; + auto last_resync = std::chrono::steady_clock::now() - std::chrono::seconds(1); + while (is_active_) { auto cfg = stereo_display_component_->GetConfig(); + // Ctrl+Home (keyboard) or Start+DPad-Down (gamepad) toggles the OSD + // menu. Always polled (independent of disable_hotkeys) so users can + // always recover from a runaway config. Start+Back is avoided here + // because several VR mods already use that pair as their own pause / + // menu chord. + DWORD menu_pad = 0; + const DWORD menu_chord_mask = XINPUT_GAMEPAD_START | XINPUT_GAMEPAD_DPAD_DOWN; + const bool menu_pad_chord = + GetXInputButtonState(menu_pad) && + ((menu_pad & menu_chord_mask) == menu_chord_mask); + if (((isCtrlDown() && isDown(VK_HOME)) || menu_pad_chord) && sleep.menu == 0) { + if (auto* osd = getOsd()) { + osd->ToggleMenu(); + osd->SetAppName(app_name_); + osd->SetVersion(stereo_version_number_); + } + sleep.menu = cfg.sleep_count_max; + } else if (sleep.menu > 0) { + --sleep.menu; + } + + // While the menu is open, suppress the rest of the hotkey poll so + // arrow keys / numbers / Enter reach ImGui instead of bumping depth. + if (auto* osd = getOsd(); osd && osd->MenuVisible()) { + std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time)); + continue; + } + if (!cfg.disable_hotkeys) { - // Ctrl+F3 Decrease Depth + // Ctrl+F3 Decrease Depth (re-sync projection; hold Shift to skip the sync) if (isCtrlDown() && isDown(VK_F3)) { stereo_display_component_->AdjustDepth(-0.001f, true); - if (isDown(VK_SHIFT)) stereo_display_component_->ResetProjection(); + if (!isDown(VK_SHIFT)) resync_pending = true; setOverlay(fmtDepthConv()); } - // Ctrl+F4 Increase Depth + // Ctrl+F4 Increase Depth (re-sync projection; hold Shift to skip the sync) else if (isCtrlDown() && isDown(VK_F4)) { stereo_display_component_->AdjustDepth(0.001f, true); - if (isDown(VK_SHIFT)) stereo_display_component_->ResetProjection(); + if (!isDown(VK_SHIFT)) resync_pending = true; setOverlay(fmtDepthConv()); } // Ctrl+F5 Decrease Convergence else if (isCtrlDown() && isDown(VK_F5)) { - stereo_display_component_->AdjustConvergence(0.005f, true); + stereo_display_component_->AdjustConvergence(0.005f, true, false); + resync_pending = true; setOverlay(fmtDepthConv()); } // Ctrl+F6 Increase Convergence else if (isCtrlDown() && isDown(VK_F6)) { - stereo_display_component_->AdjustConvergence(-0.005f, true); + stereo_display_component_->AdjustConvergence(-0.005f, true, false); + resync_pending = true; setOverlay(fmtDepthConv()); } // Ctrl+F7 Store settings into game profile if (isCtrlDown() && isDown(VK_F7) && sleep.save == 0) { if (!prev_name_.empty()) { - cfg.depth = stereo_display_component_->GetDepth(); - cfg.convergence = stereo_display_component_->GetConvergence(); - cfg.fov = stereo_display_component_->GetFoV(); - JsonManager().SaveProfileToJson(prev_name_ + "_config.json", cfg); + // Re-fetch so any depth/conv adjust earlier this tick is + // reflected in the saved file. + auto save_cfg = stereo_display_component_->GetConfig(); + JsonManager().SaveProfileToJson(prev_name_ + "_config.json", save_cfg); BeepSuccess(); setOverlay("Saved " + prev_name_ + "_config.json profile"); } @@ -717,18 +1361,8 @@ void MockControllerDeviceDriver::PollHotkeysThread() { } sleep.save = cfg.sleep_count_max; } - // Ctrl+F9 Save HMD Position & Yaw - if (isCtrlDown() && isDown(VK_F9) && sleep.hmd == 0) { - JsonManager().SaveHmdOffsets(cfg); - BeepSuccess(); - setOverlay("Saved HMD Offsets"); - sleep.hmd = cfg.sleep_count_max; - } - else if (sleep.hmd > 0) { - --sleep.hmd; - } // Ctrl+F10 Reload settings from Game Profile or (+Shift) Default Profile - else if (isCtrlDown() && isDown(VK_F10) && sleep.save == 0) { + if (isCtrlDown() && isDown(VK_F10) && sleep.save == 0) { std::string path = ""; if (isDown(VK_SHIFT)) { path = DEF_CFG; @@ -741,6 +1375,12 @@ void MockControllerDeviceDriver::PollHotkeysThread() { if (JsonManager().LoadProfileFromJson(path, cfg)) { stereo_display_component_->LoadSettings(cfg); SetAsync(cfg.async_enable); + auto_focus_.store(cfg.auto_focus); + hide_cursor_.store(cfg.hide_cursor); + lock_cursor_.store(cfg.lock_cursor); + stereo_cursor_.store(cfg.stereo_cursor); + cursor_depth_.store(cfg.cursor_depth); + cursor_size_.store(cfg.cursor_size); LOG() << "Loaded " << path.c_str() << " profile"; BeepSuccess(); setOverlay("Loaded " + path + " profile"); @@ -754,9 +1394,28 @@ void MockControllerDeviceDriver::PollHotkeysThread() { else if (sleep.save > 0) { --sleep.save; } + // Ctrl+F11 Toggle Auto-Depth + if (isCtrlDown() && isDown(VK_F11) && sleep.auto_depth == 0) { + const bool now = !stereo_display_component_->IsAutoDepthEnabled(); + stereo_display_component_->SetAutoDepthEnabled(now); + setOverlay(now ? "Auto-Depth: ON" + : "Auto-Depth: OFF (manual restored)"); + sleep.auto_depth = cfg.sleep_count_max; + } + else if (sleep.auto_depth > 0) { + --sleep.auto_depth; + } } - // Ctrl+F8 Toggle Always On Top - if (isCtrlDown() && isDown(VK_F8) && sleep.top == 0) { + // Ctrl+F8 (keyboard) or Start+DPad-Up (gamepad) Toggle Always On Top. + // Always polled (like the menu chord) so a controller-only Steam Deck + // user can raise a lowered/buried overlay back on top without a + // keyboard — the direct counterpart to Start+DPad-Down opening the menu. + DWORD top_pad = 0; + const DWORD top_chord_mask = XINPUT_GAMEPAD_START | XINPUT_GAMEPAD_DPAD_UP; + const bool top_pad_chord = + GetXInputButtonState(top_pad) && + ((top_pad & top_chord_mask) == top_chord_mask); + if (((isCtrlDown() && isDown(VK_F8)) || top_pad_chord) && sleep.top == 0) { is_on_top_ = !is_on_top_; man_on_top_ = is_on_top_.load(); sleep.top = cfg.sleep_count_max; @@ -764,149 +1423,31 @@ void MockControllerDeviceDriver::PollHotkeysThread() { else if (sleep.top > 0) { --sleep.top; } - // Ctrl+F12 Take Screenshot + // Ctrl+F12 Take Screenshot — drains on the next composited frame + // inside Dx11Renderer::WaitAndDrawPending. if (isCtrlDown() && isDown(VK_F12) && sleep.shot == 0) { - take_screenshot_ = true; + if (renderer_) { + std::string name = !app_name_.empty() ? app_name_ + : !prev_name_.empty() ? prev_name_ + : std::string("vrto3d"); + renderer_->RequestScreenshot(name); + } sleep.shot = cfg.sleep_count_max; } else if (sleep.shot > 0) { --sleep.shot; } - // Ctrl+- Decrease Sensitivity / Shift+Ctrl+- Decrease Filter Deadzone - if (isCtrlDown() && isDown(VK_OEM_MINUS)) { - const bool shift = isDown(VK_SHIFT); - if (cfg.use_track_filter) { - if (shift) { - stereo_display_component_->AdjustTrackFilterRotationDeadzone(-0.001f); - cfg = stereo_display_component_->GetConfig(); - setOverlay(fmt("Track Rot DZ: ", cfg.trk_flt_rot_dz, 3)); - } - else { - stereo_display_component_->AdjustTrackFilterRotation(-0.01f); - cfg = stereo_display_component_->GetConfig(); - setOverlay(fmt("Track Rot Sens: ", cfg.trk_flt_rot_sens, 2)); - } - } - else { - stereo_display_component_->AdjustSensitivity(-0.01f); - cfg = stereo_display_component_->GetConfig(); - setOverlay(fmt("Ctrl Sensitivity: ", cfg.ctrl_sensitivity, 2)); - } - } - // Ctrl++ Increase Sensitivity / Shift+Ctrl++ Increase Filter Deadzone - else if (isCtrlDown() && isDown(VK_OEM_PLUS)) { - const bool shift = isDown(VK_SHIFT); - if (cfg.use_track_filter) { - if (shift) { - stereo_display_component_->AdjustTrackFilterRotationDeadzone(0.001f); - cfg = stereo_display_component_->GetConfig(); - setOverlay(fmt("Track Rot DZ: ", cfg.trk_flt_rot_dz, 3)); - } - else { - stereo_display_component_->AdjustTrackFilterRotation(0.01f); - cfg = stereo_display_component_->GetConfig(); - setOverlay(fmt("Track Rot Sens: ", cfg.trk_flt_rot_sens, 2)); - } - } - else { - stereo_display_component_->AdjustSensitivity(0.01f); - cfg = stereo_display_component_->GetConfig(); - setOverlay(fmt("Ctrl Sensitivity: ", cfg.ctrl_sensitivity, 2)); - } - } - // Ctrl+[ Decrease Pitch Radius / Shift+Ctrl+[ Decrease Filter Position Deadzone - if (isCtrlDown() && isDown(VK_OEM_4)) { - const bool shift = isDown(VK_SHIFT); - if (cfg.use_track_filter) { - if (shift) { - stereo_display_component_->AdjustTrackFilterTranslationDeadzone(-0.001f); - cfg = stereo_display_component_->GetConfig(); - setOverlay(fmt("Track Pos DZ: ", cfg.trk_flt_pos_dz, 3)); - } - else { - stereo_display_component_->AdjustTrackFilterTranslation(-0.01f); - cfg = stereo_display_component_->GetConfig(); - setOverlay(fmt("Track Pos Sens: ", cfg.trk_flt_pos_sens, 2)); - } - } - else { - stereo_display_component_->AdjustRadius(-0.01f); - cfg = stereo_display_component_->GetConfig(); - setOverlay(fmt("Pitch Radius: ", cfg.pitch_radius, 2)); - } - } - // Ctrl+] Increase Pitch Radius / Shift+Ctrl+] Increase Filter Position Deadzone - else if (isCtrlDown() && isDown(VK_OEM_6)) { - const bool shift = isDown(VK_SHIFT); - if (cfg.use_track_filter) { - if (shift) { - stereo_display_component_->AdjustTrackFilterTranslationDeadzone(0.001f); - cfg = stereo_display_component_->GetConfig(); - setOverlay(fmt("Track Pos DZ: ", cfg.trk_flt_pos_dz, 3)); - } - else { - stereo_display_component_->AdjustTrackFilterTranslation(0.01f); - cfg = stereo_display_component_->GetConfig(); - setOverlay(fmt("Track Pos Sens: ", cfg.trk_flt_pos_sens, 2)); - } - } - else { - stereo_display_component_->AdjustRadius(0.01f); - cfg = stereo_display_component_->GetConfig(); - setOverlay(fmt("Pitch Radius: ", cfg.pitch_radius, 2)); - } - } - - // Ctrl+; Decrease Filter Zoom Smoothing / Shift+Ctrl+; Decrease Filter Max Zoom - if (isCtrlDown() && isDown(VK_OEM_1)) { - const bool shift = isDown(VK_SHIFT); - if (cfg.use_track_filter) { - if (shift) { - stereo_display_component_->AdjustTrackFilterMaxZoom(-0.1f); - cfg = stereo_display_component_->GetConfig(); - setOverlay(fmt("Track Max Zoom: ", cfg.trk_flt_max_zoom, 2)); - } - else { - stereo_display_component_->AdjustTrackFilterZoomSmoothing(-0.05f); - cfg = stereo_display_component_->GetConfig(); - setOverlay(fmt("Track Zoom Smooth: ", cfg.trk_flt_zoom_smooth, 2)); - } - } - } - // Ctrl+' Increase Filter Zoom Smoothing / Shift+Ctrl+' Increase Filter Max Zoom - else if (isCtrlDown() && isDown(VK_OEM_7)) { - const bool shift = isDown(VK_SHIFT); - if (cfg.use_track_filter) { - if (shift) { - stereo_display_component_->AdjustTrackFilterMaxZoom(0.1f); - cfg = stereo_display_component_->GetConfig(); - setOverlay(fmt("Track Max Zoom: ", cfg.trk_flt_max_zoom, 2)); - } - else { - stereo_display_component_->AdjustTrackFilterZoomSmoothing(0.05f); - cfg = stereo_display_component_->GetConfig(); - setOverlay(fmt("Track Zoom Smooth: ", cfg.trk_flt_zoom_smooth, 2)); - } - } - } - - // Check User binds + // Check User binds (preset hotkeys configured by the user — load/store) auto hotkey_str = stereo_display_component_->CheckUserSettings(); - - // Check for Position Adjustment - auto pos_str = stereo_display_component_->CheckPositionInput(); - if (!hotkey_str.empty()) { setOverlay(hotkey_str); } - else if (!pos_str.empty()) { - setOverlay(pos_str); - } // Check for new profile load if (app_updated_) { setOverlay("Loaded " + app_name_ + "_config.json profile"); + if (auto* osd = getOsd()) osd->SetAppName(app_name_); app_updated_ = false; } // Check for no profile @@ -916,13 +1457,17 @@ void MockControllerDeviceDriver::PollHotkeysThread() { no_profile_ = false; } - // Draw Overlay if applicable - if (!overlay_hwnd) { - overlay_hwnd = FindWindow(NULL, L"Headset Window"); - } - else if (sleep.overlay > 0) { - DrawOverlayText(overlay_hwnd, overlay_msg, cfg.window_height); - --sleep.overlay; + // Flush a deferred depth/convergence projection re-sync (see the + // resync_pending comment above the loop for the batching rules). + if (resync_pending) { + const bool adjust_held = isCtrlDown() && + (isDown(VK_F3) || isDown(VK_F4) || isDown(VK_F5) || isDown(VK_F6)); + const auto now = std::chrono::steady_clock::now(); + if (!adjust_held || now - last_resync >= std::chrono::milliseconds(150)) { + stereo_display_component_->ResetProjection(); + resync_pending = false; + last_resync = now; + } } // Sleep for ~ 1 frame @@ -932,131 +1477,27 @@ void MockControllerDeviceDriver::PollHotkeysThread() { //----------------------------------------------------------------------------- -// Purpose: Keep Headset Window on top if set +// Purpose: Expose focus/top atomics to the virtual-display presenter. +// +// The presenter owns the 3D output window and runs its own focus thread +// that observes these flags. The hotkey handler + UE3D auto-focus path +// continue to toggle them here. //----------------------------------------------------------------------------- -void MockControllerDeviceDriver::FocusUpdateThread() +vrto3d::FocusContext MockControllerDeviceDriver::GetFocusContext() { - static int sleep_time = 1000; - static HWND vr_window = NULL; - static HWND top_window = NULL; - static HWND game_window = NULL; - static LONG ex_style = 0; - static DWORD vr_pid = GetCurrentThreadId(); - static DWORD last_pid = 0; - static bool was_on_top = false; - static bool was_focused = false; - static bool window_bounds_applied = false; - - while (is_active_) - { - // Get handle for the VR window - if (vr_window == NULL) { - vr_window = FindWindow(NULL, L"Headset Window"); - if (vr_window != NULL) { - ex_style = GetWindowLong(vr_window, GWL_EXSTYLE); - } - } - - // Place the Headset Window - Has to be done twice to fix render issues - if (vr_window != NULL && !window_bounds_applied) { - // Use per-monitor DPI awareness for accurate cross-monitor placement - auto prev_dpi_ctx = SetThreadDpiAwarenessContext( - DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); - - auto cfg = stereo_display_component_->GetConfig(); - ApplyDisplaySelectionToWindowConfig(cfg); - - LOG() << "Applying Headset Window placement to (" - << cfg.window_x << "," << cfg.window_y << " " - << cfg.window_width << "x" << cfg.window_height << ")"; - - // Half-width first pass nudges DWM to commit to the target monitor - // but creates an intermediate swapchain size that ReShade latches - // onto. Only run it when the target isn't the primary display at - // (0,0) — that's when the nudge is actually needed. - const bool needs_nudge = cfg.multi_display - || cfg.window_x != 0 || cfg.window_y != 0; - if (needs_nudge) { - SetWindowPos(vr_window, HWND_TOP, - cfg.window_x, cfg.window_y, - cfg.window_width / 2, cfg.window_height, - SWP_NOACTIVATE); - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - } - SetWindowPos(vr_window, HWND_TOP, - cfg.window_x, cfg.window_y, - cfg.window_width, cfg.window_height, - SWP_NOACTIVATE); - - // Restore thread DPI context - SetThreadDpiAwarenessContext(prev_dpi_ctx); - - window_bounds_applied = true; - } - - // Keep VR display always on top for 3D rendering - if (is_on_top_ && IsProcessRunning(app_pid_)) { - top_window = GetTopWindow(GetDesktopWindow()); - if (vr_window != NULL && vr_window != top_window) { - SetWindowPos(vr_window, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); - SetWindowLong(vr_window, GWL_EXSTYLE, ex_style | (WS_EX_LAYERED | WS_EX_TRANSPARENT)); - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - - // Recenter VR - if (last_pid != app_pid_) { - PostMessage(vr_window, WM_KEYDOWN, 'Z', 0); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - PostMessage(vr_window, WM_KEYUP, 'Z', 0); - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - last_pid = app_pid_; - } - } - was_on_top = true; - } - // Unfocus and check to see if the game is still running to re-enable focus - else if (vr_window != NULL && was_on_top) { - SetWindowPos(vr_window, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); - SetWindowLong(vr_window, GWL_EXSTYLE, (ex_style | WS_EX_LAYERED) & ~WS_EX_TRANSPARENT); - if (man_on_top_) - { - std::this_thread::sleep_for(std::chrono::seconds(15)); - is_on_top_ = IsProcessRunning(app_pid_); - } - was_on_top = is_on_top_; - was_focused = false; - } - - // Focus the Game Window - if (is_on_top_ && !was_focused) - { - game_window = GetHWNDFromPID(app_pid_); - ForceFocus(game_window, vr_pid, app_pid_); - was_focused = true; - } - - // Take Screenshot - if (vr_window != NULL && take_screenshot_) { - ForceFocus(vr_window, vr_pid, vr_pid); - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - PostMessage(vr_window, WM_KEYDOWN, 'S', 0); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - PostMessage(vr_window, WM_KEYUP, 'S', 0); - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - ForceFocus(game_window, vr_pid, app_pid_); - take_screenshot_ = false; - BeepSuccess(); - } - - // Sleep for 1s - std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time)); - } + vrto3d::FocusContext fc; + fc.is_on_top = &is_on_top_; + fc.man_on_top = &man_on_top_; + fc.app_pid = &app_pid_; + fc.auto_focus = &auto_focus_; + return fc; } //----------------------------------------------------------------------------- // Purpose: Process UE3D/UEVR shared-memory monitor/depth requests //----------------------------------------------------------------------------- -void MockControllerDeviceDriver::AutoDepthThread() { +void MockControllerDeviceDriver::MonitorModeThread() { auto& rx = uevr::receiver(); static float last_hint_ipd = -1.0f; @@ -1070,7 +1511,7 @@ void MockControllerDeviceDriver::AutoDepthThread() { stereo_display_component_->GetConvergence(), stereo_display_component_->GetFoV(), 0.0f, // fov_adj (unused in monitor mode) - config.tab_enable ? 0 : 1, + (config.output_mode == OutputMode::TaB) ? 0 : 1, !no_profile_.load() ); @@ -1079,14 +1520,6 @@ void MockControllerDeviceDriver::AutoDepthThread() { if (mon) { - if (config.auto_focus && !is_on_top_ && !ue3d_on_top_) { - BeepSuccess(); - std::this_thread::sleep_for(std::chrono::seconds(2)); - is_on_top_ = true; - man_on_top_ = true; - ue3d_on_top_ = true; - } - // Depth commands from UEVR (Calibrate, VRto3D++/+/-/--) uint8_t depth_cmd = rx.get_depth_request(); if (depth_cmd >= 2 && depth_cmd <= 6) { @@ -1134,11 +1567,308 @@ void MockControllerDeviceDriver::AutoDepthThread() { } +#ifdef _WIN32 +namespace { + +// System cursor ids (OCR_*) — numeric so we don't need OEMRESOURCE defined +// before windows.h. +constexpr DWORD kSystemCursorIds[] = { + 32512, // OCR_NORMAL + 32513, // OCR_IBEAM + 32514, // OCR_WAIT + 32515, // OCR_CROSS + 32516, // OCR_UP + 32642, // OCR_SIZENWSE + 32643, // OCR_SIZENESW + 32644, // OCR_SIZEWE + 32645, // OCR_SIZENS + 32646, // OCR_SIZEALL + 32648, // OCR_NO + 32649, // OCR_HAND + 32650, // OCR_APPSTARTING + 32651, // OCR_HELP +}; + +// Replace the entire system cursor set with fully transparent cursors. +// This is the strongest cursor hide available to an external process: it +// survives the game's WM_SETCURSOR re-assertions because the cursor the +// game sets IS blank. Undone by RestoreSystemCursors. +bool BlankSystemCursors() { + const int w = GetSystemMetrics(SM_CXCURSOR); + const int h = GetSystemMetrics(SM_CYCURSOR); + if (w <= 0 || h <= 0) return false; + // 1bpp masks, scanlines word-aligned. AND=1 / XOR=0 -> transparent. + const size_t mask_bytes = static_cast(((w + 15) / 16) * 2) * h; + std::vector and_mask(mask_bytes, 0xFF); + std::vector xor_mask(mask_bytes, 0x00); + bool any = false; + for (DWORD id : kSystemCursorIds) { + HCURSOR blank = CreateCursor(nullptr, 0, 0, w, h, + and_mask.data(), xor_mask.data()); + if (!blank) continue; + // SetSystemCursor takes ownership of (and eventually destroys) the + // handle on success; on failure it's still ours to free. + if (SetSystemCursor(blank, id)) any = true; + else DestroyCursor(blank); + } + return any; +} + +// Reload the user's cursor scheme from the registry, undoing +// BlankSystemCursors. +void RestoreSystemCursors() { + SystemParametersInfoW(SPI_SETCURSORS, 0, nullptr, 0); +} + +} // namespace +#endif + +//----------------------------------------------------------------------------- +// Purpose: Apply hide_cursor / lock_cursor / stereo_cursor to the connected +// game from out-of-process (we live in vrserver.exe, the game is its own +// process). +// +// Hide (hide_cursor, or stereo_cursor which replaces the pointer): the +// strongest cross-process hide Windows allows — blank the entire system +// cursor set via SetSystemCursor while the game holds the foreground, so +// the cursor stays invisible no matter how often the game's WndProc +// re-asserts it. Restored via SPI_SETCURSORS the moment focus leaves the +// game (or the toggles turn off / the driver exits). A per-tick +// AttachThreadInput + SetCursor(NULL) supplements this for games that set +// a custom, non-system class cursor, which SetSystemCursor can't blank. +// +// Lock (lock_cursor): ClipCursor to the focused game window's CLIENT rect +// (borders/title bar excluded), re-asserted every ~10ms because Windows +// silently drops the clip on every foreground transition. AttachThreadInput +// to the game's input queue so the clip isn't rejected as a cross-process +// trap. +// +// Stereo cursor: pushes draw state (active/depth/size) into the OSD +// renderer, which draws a per-eye arrow at the OS cursor position in place +// of the blanked hardware cursor (or flips on ImGui's software cursor +// while the OSD menu is open). +// +// Focus is judged by PID, not a single cached HWND — any foreground window +// belonging to the game's process counts, and the clip follows the actual +// foreground window. +//----------------------------------------------------------------------------- +void MockControllerDeviceDriver::CursorControlThread() +{ +#ifndef _WIN32 + // Cursor clipping/hiding is not implementable from an external process on + // Wayland; X11 support could ride on XGrabPointer later. Idle politely. + while (is_active_.load(std::memory_order_relaxed)) + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + return; +#else + bool clip_active = false; + bool cursors_blank = false; + // Net ShowCursor(FALSE) calls we pushed onto the game's input queue to + // hide a custom (non-system) class cursor, and the thread they went to. + int shows_forced = 0; + DWORD forced_tid = 0; + + const auto release_clip = [&] { + if (clip_active) { + ClipCursor(nullptr); + clip_active = false; + } + }; + const auto restore_cursors = [&] { + if (cursors_blank) { + RestoreSystemCursors(); + cursors_blank = false; + LOG() << "CursorControl: system cursors restored"; + } + }; + // Undo the forced ShowCursor(FALSE) count on the game's queue: attach and + // raise the display count back to >= 0, never by more than we pushed. If + // the game thread is gone, there's no queue left to fix. + const auto restore_show_count = [&] { + if (shows_forced > 0) { + const DWORD my_tid = GetCurrentThreadId(); + if (forced_tid && forced_tid != my_tid + && AttachThreadInput(my_tid, forced_tid, TRUE)) { + int raised = 0; + while (shows_forced-- > 0) { + ++raised; + if (ShowCursor(TRUE) >= 0) break; + } + AttachThreadInput(my_tid, forced_tid, FALSE); + LOG() << "CursorControl: game cursor show count restored (+" << raised << ")"; + } + } + shows_forced = 0; + forced_tid = 0; + }; + + while (is_active_.load(std::memory_order_relaxed)) { + const bool want_hide = hide_cursor_.load(std::memory_order_relaxed); + const bool want_lock = lock_cursor_.load(std::memory_order_relaxed); + const bool want_stereo = stereo_cursor_.load(std::memory_order_relaxed); + // Only act while our VR overlay is actually on top of the game — when + // the user toggles topmost off (Ctrl+F8 / OSD) they're back at the + // desktop and expect their real cursor / clip back, regardless of + // these toggles. + const bool on_top = is_on_top_.load(std::memory_order_relaxed); + const bool menu_open = renderer_ && renderer_->Osd() + && renderer_->Osd()->MenuVisible(); + + if (!on_top || (!want_hide && !want_lock && !want_stereo)) { + release_clip(); + restore_cursors(); + restore_show_count(); + if (renderer_ && renderer_->Osd()) { + renderer_->Osd()->SetStereoCursor(false, 0.0f, 32.0f, nullptr); + } + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + continue; + } + + const uint32_t pid = app_pid_.load(); + HWND fg = GetForegroundWindow(); + DWORD fg_pid = 0; + const DWORD fg_tid = fg ? GetWindowThreadProcessId(fg, &fg_pid) : 0; + const bool game_is_fg = pid != 0 && fg && fg_pid == pid; + const bool self_is_fg = fg && fg_pid == GetCurrentProcessId(); + + // Stereo cursor draws over the focused game, or over our own OSD + // menu (where the renderer switches to ImGui's software cursor). + // The game window handle lets the renderer normalize the arrow + // against the GAME's client rect so it tracks the in-game cursor 1:1. + const bool stereo_active = want_stereo + && (game_is_fg || (menu_open && self_is_fg)); + if (renderer_ && renderer_->Osd()) { + renderer_->Osd()->SetStereoCursor( + stereo_active, + cursor_depth_.load(std::memory_order_relaxed), + static_cast(cursor_size_.load(std::memory_order_relaxed)), + game_is_fg ? static_cast(fg) : nullptr); + } + + // Hardware-cursor hide. The stereo cursor implies a hide (it IS the + // pointer); plain hide_cursor keeps the old suppress-while-menu-open + // behavior, since without the stereo cursor there'd be no visible + // pointer left to click ImGui widgets with. + const bool hw_hide = (game_is_fg && !menu_open && (want_hide || want_stereo)) + || (menu_open && self_is_fg && want_stereo); + if (hw_hide && !cursors_blank) { + cursors_blank = BlankSystemCursors(); + if (cursors_blank) { + LOG() << "CursorControl: system cursors blanked (hide engaged, fg_pid=" << fg_pid << ")"; + } + } else if (!hw_hide) { + restore_cursors(); + } + + if (!game_is_fg || menu_open) { + release_clip(); + restore_show_count(); + std::this_thread::sleep_for(std::chrono::milliseconds(menu_open ? 33 : 100)); + continue; + } + + // Game has the foreground: attach to its input queue so ClipCursor / + // SetCursor share state with the game's thread (Windows otherwise + // blocks cursor clipping from non-foreground processes). + const DWORD my_tid = GetCurrentThreadId(); + const BOOL attached = (fg_tid && fg_tid != my_tid) + ? AttachThreadInput(my_tid, fg_tid, TRUE) + : FALSE; + + if (want_lock) { + RECT rc{}; + POINT tl{}, br{}; + if (GetClientRect(fg, &rc)) { + tl = { rc.left, rc.top }; + br = { rc.right, rc.bottom }; + if (ClientToScreen(fg, &tl) && ClientToScreen(fg, &br) + && br.x > tl.x && br.y > tl.y) { + RECT clip{ tl.x, tl.y, br.x, br.y }; + ClipCursor(&clip); + if (!clip_active) { + LOG() << "CursorControl: cursor clipped to game client rect (" + << tl.x << "," << tl.y << ")-(" << br.x << "," << br.y << ")"; + } + clip_active = true; + } + } + } else { + release_clip(); + } + + if (want_hide || want_stereo) { + // Belt-and-braces for custom class cursors (see header comment). + SetCursor(nullptr); + + // Games that draw a custom cursor re-assert it faster than the + // SetCursor(NULL) tick and aren't touched by the blanked system + // set. Strongest remaining lever: while attached to the game's + // input queue, drive its ShowCursor display count negative — + // that state lives on the game's queue and persists after we + // detach, hiding ANY cursor it sets. Undone (bounded) by + // restore_show_count on focus loss / toggle off / exit. + if (attached) { + CURSORINFO ci{}; + ci.cbSize = sizeof(ci); + if (GetCursorInfo(&ci) && (ci.flags & CURSOR_SHOWING)) { + const int prev_forced = shows_forced; + forced_tid = fg_tid; + // Tracked count is clamped — restore only ever raises + // until the count is back >= 0, so excess tracking is + // pointless; re-forcing itself stays unbounded. + for (int i = 0; i < 8; ++i) { + if (shows_forced < 1024) ++shows_forced; + if (ShowCursor(FALSE) < 0) break; + } + if (prev_forced == 0 && shows_forced > 0) { + LOG() << "CursorControl: forced game cursor display count negative (net " + << shows_forced << ")"; + } + } + } + } + + if (attached) { + AttachThreadInput(my_tid, fg_tid, FALSE); + } + + // Fast re-assert: Windows drops the clip on every foreground + // transition and games re-show their cursor at will. + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + release_clip(); + restore_cursors(); + restore_show_count(); +#endif +} + + //----------------------------------------------------------------------------- // Purpose: Load Game Specific Settings from Steam\config\vrto3d\app_name_config.json //----------------------------------------------------------------------------- void MockControllerDeviceDriver::LoadSettings(const std::string& app_name, uint32_t app_pid, vr::EVREventType status) { + // Quick same-pid reconnect (e.g. RealVR re-inits VR after FoV/res + // change): the disconnect branch dropped focus and armed a 15s grace + // timer. Refocus immediately so the user isn't stuck without input + // passthrough — but only if focus was actually on before the + // disconnect (we don't want to override a user "always-on-top off" + // choice). The pending 15s thread will wake later and re-assert, + // which is a no-op since flags are already set. + if (status == vr::VREvent_ProcessConnected + && app_name == app_name_ + && app_pid == app_pid_ + && focus_pre_disconnect_.load() + && !man_on_top_.load()) + { + is_on_top_ = true; + man_on_top_ = true; + focus_pre_disconnect_.store(false); + return; + } + if ((app_name != app_name_ || app_pid != app_pid_) && status == vr::VREvent_ProcessConnected) { app_name_ = app_name; @@ -1153,11 +1883,6 @@ void MockControllerDeviceDriver::LoadSettings(const std::string& app_name, uint3 LOG() << "Loaded " << app_name.c_str() << " profile"; BeepSuccess(); app_updated_ = true; - if (config.auto_focus) { - std::this_thread::sleep_for(std::chrono::seconds(8)); - is_on_top_ = true; - man_on_top_ = true; - } } else { BeepFailure(); @@ -1165,12 +1890,102 @@ void MockControllerDeviceDriver::LoadSettings(const std::string& app_name, uint3 } SetAsync(config.async_enable); + // Mirror the freshly-loaded profile's auto_focus to the live atomic + // observed by presenter focus loops. + auto_focus_.store(config.auto_focus); + hide_cursor_.store(config.hide_cursor); + lock_cursor_.store(config.lock_cursor); + stereo_cursor_.store(config.stereo_cursor); + cursor_depth_.store(config.cursor_depth); + cursor_size_.store(config.cursor_size); + + if (config.auto_focus) { + // Run off-thread: this is called from RunFrame, and a 10s sleep + // here freezes the driver host's event loop. Snapshot the pid so + // a fast disconnect/reconnect doesn't make us focus/recenter for + // a different app. Retry the recenter a few times because games + // commonly call ResetSeatedZeroPose during their own VR init and + // will clobber a single well-timed shot. + const uint32_t pid = app_pid_.load(); + std::thread([this, pid]() { + std::this_thread::sleep_for(std::chrono::seconds(8)); + if (!is_active_) return; + if (app_pid_.load() != pid) return; + is_on_top_ = true; + man_on_top_ = true; + // Give the game a moment after focus before kicking the + // recenter, so the first attempt lands after the game has + // settled into its initial pose rather than mid-init. + std::this_thread::sleep_for(std::chrono::seconds(4)); + if (!is_active_) return; + if (app_pid_.load() != pid) return; + if (!man_on_top_.load()) return; + for (int i = 0; i < 3; ++i) { + const std::string tag = "auto_focus#" + std::to_string(i + 1); + if (vrto3d::TriggerOpenVRRecenter(tag.c_str())) return; + if (i < 2) std::this_thread::sleep_for(std::chrono::seconds(2)); + if (!is_active_) return; + if (app_pid_.load() != pid) return; + if (!man_on_top_.load()) return; + } + }).detach(); + } } else if (status == vr::VREvent_ProcessDisconnected) { + // Capture the user's pre-disconnect focus preference so both the + // 15s grace thread and the quick-reconnect path can honor a user + // "always-on-top off" choice instead of forcing focus back on. + const bool was_focused = man_on_top_.load(); + focus_pre_disconnect_.store(was_focused); + is_on_top_ = false; - ue3d_on_top_ = false; + man_on_top_ = false; + + if (!was_focused) return; + + // Some games briefly disconnect from SteamVR (compositor blip, + // scene-app handoff) and immediately reconnect with the same exe + // still alive. Wait 15s, then re-engage focus if the original + // process is still running and no new app has connected since. + uint32_t pid = app_pid_.load(); + std::thread([this, pid]() { + std::this_thread::sleep_for(std::chrono::seconds(15)); + if (!is_active_) return; + if (app_pid_.load() == pid && IsProcessRunning(pid)) { + is_on_top_ = true; + man_on_top_ = true; + focus_pre_disconnect_.store(false); + } + }).detach(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Consume the pose-reset signal. Zeros the cached OpenTrack +// attitude/position so stale UDP-derived bias can't bleed into the pose the +// next time it's consumed (e.g. when use_open_track is toggled back on, or +// when the user pressed the reset hotkey while OpenTrack was active), then +// clears the flag and asks SteamVR to take the cleaned HMD pose as the new +// seated/standing zero. The TriggerOpenVRRecenter dispatch is deferred to a +// detached thread so PoseUpdateThread has a tick to publish the cleaned pose +// — otherwise SteamVR's snapshot would bake in the stale bias we just +// cleared. Single consumption point for both the pose-reset hotkey and the +// OSD recenter-on-disable toggle paths. +//----------------------------------------------------------------------------- +void MockControllerDeviceDriver::ConsumePoseReset() +{ + { + std::lock_guard lock(trk_mutex_); + open_track_att_ = HmdQuaternion_Identity; + open_track_pos_ = { 0.0, 0.0, 0.0 }; } + stereo_display_component_->SetReset(); + std::thread([]() { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + vrto3d::TriggerOpenVRRecenter("pose_reset"); + }).detach(); } @@ -1200,6 +2015,7 @@ void MockControllerDeviceDriver::EnterStandby() //----------------------------------------------------------------------------- void MockControllerDeviceDriver::Deactivate() { + LOG() << "MockControllerDeviceDriver::Deactivate"; if ( is_active_.exchange( false ) ) { if (xinput_thread_.joinable()) { @@ -1211,21 +2027,31 @@ void MockControllerDeviceDriver::Deactivate() if (hotkey_thread_.joinable()) { hotkey_thread_.join(); } - if (focus_thread_.joinable()) { - focus_thread_.join(); - } - if (depth_thread_.joinable()) { - depth_thread_.join(); + if (monitor_thread_.joinable()) { + monitor_thread_.join(); } if (track_thread_.joinable()) { track_thread_.join(); } + if (cursor_thread_.joinable()) { + cursor_thread_.join(); + } + // Direct-mode component holds a raw Dx11Renderer*; destroy it first + // so any in-flight SubmitLayer/Present can no longer reach the + // renderer before the renderer is torn down. + direct_mode_component_.reset(); + if (renderer_) { + renderer_->Shutdown(); + renderer_.reset(); + } } // unassign our controller index (we don't want to be calling vrserver anymore after Deactivate() has been called device_index_ = vr::k_unTrackedDeviceIndexInvalid; } +MockControllerDeviceDriver::~MockControllerDeviceDriver() = default; + //----------------------------------------------------------------------------- // DISPLAY DRIVER METHOD DEFINITIONS @@ -1233,7 +2059,12 @@ void MockControllerDeviceDriver::Deactivate() StereoDisplayComponent::StereoDisplayComponent( const StereoDisplayDriverConfiguration &config ) : config_( config ), depth_(config.depth), convergence_(config.convergence), fov_(config.fov) -{} +{ + manual_depth_.store(config.depth); + auto_depth_enabled_.store(config.auto_depth_enabled); + auto_depth_target_disparity_.store(config.auto_depth_target_disparity); + auto_depth_smoothing_.store(config.auto_depth_smoothing); +} //----------------------------------------------------------------------------- @@ -1261,68 +2092,23 @@ bool StereoDisplayComponent::IsDisplayRealDisplay() { return false; } void StereoDisplayComponent::GetRecommendedRenderTargetSize( uint32_t *pnWidth, uint32_t *pnHeight ) { std::shared_lock lock(cfg_mutex_); - *pnWidth = config_.render_width; - *pnHeight = config_.render_height; + *pnWidth = static_cast(config_.render_width); + *pnHeight = static_cast(config_.render_height); } //----------------------------------------------------------------------------- -// Purpose: Render in SbS or TaB Stereo3D +// Canonical SbS eye viewport. Never called by the compositor in direct mode +// but kept functional to satisfy the IVRDisplayComponent pure-virtual contract. //----------------------------------------------------------------------------- void StereoDisplayComponent::GetEyeOutputViewport( vr::EVREye eEye, uint32_t *pnX, uint32_t *pnY, uint32_t *pnWidth, uint32_t *pnHeight ) { std::shared_lock lock(cfg_mutex_); - if (config_.reverse_enable) - { - eEye = static_cast(!static_cast (eEye)); - } - // Use Top and Bottom Rendering - if (config_.tab_enable) - { - *pnX = 0; - // Each eye will have full width - *pnWidth = config_.window_width; - // Each eye will have half height - *pnHeight = (config_.window_height - config_.framepack_offset) / 2; - if (eEye == vr::Eye_Left) - { - // Left eye viewport on the top half of the window - *pnY = 0; - } - else - { - // Right eye viewport on the bottom half of the window - *pnY = *pnHeight + config_.framepack_offset; - } - } - - // Use Side by Side Rendering - else - { - // Each eye will have half height for virtual desktop - if (config_.vd_fsbs_hack) - { - *pnY = config_.window_height / 4; - *pnHeight = config_.window_height / 2; - } - // Each eye will have full height - else - { - *pnY = 0; - *pnHeight = config_.window_height; - } - // Each eye will have half width - *pnWidth = config_.window_width / 2; - if (eEye == vr::Eye_Left) - { - // Left eye viewport on the left half of the window - *pnX = 0; - } - else - { - // Right eye viewport on the right half of the window - *pnX = config_.window_width / 2; - } - } + const uint32_t eye_w = static_cast(config_.render_width); + const uint32_t eye_h = static_cast(config_.render_height); + *pnWidth = eye_w; + *pnHeight = eye_h; + *pnY = 0; + *pnX = (eEye == vr::Eye_Left) ? 0u : eye_w; } //----------------------------------------------------------------------------- @@ -1361,7 +2147,9 @@ void StereoDisplayComponent::GetProjectionRaw( vr::EVREye eEye, float *pfLeft, f } //----------------------------------------------------------------------------- -// Purpose: Don't distort any coordinates for Stereo3D +// IVRDisplayComponent distortion / window-bounds members. Never called by the +// compositor in direct mode but kept functional to satisfy the interface's +// pure-virtual contract. //----------------------------------------------------------------------------- vr::DistortionCoordinates_t StereoDisplayComponent::ComputeDistortion( vr::EVREye eEye, float fU, float fV ) { @@ -1377,16 +2165,13 @@ vr::DistortionCoordinates_t StereoDisplayComponent::ComputeDistortion( vr::EVREy bool StereoDisplayComponent::ComputeInverseDistortion(vr::HmdVector2_t* pResult, vr::EVREye eEye, uint32_t unChannel, float fU, float fV) { return false; } -//----------------------------------------------------------------------------- -// Purpose: To inform vrcompositor what the window bounds for this virtual HMD are. -//----------------------------------------------------------------------------- void StereoDisplayComponent::GetWindowBounds( int32_t *pnX, int32_t *pnY, uint32_t *pnWidth, uint32_t *pnHeight ) { std::shared_lock lock(cfg_mutex_); *pnX = 0; *pnY = 0; - *pnWidth = config_.window_width; - *pnHeight = config_.window_height; + *pnWidth = static_cast(config_.render_width) * 2u; + *pnHeight = static_cast(config_.render_height); } //----------------------------------------------------------------------------- @@ -1395,30 +2180,207 @@ void StereoDisplayComponent::GetWindowBounds( int32_t *pnX, int32_t *pnY, uint32 StereoDisplayDriverConfiguration StereoDisplayComponent::GetConfig() { std::shared_lock lock(cfg_mutex_); - return config_; + StereoDisplayDriverConfiguration cfg = config_; + // depth/convergence/fov live in atomics during runtime — config_ only + // holds the last JSON-loaded values. Sync them so callers that round-trip + // a snapshot through LoadSettings don't clobber live hotkey adjustments. + // For depth specifically, report manual_depth_ (the user's intended + // ceiling) rather than the live, possibly auto-modulated depth_ — otherwise + // any OSD control that round-trips cfg through LoadSettings while + // auto-depth is active would bake the auto-attenuated value into the + // ceiling and defeat the snap-back on disable. + cfg.depth = manual_depth_.load(std::memory_order_relaxed); + cfg.convergence = convergence_.load(std::memory_order_relaxed); + cfg.fov = fov_.load(std::memory_order_relaxed); + return cfg; } //----------------------------------------------------------------------------- // Purpose: To update the Depth value //----------------------------------------------------------------------------- -void StereoDisplayComponent::AdjustDepth(float new_depth, bool is_delta) +void StereoDisplayComponent::ApplyDepth(float new_depth) { + if (new_depth < 0.0f) new_depth = 0.0f; float cur_depth = GetDepth(); - if (is_delta) { - new_depth += cur_depth; - new_depth = (new_depth < 0) ? 0 : new_depth; - } while (!depth_.compare_exchange_weak(cur_depth, new_depth, std::memory_order_relaxed)); vr::PropertyContainerHandle_t container = vr::VRProperties()->TrackedDeviceToPropertyContainer(device_index_); vr::VRProperties()->SetFloatProperty(container, vr::Prop_UserIpdMeters_Float, new_depth); } +void StereoDisplayComponent::AdjustDepth(float new_depth, bool is_delta) +{ + if (is_delta) { + new_depth += manual_depth_.load(std::memory_order_relaxed); + if (new_depth < 0.0f) new_depth = 0.0f; + } + // Manual edits always update the user's intended ceiling. + manual_depth_.store(new_depth, std::memory_order_relaxed); + + if (auto_depth_enabled_.load(std::memory_order_relaxed)) { + // Auto on: the auto loop drives the live depth_ down toward the + // disparity target. Don't yank the live depth up past the new + // ceiling — clamp it from above instead. + ApplyDepth((std::min)(GetDepth(), new_depth)); + } else { + ApplyDepth(new_depth); + } +} + + +bool StereoDisplayComponent::IsAutoDepthEnabled() const +{ + return auto_depth_enabled_.load(std::memory_order_relaxed); +} + + +void StereoDisplayComponent::SetAutoDepthEnabled(bool enabled) +{ + auto_depth_enabled_.store(enabled, std::memory_order_relaxed); + // Reset the input-side disparity filters so the next enable starts fresh + // rather than carrying stale samples from a previous session. The median + // ring is renderer-thread-owned, so it's cleared there via the flag. + auto_depth_disp_ema_.store(-1.0f, std::memory_order_relaxed); + auto_depth_filter_reset_.store(true, std::memory_order_relaxed); + if (!enabled) { + // Snap the live depth back to the user's manual ceiling. + ApplyDepth(manual_depth_.load(std::memory_order_relaxed)); + } +} + + +float StereoDisplayComponent::GetManualDepth() const +{ + return manual_depth_.load(std::memory_order_relaxed); +} + + +float StereoDisplayComponent::GetAutoDepthTargetDisparity() const +{ + return auto_depth_target_disparity_.load(std::memory_order_relaxed); +} + + +void StereoDisplayComponent::SetAutoDepthTargetDisparity(float frac) +{ + if (frac < 0.001f) frac = 0.001f; + if (frac > 0.01f) frac = 0.01f; + auto_depth_target_disparity_.store(frac, std::memory_order_relaxed); +} + + +float StereoDisplayComponent::GetAutoDepthSmoothing() const +{ + return auto_depth_smoothing_.load(std::memory_order_relaxed); +} + + +void StereoDisplayComponent::SetAutoDepthSmoothing(float v) +{ + if (v < 0.005f) v = 0.005f; + if (v > 0.25f) v = 0.25f; + auto_depth_smoothing_.store(v, std::memory_order_relaxed); +} + + +bool StereoDisplayComponent::IsAutoDepthLoggingEnabled() const +{ + return auto_depth_log_enabled_.load(std::memory_order_relaxed); +} + + +void StereoDisplayComponent::SetAutoDepthLoggingEnabled(bool enabled) +{ + auto_depth_log_enabled_.store(enabled, std::memory_order_relaxed); +} + + +void StereoDisplayComponent::FeedAutoDepthSample(uint32_t max_disp_px, uint32_t eye_w_px, + uint32_t disp_step_px) +{ + if (!auto_depth_enabled_.load(std::memory_order_relaxed) || eye_w_px == 0) { + return; + } + if (auto_depth_filter_reset_.exchange(false, std::memory_order_relaxed)) { + auto_depth_hist_n_ = 0; + auto_depth_hist_i_ = 0; + } + const float ceiling = manual_depth_.load(std::memory_order_relaxed); + const float target_frac = auto_depth_target_disparity_.load(std::memory_order_relaxed); + const float smoothing = auto_depth_smoothing_.load(std::memory_order_relaxed); + const float raw_frac = static_cast(max_disp_px) / static_cast(eye_w_px); + + // Temporal median prefilter: a single-frame disparity spike (particle, + // muzzle flash, camera clip) must not reach the EMA at all — the + // asymmetric alpha reacts fast on the rising edge by design, so it + // can't defend against outliers on its own. + auto_depth_frac_hist_[auto_depth_hist_i_] = raw_frac; + auto_depth_hist_i_ = (auto_depth_hist_i_ + 1) % kAutoDepthHist; + if (auto_depth_hist_n_ < kAutoDepthHist) ++auto_depth_hist_n_; + float sorted[kAutoDepthHist]; + std::copy(auto_depth_frac_hist_, auto_depth_frac_hist_ + auto_depth_hist_n_, sorted); + std::nth_element(sorted, sorted + auto_depth_hist_n_ / 2, sorted + auto_depth_hist_n_); + const float med_frac = sorted[auto_depth_hist_n_ / 2]; + + // Input-side jitter filter: low-pass the disparity-fraction samples + // before they drive the depth target. Uses an asymmetric EMA — fast + // on rising disparity (close objects must clamp depth quickly for + // comfort) and slow on falling disparity (so a noisy frame that + // misses a close object doesn't briefly snap depth back up). + constexpr float kAlphaUp = 0.20f; // disparity rising -> moderate + constexpr float kAlphaDown = 0.05f; // disparity falling -> slow ease + // Deadband: sized to the analyzer's disparity quantization (~1.5 buckets + // covers frame-to-frame bucket jitter), capped at half the target so the + // band can never mask a real error — the old fixed 0.005 equaled the + // default target and swallowed multiples of it at low target settings. + const float step_frac = static_cast(disp_step_px) / static_cast(eye_w_px); + const float deadband = (std::min)(1.5f * step_frac, 0.5f * target_frac); + + float prev_ema = auto_depth_disp_ema_.load(std::memory_order_relaxed); + float new_ema; + if (prev_ema < 0.0f) { + new_ema = med_frac; // first sample primes the filter + } else { + const float diff = med_frac - prev_ema; + if (std::fabs(diff) < deadband) { + new_ema = prev_ema; // inside deadband — hold steady + } else { + const float alpha = (diff > 0.0f) ? kAlphaUp : kAlphaDown; + new_ema = prev_ema + alpha * diff; + } + } + auto_depth_disp_ema_.store(new_ema, std::memory_order_relaxed); + + // Auto pull-in floor: never attenuate below this fraction of the user's + // ceiling — a pathological frame (huge close object) should dim the + // stereo, not flatten it. Analog of UEVR-3D's min_convergence_m. + constexpr float kMinDepthFrac = 0.15f; + + const float disp_frac = new_ema; + float target_depth; + if (disp_frac <= target_frac || disp_frac <= 0.0f) { + target_depth = ceiling; + } else { + target_depth = ceiling * (target_frac / disp_frac); + if (target_depth > ceiling) target_depth = ceiling; + if (target_depth < kMinDepthFrac * ceiling) target_depth = kMinDepthFrac * ceiling; + } + const float cur = GetDepth(); + const float smoothed = cur + (target_depth - cur) * smoothing; + // The lerp never exactly converges, and every ApplyDepth pushes a + // SteamVR property write — skip sub-epsilon deltas so a settled loop + // stops churning vrserver. + if (std::fabs(smoothed - cur) > 1e-6f) { + ApplyDepth(smoothed); + } +} + + //----------------------------------------------------------------------------- // Purpose: To update the Convergence value //----------------------------------------------------------------------------- -void StereoDisplayComponent::AdjustConvergence(float new_conv, bool is_delta) +void StereoDisplayComponent::AdjustConvergence(float new_conv, bool is_delta, bool resync) { float cur_conv = GetConvergence(); if (is_delta) { @@ -1428,7 +2390,9 @@ void StereoDisplayComponent::AdjustConvergence(float new_conv, bool is_delta) if (NearlyEqual(cur_conv, new_conv)) return; while (!convergence_.compare_exchange_weak(cur_conv, new_conv, std::memory_order_relaxed)); - ResetProjection(); + // resync=false lets the hotkey poll loop batch the projection re-sync + // instead of firing it on every autorepeat tick. + if (resync) ResetProjection(); } @@ -1555,62 +2519,6 @@ std::string StereoDisplayComponent::CheckUserSettings() } -//----------------------------------------------------------------------------- -// Purpose: Move HMD origin position with keyboard input -//----------------------------------------------------------------------------- -std::string StereoDisplayComponent::CheckPositionInput() { - if (!isCtrlDown()) - return ""; - - const float step = 0.01f; - auto config = GetConfig(); - - if (isDown(VK_HOME)) { - config.hmd_y -= step; // Forward - } - else if (isDown(VK_END)) { - config.hmd_y += step; // Backward - } - else if (isDown(VK_DELETE)) { - config.hmd_x -= step; // Left - } - else if (isDown(VK_NEXT)) { - if (isDown(VK_SHIFT)) { - config.hmd_height -= step; // Down - } - else { - config.hmd_x += step; // Right - } - } - else if (isDown(VK_PRIOR)) { - if (isDown(VK_SHIFT)) { - config.hmd_height += step; // Up - } - else { - config.hmd_yaw -= step * 10; // Yaw CW - } - } - else if (isDown(VK_INSERT)) { - config.hmd_yaw += step * 10; // Yaw CCW - } - else { - return ""; - } - - std::ostringstream oss; - oss << std::fixed << std::setprecision(2); - oss << "Pos X:" << config.hmd_x - << " Y:" << config.hmd_y - << " Z:" << config.hmd_height - << " Yaw:" << config.hmd_yaw; - - std::unique_lock lock(cfg_mutex_); - config_ = config; - - return oss.str(); -} - - //----------------------------------------------------------------------------- // Purpose: Adjust XInput Right Stick sensitivity //----------------------------------------------------------------------------- @@ -1735,16 +2643,37 @@ void StereoDisplayComponent::SetReset() } +//----------------------------------------------------------------------------- +// Purpose: Toggle Reset on +//----------------------------------------------------------------------------- +void StereoDisplayComponent::RequestPoseReset() +{ + std::unique_lock lock(cfg_mutex_); + config_.pose_reset = true; +} + + //----------------------------------------------------------------------------- // Purpose: Load Game Specific Settings from Steam\config\vrto3d\app_name_config.json //----------------------------------------------------------------------------- void StereoDisplayComponent::LoadSettings(StereoDisplayDriverConfiguration& config) { + // Apply auto-depth settings BEFORE AdjustDepth so that the new ceiling / + // enabled state are observed when AdjustDepth decides whether to clamp. + auto_depth_target_disparity_.store(config.auto_depth_target_disparity, std::memory_order_relaxed); + auto_depth_smoothing_.store(config.auto_depth_smoothing, std::memory_order_relaxed); + // Disable auto temporarily so AdjustDepth applies the JSON depth as the + // live value, then restore the desired enabled state. + const bool want_auto = config.auto_depth_enabled; + auto_depth_enabled_.store(false, std::memory_order_relaxed); + // Apply loaded settings AdjustDepth(config.depth, false); AdjustConvergence(config.convergence, false); AdjustFoV(config.fov); + auto_depth_enabled_.store(want_auto, std::memory_order_relaxed); + std::unique_lock lock(cfg_mutex_); config_ = config; lock.unlock(); diff --git a/vrto3d/src/hmd_device_driver.h b/vrto3d/src/hmd_device_driver.h index 099abe35..8303c564 100644 --- a/vrto3d/src/hmd_device_driver.h +++ b/vrto3d/src/hmd_device_driver.h @@ -23,6 +23,7 @@ #include #include "accela_hamilton_runtime.h" +#include "focus_context.h" #include "vrto3dlib/json_manager.h" #include "vrto3dlib/uevr_receiver.hpp" @@ -30,6 +31,21 @@ struct _XINPUT_STATE; typedef _XINPUT_STATE XINPUT_STATE; +// Platform renderer pair: D3D11 on Windows, Vulkan on Linux. Same public +// surface (Init/Shutdown/ConfigureOsd/Osd/RequestScreenshot/OnAppConnect/...), +// selected at compile time so shared driver code uses the aliases only. +#ifdef _WIN32 +class Dx11Renderer; +class DirectModeComponent; +using StereoRenderer = Dx11Renderer; +using StereoDirectMode = DirectModeComponent; +#else +class VkRenderer; +class DirectModeComponentVk; +using StereoRenderer = VkRenderer; +using StereoDirectMode = DirectModeComponentVk; +#endif + class StereoDisplayComponent : public vr::IVRDisplayComponent { @@ -37,6 +53,10 @@ class StereoDisplayComponent : public vr::IVRDisplayComponent explicit StereoDisplayComponent( const StereoDisplayDriverConfiguration &config ); // ----- Functions to override vr::IVRDisplayComponent ----- + // IVRDisplayComponent's distortion / viewport / bounds members are pure + // virtuals and must be overridden for the class to be instantiable, but + // in direct mode the compositor never calls them — the overrides below + // are stubs. bool IsDisplayOnDesktop() override; bool IsDisplayRealDisplay() override; void GetRecommendedRenderTargetSize( uint32_t *pnWidth, uint32_t *pnHeight ) override; @@ -47,18 +67,38 @@ class StereoDisplayComponent : public vr::IVRDisplayComponent void GetWindowBounds( int32_t *pnX, int32_t *pnY, uint32_t *pnWidth, uint32_t *pnHeight ) override; StereoDisplayDriverConfiguration GetConfig(); void AdjustDepth(float new_depth, bool is_delta); - void AdjustConvergence(float new_conv, bool is_delta); + void AdjustConvergence(float new_conv, bool is_delta, bool resync = true); void AdjustFoV(float new_fov); float GetDepth(); float GetConvergence(); float GetFoV(); + // Auto-depth: an external GPU pass measures the SbS frame's max disparity + // each frame and feeds it back via FeedAutoDepthSample. While enabled, the + // component lerps depth_ toward a target derived from `manual_depth_ * + // (target / observed)`, capped by `manual_depth_` (the user's intended + // ceiling). Toggling off snaps depth_ back to manual_depth_. + bool IsAutoDepthEnabled() const; + void SetAutoDepthEnabled(bool enabled); + float GetManualDepth() const; + float GetAutoDepthTargetDisparity() const; + void SetAutoDepthTargetDisparity(float frac); + float GetAutoDepthSmoothing() const; + void SetAutoDepthSmoothing(float v); + // Diagnostic toggle for the AutoDepthAnalyzer's periodic histogram / + // disparity logs. Off by default — only useful when tuning thresholds. + bool IsAutoDepthLoggingEnabled() const; + void SetAutoDepthLoggingEnabled(bool enabled); + // disp_step_px = the analyzer's disparity quantization (histogram bucket + // width in source pixels); the input deadband is derived from it. + void FeedAutoDepthSample(uint32_t max_disp_px, uint32_t eye_w_px, + uint32_t disp_step_px); + // UE3D Monitor Mode void SetMonitorMode(bool enabled); bool IsMonitorMode(); std::string CheckUserSettings(); - std::string CheckPositionInput(); void AdjustSensitivity(float delta); void AdjustRadius(float delta); void AdjustTrackFilterRotation(float delta); @@ -68,11 +108,19 @@ class StereoDisplayComponent : public vr::IVRDisplayComponent void AdjustTrackFilterZoomSmoothing(float delta); void AdjustTrackFilterMaxZoom(float delta); void SetReset(); + // Raise the reset flag so the next XInputUpdateThread tick consumes it + // (zeros pitch/yaw, OpenTrack state, and clears the flag via + // MockControllerDeviceDriver::ConsumePoseReset). + void RequestPoseReset(); void LoadSettings(StereoDisplayDriverConfiguration& config); void ResetProjection(); void Init(uint32_t device_index); private: + // Push new depth into depth_ (atomic CAS) and the OpenVR + // Prop_UserIpdMeters_Float property. Used by both manual and auto paths. + void ApplyDepth(float new_depth); + StereoDisplayDriverConfiguration config_; std::atomic< float > depth_; std::atomic< float > convergence_; @@ -83,17 +131,44 @@ class StereoDisplayComponent : public vr::IVRDisplayComponent // UE3D Monitor Mode std::atomic< bool > monitor_mode_{ false }; + + // Auto-depth state + std::atomic< bool > auto_depth_enabled_{ false }; + std::atomic< float > manual_depth_{ 0.1f }; + std::atomic< float > auto_depth_target_disparity_{ 0.005f }; + std::atomic< float > auto_depth_smoothing_{ 0.08f }; + // EMA of incoming disparity-fraction samples (input-side jitter filter). + // -1 = uninitialized. Reset whenever auto-depth is toggled off. + std::atomic< float > auto_depth_disp_ema_{ -1.0f }; + std::atomic< bool > auto_depth_log_enabled_{ false }; + // Short temporal median over raw disparity fractions ahead of the EMA — + // a single-frame spike (particle, muzzle flash, camera clip) must not + // yank the control loop. Touched only by FeedAutoDepthSample (renderer + // thread); other threads request a clear via auto_depth_filter_reset_. + static constexpr int kAutoDepthHist = 5; + float auto_depth_frac_hist_[kAutoDepthHist] = {}; + int auto_depth_hist_n_ = 0; + int auto_depth_hist_i_ = 0; + std::atomic< bool > auto_depth_filter_reset_{ false }; }; //----------------------------------------------------------------------------- -// Purpose: Represents a Mock HMD in the system +// Purpose: Virtual HMD operating in SteamVR direct mode. The +// IVRDriverDirectModeComponent (held by direct_mode_component_) takes ownership +// of the per-eye texture exchange — game eye textures flow in via SubmitLayer +// rather than the compositor's IVRVirtualDisplay::Present path. Direct mode is +// what lets us bypass the compositor's lens-distortion / panel-mask pass that +// would otherwise carve black corners out of every frame. //----------------------------------------------------------------------------- class MockControllerDeviceDriver : public vr::ITrackedDeviceServerDriver { public: MockControllerDeviceDriver(); + ~MockControllerDeviceDriver(); + + // ITrackedDeviceServerDriver vr::EVRInitError Activate( uint32_t unObjectId ) override; void EnterStandby() override; void *GetComponent( const char *pchComponentNameAndVersion ) override; @@ -105,12 +180,38 @@ class MockControllerDeviceDriver : public vr::ITrackedDeviceServerDriver void XInputUpdateThread(); void PoseUpdateThread(); void PollHotkeysThread(); - void FocusUpdateThread(); - void AutoDepthThread(); + void MonitorModeThread(); + // Polls the connected game's foreground state and asserts cursor + // visibility / clip state per the live hide_cursor_ / lock_cursor_ / + // stereo_cursor_ atomics: blanks the system cursor set while the game is + // focused (hide / stereo cursor), clips the cursor to the focused game + // window's client rect (lock), and pushes the stereo-cursor draw state + // into the OSD renderer each tick. + void CursorControlThread(); void LoadSettings(const std::string& app_name, uint32_t app_pid, vr::EVREventType status); void SetAsync(bool enable); + // Pose-reset consumption point. Called by XInputUpdateThread (and the + // OSD Recenter button path) once the XInput-derived pitch/yaw have been + // zeroed: also zeros the cached OpenTrack attitude/position so disabling + // OpenTrack — or pressing the pose-reset hotkey while it's on — doesn't + // leave stale UDP-derived bias to be re-applied later. Finally clears the + // pose_reset flag via StereoDisplayComponent::SetReset(). + void ConsumePoseReset(); + + // Accessors used by VirtualDisplayDevice to reach shared config / focus + // state without duplicating it. + const StereoDisplayComponent* GetStereoComponent() const { return stereo_display_component_.get(); } + StereoDisplayComponent* GetStereoComponent() { return stereo_display_component_.get(); } + vrto3d::FocusContext GetFocusContext(); + + // Reach the renderer + direct-mode component so the device provider can + // drain stale shared-texture handles on VREvent_ProcessDisconnected and + // toggle the renderer's pause-on-disconnect circuit-breaker. + StereoRenderer* GetRenderer() { return renderer_.get(); } + StereoDirectMode* GetDirectModeComponent() { return direct_mode_component_.get(); } + private: std::unique_ptr< StereoDisplayComponent > stereo_display_component_; @@ -125,11 +226,26 @@ class MockControllerDeviceDriver : public vr::ITrackedDeviceServerDriver std::atomic< bool > no_profile_; std::atomic< bool > is_active_; - std::atomic< uint32_t > device_index_; + std::atomic< uint32_t > device_index_; // HMD object id (from Activate) + std::unique_ptr< StereoRenderer > renderer_; + std::unique_ptr< StereoDirectMode > direct_mode_component_; std::atomic< bool > is_on_top_; std::atomic< bool > man_on_top_; - std::atomic< bool > ue3d_on_top_; - std::atomic< bool > take_screenshot_; + // Snapshot of man_on_top_ at the moment of ProcessDisconnected, so a + // same-pid reconnect (e.g. RealVR re-init) can refocus immediately + // only if the user actually had focus enabled before the disconnect. + std::atomic< bool > focus_pre_disconnect_{ false }; + // Live mirror of stereo_display_component_->GetConfig().auto_focus. + // Updated whenever a profile/config is (re)loaded so the presenter's + // focus loop sees toggles immediately. Default true matches stereo_config.h. + std::atomic< bool > auto_focus_{ true }; + // Live mirrors of the corresponding cfg fields. Polled by CursorControlThread + // so OSD toggles take effect without a restart. + std::atomic< bool > hide_cursor_{ false }; + std::atomic< bool > lock_cursor_{ false }; + std::atomic< bool > stereo_cursor_{ false }; + std::atomic< float > cursor_depth_{ 0.0f }; + std::atomic< int > cursor_size_{ 32 }; std::atomic< bool > launch_script_executed_; std::mutex pose_mutex_; @@ -143,9 +259,9 @@ class MockControllerDeviceDriver : public vr::ITrackedDeviceServerDriver std::thread xinput_thread_; std::thread pose_thread_; std::thread hotkey_thread_; - std::thread focus_thread_; - std::thread depth_thread_; + std::thread monitor_thread_; std::thread track_thread_; + std::thread cursor_thread_; vr::HmdQuaternion_t open_track_att_; std::array open_track_pos_; diff --git a/vrto3d/src/hmd_driver_factory.cpp b/vrto3d/src/hmd_driver_factory.cpp index 945dc0f5..6538918a 100644 --- a/vrto3d/src/hmd_driver_factory.cpp +++ b/vrto3d/src/hmd_driver_factory.cpp @@ -18,14 +18,12 @@ #include "openvr_driver.h" #include -#if defined( _WIN32 ) +#ifdef _WIN32 #define HMD_DLL_EXPORT extern "C" __declspec( dllexport ) #define HMD_DLL_IMPORT extern "C" __declspec( dllimport ) -#elif defined( __GNUC__ ) || defined( COMPILER_GCC ) || defined( __APPLE__ ) -#define HMD_DLL_EXPORT extern "C" __attribute__( ( visibility( "default" ) ) ) -#define HMD_DLL_IMPORT extern "C" #else -#error "Unsupported Platform." +#define HMD_DLL_EXPORT extern "C" __attribute__(( visibility( "default" ) )) +#define HMD_DLL_IMPORT extern "C" #endif MyDeviceProvider device_provider; diff --git a/vrto3d/src/osd/osd_input.cpp b/vrto3d/src/osd/osd_input.cpp new file mode 100644 index 00000000..9d06d2da --- /dev/null +++ b/vrto3d/src/osd/osd_input.cpp @@ -0,0 +1,435 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#include "osd/osd_input.h" + +#include + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include + +#include "imgui.h" +#include "vrto3dlib/key_names.h" + +namespace vrto3d::osd { + +namespace { + +// Shared low-level mouse hook for wheel + button events. Installed once on +// first OsdInput::SetMouseHookActive(true); removed on last release. +// GetAsyncKeyState(VK_LBUTTON) returns 0 when our process isn't foreground +// (the SteamVR driver never is), so we have to source button state from the +// hook too. +std::atomic g_mouse_hook_refs{0}; +HHOOK g_mouse_hook = nullptr; +std::atomic g_wheel_delta_y{0.0f}; +std::atomic g_wheel_delta_x{0.0f}; +std::atomic g_mb_left{false}; +std::atomic g_mb_right{false}; +std::atomic g_mb_middle{false}; + +// std::atomic::fetch_add is C++20 — emulate via CAS for C++17. +void AtomicFloatAdd(std::atomic& a, float v) { + float old = a.load(); + while (!a.compare_exchange_weak(old, old + v)) {} +} + +LRESULT CALLBACK MouseHookProc(int code, WPARAM wParam, LPARAM lParam) { + if (code == HC_ACTION) { + const MSLLHOOKSTRUCT* ms = reinterpret_cast(lParam); + switch (wParam) { + case WM_MOUSEWHEEL: { + const short delta = static_cast(HIWORD(ms->mouseData)); + AtomicFloatAdd(g_wheel_delta_y, static_cast(delta) / WHEEL_DELTA); + break; + } + case WM_MOUSEHWHEEL: { + const short delta = static_cast(HIWORD(ms->mouseData)); + AtomicFloatAdd(g_wheel_delta_x, static_cast(delta) / WHEEL_DELTA); + break; + } + case WM_LBUTTONDOWN: g_mb_left.store(true); break; + case WM_LBUTTONUP: g_mb_left.store(false); break; + case WM_RBUTTONDOWN: g_mb_right.store(true); break; + case WM_RBUTTONUP: g_mb_right.store(false); break; + case WM_MBUTTONDOWN: g_mb_middle.store(true); break; + case WM_MBUTTONUP: g_mb_middle.store(false); break; + default: break; + } + } + return CallNextHookEx(nullptr, code, wParam, lParam); +} + +void EnsureMouseHook() { + if (g_mouse_hook_refs.fetch_add(1) == 0) { + g_mouse_hook = SetWindowsHookExW(WH_MOUSE_LL, MouseHookProc, GetModuleHandleW(nullptr), 0); + } +} + +void ReleaseMouseHook() { + if (g_mouse_hook_refs.fetch_sub(1) == 1) { + if (g_mouse_hook) { + UnhookWindowsHookEx(g_mouse_hook); + g_mouse_hook = nullptr; + } + } +} + +ImGuiKey VkToImGuiKey(int vk) { + switch (vk) { + case VK_TAB: return ImGuiKey_Tab; + case VK_LEFT: return ImGuiKey_LeftArrow; + case VK_RIGHT: return ImGuiKey_RightArrow; + case VK_UP: return ImGuiKey_UpArrow; + case VK_DOWN: return ImGuiKey_DownArrow; + case VK_PRIOR: return ImGuiKey_PageUp; + case VK_NEXT: return ImGuiKey_PageDown; + case VK_HOME: return ImGuiKey_Home; + case VK_END: return ImGuiKey_End; + case VK_INSERT: return ImGuiKey_Insert; + case VK_DELETE: return ImGuiKey_Delete; + case VK_BACK: return ImGuiKey_Backspace; + case VK_SPACE: return ImGuiKey_Space; + case VK_RETURN: return ImGuiKey_Enter; + case VK_ESCAPE: return ImGuiKey_Escape; + case VK_LCONTROL: return ImGuiKey_LeftCtrl; + case VK_RCONTROL: return ImGuiKey_RightCtrl; + case VK_LSHIFT: return ImGuiKey_LeftShift; + case VK_RSHIFT: return ImGuiKey_RightShift; + case VK_LMENU: return ImGuiKey_LeftAlt; + case VK_RMENU: return ImGuiKey_RightAlt; + case VK_OEM_COMMA: return ImGuiKey_Comma; + case VK_OEM_MINUS: return ImGuiKey_Minus; + case VK_OEM_PERIOD: return ImGuiKey_Period; + case VK_OEM_2: return ImGuiKey_Slash; + case VK_OEM_1: return ImGuiKey_Semicolon; + case VK_OEM_PLUS: return ImGuiKey_Equal; + case VK_OEM_4: return ImGuiKey_LeftBracket; + case VK_OEM_6: return ImGuiKey_RightBracket; + case VK_OEM_5: return ImGuiKey_Backslash; + case VK_OEM_3: return ImGuiKey_GraveAccent; + case VK_OEM_7: return ImGuiKey_Apostrophe; + default: break; + } + if (vk >= '0' && vk <= '9') return static_cast(ImGuiKey_0 + (vk - '0')); + if (vk >= 'A' && vk <= 'Z') return static_cast(ImGuiKey_A + (vk - 'A')); + if (vk >= VK_F1 && vk <= VK_F12) return static_cast(ImGuiKey_F1 + (vk - VK_F1)); + if (vk >= VK_NUMPAD0 && vk <= VK_NUMPAD9) return static_cast(ImGuiKey_Keypad0 + (vk - VK_NUMPAD0)); + return ImGuiKey_None; +} + +// Reverse lookup: numeric code → canonical portable name, delegated to the +// shared key_names vocabulary so captured binds round-trip through JsonManager +// (identical to the Linux OSD path in osd_input_linux.cpp). +std::string NameForVk(int vk) { + return vrto3d::keys::NameFromKeyCode(vk); +} + +std::string NameForPadMask(uint32_t mask) { + return vrto3d::keys::NameFromPadBits(static_cast(mask)); +} + +} // namespace + +OsdInput::OsdInput() { + // Mouse hook deferred — OsdRenderer enables it via SetMouseHookActive + // only when the menu is open (or capturing), so a closed OSD doesn't + // route every system-wide mouse event through our process. +} + +OsdInput::~OsdInput() { + if (hook_active_) ReleaseMouseHook(); +} + +void OsdInput::SetMouseHookActive(bool active) { + if (active == hook_active_) return; + if (active) EnsureMouseHook(); + else ReleaseMouseHook(); + hook_active_ = active; +} + +void OsdInput::Poll() { + keys_prev_ = keys_curr_; + for (int vk = 8; vk < 256; ++vk) { + keys_curr_[vk] = (GetAsyncKeyState(vk) & 0x8000) != 0; + } + // XInput poll — only port 0; matches existing PollHotkeysThread scope. + // `pad_curr_` keeps the raw 16-bit `wButtons` mask (no triggers) so the + // existing chord-capture iteration stays valid; trigger pressure and + // stick deflection are tracked separately for ImGui analog nav. + XINPUT_STATE st{}; + pad_prev_ = pad_curr_; + if (XInputGetState(0, &st) == ERROR_SUCCESS) { + pad_connected_ = true; + pad_curr_ = st.Gamepad.wButtons; + auto norm_stick = [](SHORT v, SHORT dz) { + int a = (v >= 0) ? v : -v; + if (a <= dz) return 0.0f; + float n = (a - dz) / static_cast(32767 - dz); + if (n > 1.0f) n = 1.0f; + return (v < 0) ? -n : n; + }; + pad_lx_ = norm_stick(st.Gamepad.sThumbLX, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE); + pad_ly_ = norm_stick(st.Gamepad.sThumbLY, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE); + pad_rx_ = norm_stick(st.Gamepad.sThumbRX, XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE); + pad_ry_ = norm_stick(st.Gamepad.sThumbRY, XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE); + pad_lt_ = st.Gamepad.bLeftTrigger / 255.0f; + pad_rt_ = st.Gamepad.bRightTrigger / 255.0f; + } else { + pad_connected_ = false; + pad_curr_ = 0; + pad_lx_ = pad_ly_ = pad_rx_ = pad_ry_ = 0.0f; + pad_lt_ = pad_rt_ = 0.0f; + } +} + +void OsdInput::FeedImGui(ImGuiIO& io, const OsdSurface& surface) { + // Cursor → per-eye coords. The headset window's client size depends + // on the active OutputMode (SbS=2W×H, weaved/anaglyph=W×H, + // FramePacked=W×(2H+gap), VirtualDesktop=2W×2H, etc.). We just + // normalize the cursor against the actual client rect and remap + // into the OSD's per-eye dimensions — works uniformly for every + // presenter without needing per-mode special cases. + HWND hwnd = static_cast(surface.hwnd); + if (!hwnd) { + HWND cached = static_cast(cached_hwnd_); + if (!cached || !IsWindow(cached)) { + cached = FindWindowW(L"VRto3D_PresentWindow", nullptr); + cached_hwnd_ = cached; + } + hwnd = cached; + } + if (hwnd) { + POINT p; + RECT client{}; + if (GetCursorPos(&p) && GetClientRect(hwnd, &client)) { + ScreenToClient(hwnd, &p); + const int cw = client.right - client.left; + const int ch = client.bottom - client.top; + if (cw > 0 && ch > 0 && surface.eye_w > 0 && surface.eye_h > 0) { + float u = static_cast(p.x) / static_cast(cw); + float v = static_cast(p.y) / static_cast(ch); + if (surface.layout == StereoLayout::HorizontalSbs) { + // Fold right half back so clicks on either eye-half + // land on the same OSD coordinate. + u = (u >= 0.5f) ? (u - 0.5f) * 2.0f : u * 2.0f; + } else if (surface.layout == StereoLayout::VerticalTab) { + v = (v >= 0.5f) ? (v - 0.5f) * 2.0f : v * 2.0f; + } + if (u < 0.0f) u = 0.0f; if (u > 1.0f) u = 1.0f; + if (v < 0.0f) v = 0.0f; if (v > 1.0f) v = 1.0f; + io.AddMousePosEvent(u * static_cast(surface.eye_w), + v * static_cast(surface.eye_h)); + } + } + } + + // Mouse buttons — read from the LL hook, not GetAsyncKeyState which + // returns 0 unless the calling process owns the foreground window. + io.AddMouseButtonEvent(0, g_mb_left.load()); + io.AddMouseButtonEvent(1, g_mb_right.load()); + io.AddMouseButtonEvent(2, g_mb_middle.load()); + + // Mouse wheel — drained from the global hook. + const float wy = g_wheel_delta_y.exchange(0.0f); + const float wx = g_wheel_delta_x.exchange(0.0f); + if (wy != 0.0f || wx != 0.0f) io.AddMouseWheelEvent(wx, wy); + + // Modifiers. + io.AddKeyEvent(ImGuiMod_Ctrl, keys_curr_[VK_CONTROL]); + io.AddKeyEvent(ImGuiMod_Shift, keys_curr_[VK_SHIFT]); + io.AddKeyEvent(ImGuiMod_Alt, keys_curr_[VK_MENU]); + io.AddKeyEvent(ImGuiMod_Super, keys_curr_[VK_LWIN] || keys_curr_[VK_RWIN]); + + // Edges for everything else — only fire on transitions. On press, also + // translate to a printable Unicode character via the active layout so + // ImGui::InputText receives WM_CHAR-equivalent text input (we don't + // have a real message pump here). + BYTE kbd_state[256]; + bool kbd_state_ok = (GetKeyboardState(kbd_state) != 0); + HKL kbd_layout = GetKeyboardLayout(0); + for (int vk = 8; vk < 256; ++vk) { + if (vk == VK_LBUTTON || vk == VK_RBUTTON || vk == VK_MBUTTON || + vk == VK_CONTROL || vk == VK_SHIFT || vk == VK_MENU) + continue; + if (keys_curr_[vk] != keys_prev_[vk]) { + ImGuiKey ik = VkToImGuiKey(vk); + if (ik != ImGuiKey_None) io.AddKeyEvent(ik, keys_curr_[vk]); + + // Character input on press only. + if (keys_curr_[vk] && !keys_prev_[vk] && kbd_state_ok && + !keys_curr_[VK_CONTROL] && !keys_curr_[VK_MENU]) { + UINT scan = MapVirtualKeyExW(vk, MAPVK_VK_TO_VSC, kbd_layout); + wchar_t wbuf[8] = {}; + int n = ToUnicodeEx(static_cast(vk), scan, kbd_state, + wbuf, 8, 0, kbd_layout); + // n > 0: characters produced; n < 0: dead key (skip); + // n == 0: no translation. Filter out control characters. + if (n > 0) { + for (int k = 0; k < n; ++k) { + unsigned cp = static_cast(wbuf[k]); + if (cp >= 0x20 && cp != 0x7F) io.AddInputCharacter(cp); + } + } + } + } + } + + // XInput → ImGui gamepad nav keys. ImGui ignores gamepad input unless + // both ConfigFlags_NavEnableGamepad AND BackendFlags_HasGamepad are set + // — toggle the backend bit with controller-connected so keyboard-only + // users don't get phantom nav highlights when there's no pad attached. + // Defaults: A=activate, B=cancel, X=text-input, Y=tweak-slower, D-pad + + // left stick navigate, shoulders page through tabs, right stick scrolls. + if (pad_connected_) io.BackendFlags |= ImGuiBackendFlags_HasGamepad; + else io.BackendFlags &= ~ImGuiBackendFlags_HasGamepad; + { + auto pad_bit = [&](uint16_t mask) -> bool { return (pad_curr_ & mask) != 0; }; + io.AddKeyEvent(ImGuiKey_GamepadFaceDown, pad_bit(XINPUT_GAMEPAD_A)); + io.AddKeyEvent(ImGuiKey_GamepadFaceRight, pad_bit(XINPUT_GAMEPAD_B)); + io.AddKeyEvent(ImGuiKey_GamepadFaceLeft, pad_bit(XINPUT_GAMEPAD_X)); + io.AddKeyEvent(ImGuiKey_GamepadFaceUp, pad_bit(XINPUT_GAMEPAD_Y)); + io.AddKeyEvent(ImGuiKey_GamepadDpadLeft, pad_bit(XINPUT_GAMEPAD_DPAD_LEFT)); + io.AddKeyEvent(ImGuiKey_GamepadDpadRight, pad_bit(XINPUT_GAMEPAD_DPAD_RIGHT)); + io.AddKeyEvent(ImGuiKey_GamepadDpadUp, pad_bit(XINPUT_GAMEPAD_DPAD_UP)); + io.AddKeyEvent(ImGuiKey_GamepadDpadDown, pad_bit(XINPUT_GAMEPAD_DPAD_DOWN)); + io.AddKeyEvent(ImGuiKey_GamepadL1, pad_bit(XINPUT_GAMEPAD_LEFT_SHOULDER)); + io.AddKeyEvent(ImGuiKey_GamepadR1, pad_bit(XINPUT_GAMEPAD_RIGHT_SHOULDER)); + io.AddKeyEvent(ImGuiKey_GamepadL3, pad_bit(XINPUT_GAMEPAD_LEFT_THUMB)); + io.AddKeyEvent(ImGuiKey_GamepadR3, pad_bit(XINPUT_GAMEPAD_RIGHT_THUMB)); + io.AddKeyEvent(ImGuiKey_GamepadStart, pad_bit(XINPUT_GAMEPAD_START)); + io.AddKeyEvent(ImGuiKey_GamepadBack, pad_bit(XINPUT_GAMEPAD_BACK)); + + auto axis = [&](ImGuiKey k, float v) { + io.AddKeyAnalogEvent(k, v > 0.0f, v); + }; + axis(ImGuiKey_GamepadL2, pad_lt_); + axis(ImGuiKey_GamepadR2, pad_rt_); + axis(ImGuiKey_GamepadLStickLeft, pad_lx_ < 0.0f ? -pad_lx_ : 0.0f); + axis(ImGuiKey_GamepadLStickRight, pad_lx_ > 0.0f ? pad_lx_ : 0.0f); + axis(ImGuiKey_GamepadLStickUp, pad_ly_ > 0.0f ? pad_ly_ : 0.0f); + axis(ImGuiKey_GamepadLStickDown, pad_ly_ < 0.0f ? -pad_ly_ : 0.0f); + axis(ImGuiKey_GamepadRStickLeft, pad_rx_ < 0.0f ? -pad_rx_ : 0.0f); + axis(ImGuiKey_GamepadRStickRight, pad_rx_ > 0.0f ? pad_rx_ : 0.0f); + axis(ImGuiKey_GamepadRStickUp, pad_ry_ > 0.0f ? pad_ry_ : 0.0f); + axis(ImGuiKey_GamepadRStickDown, pad_ry_ < 0.0f ? -pad_ry_ : 0.0f); + } + + // Capture sampling. + if (capturing_) { + if (capture_combo_) { + // Combo mode: accumulate everything held until release-all. + bool any_held = false; + for (int vk = 8; vk < 256; ++vk) { + if (vk == VK_LBUTTON || vk == VK_RBUTTON || vk == VK_MBUTTON) continue; + if (keys_curr_[vk]) { + any_held = true; + std::string name = NameForVk(vk); + if (!name.empty()) capture_set_.insert(std::move(name)); + } + } + for (uint32_t bit = 1; bit; bit <<= 1) { + if (pad_curr_ & bit) { + any_held = true; + std::string name = NameForPadMask(bit); + if (!name.empty()) capture_set_.insert(std::move(name)); + } + } + if (any_held) had_any_press_ = true; + if (had_any_press_ && !any_held && !capture_set_.empty()) { + std::string out; + for (const auto& n : capture_set_) { + if (!out.empty()) out += '+'; + out += n; + } + captured_.valid = true; + captured_.key_name = std::move(out); + capturing_ = false; + capture_set_.clear(); + had_any_press_ = false; + } + } else { + // Single-key mode: first newly-pressed key/button wins. + for (int vk = 8; vk < 256; ++vk) { + if (vk == VK_LBUTTON || vk == VK_RBUTTON || vk == VK_MBUTTON || + vk == VK_SHIFT || vk == VK_CONTROL || vk == VK_MENU || + vk == VK_LSHIFT || vk == VK_RSHIFT || vk == VK_LCONTROL|| + vk == VK_RCONTROL|| vk == VK_LMENU || vk == VK_RMENU) + continue; + if (keys_curr_[vk] && !keys_prev_[vk]) { + captured_.valid = true; + captured_.key_name = NameForVk(vk); + capturing_ = false; + break; + } + } + const uint16_t newly_down = static_cast(pad_curr_ & ~pad_prev_); + if (capturing_ && newly_down) { + for (uint32_t bit = 1; bit; bit <<= 1) { + if (newly_down & bit) { + std::string name = NameForPadMask(bit); + if (!name.empty()) { + captured_.valid = true; + captured_.key_name = std::move(name); + capturing_ = false; + break; + } + } + } + } + } + } +} + +bool OsdInput::WasPressed(int vk) const { + if (vk <= 0 || vk >= 256) return false; + return keys_curr_[vk] && !keys_prev_[vk]; +} + +bool OsdInput::IsHeld(int vk) const { + if (vk <= 0 || vk >= 256) return false; + return keys_curr_[vk]; +} + +void OsdInput::BeginCapture(bool combo) { + capturing_ = true; + capture_combo_ = combo; + captured_ = {}; + capture_set_.clear(); + had_any_press_ = false; +} + +void OsdInput::CancelCapture() { + capturing_ = false; + capture_combo_ = false; + captured_ = {}; + capture_set_.clear(); + had_any_press_ = false; +} + +CapturedKey OsdInput::PollCapture() { + if (captured_.valid) { + auto out = captured_; + captured_ = {}; + return out; + } + return {}; +} + +} // namespace vrto3d::osd diff --git a/vrto3d/src/osd/osd_input.h b/vrto3d/src/osd/osd_input.h new file mode 100644 index 00000000..da01d42b --- /dev/null +++ b/vrto3d/src/osd/osd_input.h @@ -0,0 +1,122 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include +#include +#include +#include + +struct ImGuiIO; + +namespace vrto3d::osd { + +// How the active presenter lays out the SBS frame on the user's screen. +// Determines how cursor coords are folded back into per-eye OSD space. +enum class StereoLayout { + Mono, // single composited image (LeiaSR weaved, Anaglyph, + // RowInterlaced, ColInterlaced, Checkerboard, NvStereoDx9): + // OSD appears once on screen — simple normalize-and-scale. + HorizontalSbs, // window is 2*eye_w × eye_h, OSD appears in both halves + // (SbS, VirtualDesktop, DualDisplay, DualDisplayFlip). + VerticalTab, // window is eye_w × 2*eye_h (or +gap), OSD appears in + // top + bottom (TaB, FramePacked*). +}; + +// Identifies the surface the OSD is rendering into. Used to map mouse +// coordinates from screen space to per-eye pixel space. +struct OsdSurface { + int eye_w = 0; // per-eye width (== osd_tex_ width) + int eye_h = 0; // per-eye height (== osd_tex_ height) + void* hwnd = nullptr; // Win32 HWND of the headset window (for client-area mouse mapping) + StereoLayout layout = StereoLayout::Mono; +}; + +// One-shot capture result returned by OsdInput::PollCapture(). +// `key_name` is a portable key-name string (or '+'-joined combo) from the +// vrto3dlib/key_names vocabulary, which resolves it back to a code. +struct CapturedKey { + bool valid = false; + std::string key_name; +}; + +// Win32 keyboard + XInput pump for the OSD. Polls global state each frame +// (the SteamVR driver is never the foreground process), feeds ImGuiIO, and +// supports a one-shot key-capture mode for the User Hotkeys picker. +class OsdInput { +public: + OsdInput(); + ~OsdInput(); + + OsdInput(const OsdInput&) = delete; + OsdInput& operator=(const OsdInput&) = delete; + + // Refresh internal key/mouse/wheel state. Call once per frame. + void Poll(); + + // Push refreshed state into ImGui's IO. Caller must have already + // called ImGui::SetCurrentContext(). + void FeedImGui(ImGuiIO& io, const OsdSurface& surface); + + // Returns true the first frame `vk` transitions key-down. Pass a Win32 + // VK_* code for keyboard keys or an XINPUT_GAMEPAD_* mask for buttons. + bool WasPressed(int vk) const; + + // True while `vk` is currently held. + bool IsHeld(int vk) const; + + // Begin a one-shot key-capture session for the User Hotkeys picker. + // `combo == true` accumulates every key/button held simultaneously and + // commits the captured combo (joined with '+') only after the user + // releases everything — required for XInput chord bindings like + // "XINPUT_GAMEPAD_LEFT_SHOULDER+XINPUT_GAMEPAD_RIGHT_SHOULDER". + void BeginCapture(bool combo = false); + void CancelCapture(); + CapturedKey PollCapture(); + bool IsCapturing() const { return capturing_; } + + // Toggle the always-on global mouse hook. Cheap when stable; called by + // OsdRenderer when the menu visibility changes so we don't keep a + // WH_MOUSE_LL hook installed (and dispatched to our process for every + // system-wide mouse event) when the OSD isn't actually consuming clicks. + void SetMouseHookActive(bool active); + +private: + std::array keys_curr_{}; + std::array keys_prev_{}; + uint16_t pad_curr_ = 0; + uint16_t pad_prev_ = 0; + // Analog axes from XInput port 0, normalized to [-1,1] for sticks and + // [0,1] for triggers. Fed to ImGui as analog gamepad keys so stick nav + // and analog scroll work in the OSD menu. + float pad_lx_ = 0.0f; + float pad_ly_ = 0.0f; + float pad_rx_ = 0.0f; + float pad_ry_ = 0.0f; + float pad_lt_ = 0.0f; + float pad_rt_ = 0.0f; + bool pad_connected_ = false; + bool capturing_ = false; + bool capture_combo_ = false; + bool had_any_press_ = false; + std::set capture_set_; + CapturedKey captured_; + void* cached_hwnd_ = nullptr; // HWND + bool hook_active_ = false; +}; + +} // namespace vrto3d::osd diff --git a/vrto3d/src/osd/osd_input_linux.cpp b/vrto3d/src/osd/osd_input_linux.cpp new file mode 100644 index 00000000..b2ca46ab --- /dev/null +++ b/vrto3d/src/osd/osd_input_linux.cpp @@ -0,0 +1,405 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#ifndef _WIN32 + +// Linux implementation of OsdInput. Same public API as the Win32 version in +// osd_input.cpp so osd_menu.cpp / osd_renderer_vk.cpp compile unchanged. +// +// Sources all state from VRto3DLib's portable evdev pump (input_state.h): +// - keyboard level state via IsKeyDown (GetAsyncKeyState equivalent), +// - keyboard edges + typed text via DrainKeyEvents / DrainTypedUtf8 +// (replaces the win version's GetKeyboardState+ToUnicodeEx dance), +// - mouse via GetMouseState — the evdev virtual cursor already lives in +// per-eye pixel space (clamped to SetMouseRegion), so unlike the Win32 +// path there is no window-rect normalize/fold step, +// - gamepad via GetGamepadState (XINPUT_GAMEPAD-shaped). +// Everything evdev reports is global, so the WH_MOUSE_LL hook the Windows +// build needs is unnecessary here; SetMouseHookActive is a stub. + +#include "osd/osd_input.h" + +#include +#include +#include + +#include "imgui.h" + +#include "vrto3dlib/input_state.h" + +// VK_* / XINPUT_GAMEPAD_* numeric codes (plus deadzone constants) and the +// portable key-name vocabulary — the same headers the driver and JsonManager +// use, so captured names round-trip through profiles unchanged. +#include "vrto3dlib/key_codes.h" +#include "vrto3dlib/key_names.h" + +namespace vrto3d::osd { + +namespace { + +// Latest evdev snapshot, refreshed by Poll(). File-scope (not members) since +// the OsdInput layout in osd_input.h is shared with the Win32 build; there is +// exactly one OsdInput per driver, mirroring the win version's file-scope +// hook state. Poll() replaces the previous batch every frame, so edge/text +// events that were never fed to ImGui (menu closed) are discarded instead of +// flooding the UI when it opens. +struct PumpState { + vrto3d::input::MouseState mouse; + std::vector key_events; + std::string typed_utf8; + int region_w = 0; + int region_h = 0; +}; +PumpState g_pump; + +ImGuiKey VkToImGuiKey(int vk) { + switch (vk) { + case VK_TAB: return ImGuiKey_Tab; + case VK_LEFT: return ImGuiKey_LeftArrow; + case VK_RIGHT: return ImGuiKey_RightArrow; + case VK_UP: return ImGuiKey_UpArrow; + case VK_DOWN: return ImGuiKey_DownArrow; + case VK_PRIOR: return ImGuiKey_PageUp; + case VK_NEXT: return ImGuiKey_PageDown; + case VK_HOME: return ImGuiKey_Home; + case VK_END: return ImGuiKey_End; + case VK_INSERT: return ImGuiKey_Insert; + case VK_DELETE: return ImGuiKey_Delete; + case VK_BACK: return ImGuiKey_Backspace; + case VK_SPACE: return ImGuiKey_Space; + case VK_RETURN: return ImGuiKey_Enter; + case VK_ESCAPE: return ImGuiKey_Escape; + case VK_LCONTROL: return ImGuiKey_LeftCtrl; + case VK_RCONTROL: return ImGuiKey_RightCtrl; + case VK_LSHIFT: return ImGuiKey_LeftShift; + case VK_RSHIFT: return ImGuiKey_RightShift; + case VK_LMENU: return ImGuiKey_LeftAlt; + case VK_RMENU: return ImGuiKey_RightAlt; + case VK_OEM_COMMA: return ImGuiKey_Comma; + case VK_OEM_MINUS: return ImGuiKey_Minus; + case VK_OEM_PERIOD: return ImGuiKey_Period; + case VK_OEM_2: return ImGuiKey_Slash; + case VK_OEM_1: return ImGuiKey_Semicolon; + case VK_OEM_PLUS: return ImGuiKey_Equal; + case VK_OEM_4: return ImGuiKey_LeftBracket; + case VK_OEM_6: return ImGuiKey_RightBracket; + case VK_OEM_5: return ImGuiKey_Backslash; + case VK_OEM_3: return ImGuiKey_GraveAccent; + case VK_OEM_7: return ImGuiKey_Apostrophe; + default: break; + } + if (vk >= '0' && vk <= '9') return static_cast(ImGuiKey_0 + (vk - '0')); + if (vk >= 'A' && vk <= 'Z') return static_cast(ImGuiKey_A + (vk - 'A')); + if (vk >= VK_F1 && vk <= VK_F12) return static_cast(ImGuiKey_F1 + (vk - VK_F1)); + if (vk >= VK_NUMPAD0 && vk <= VK_NUMPAD9) return static_cast(ImGuiKey_Keypad0 + (vk - VK_NUMPAD0)); + return ImGuiKey_None; +} + +// Reverse lookup: numeric code → canonical portable name, delegated to the +// shared key_names vocabulary so captured binds match what JsonManager writes. +// Keyboard/mouse codes and gamepad bitmasks go through separate lookups so a +// D-pad bit (0x0001) can't alias VK_LBUTTON (0x01) the way a single combined +// map would. +std::string NameForVk(int vk) { + return vrto3d::keys::NameFromKeyCode(vk); +} + +std::string NameForPadMask(uint32_t mask) { + return vrto3d::keys::NameFromPadBits(static_cast(mask)); +} + +} // namespace + +OsdInput::OsdInput() { + // Make sure the evdev pump is running. Idempotent — the driver's hotkey + // thread has usually started it already; this covers standalone use. + vrto3d::input::Start(); +} + +OsdInput::~OsdInput() { + // The evdev pump is shared driver-wide (hotkeys keep using it); nothing + // to release here — no per-instance hook like the Win32 build. +} + +void OsdInput::SetMouseHookActive(bool active) { + // evdev input is always global; there is no LL hook to install/remove. + // Track the flag anyway for API parity. + hook_active_ = active; +} + +void OsdInput::Poll() { + keys_prev_ = keys_curr_; + for (int vk = 8; vk < 256; ++vk) { + keys_curr_[vk] = vrto3d::input::IsKeyDown(vk); + } + + // Mouse snapshot. GetMouseState drains the wheel accumulator, so take it + // once per Poll and let FeedImGui consume the copy. Mirror the buttons + // into keys_curr_ so WasPressed/IsHeld and the capture exclusion lists + // treat VK_LBUTTON & co. the same way GetAsyncKeyState does on Windows. + g_pump.mouse = vrto3d::input::GetMouseState(); + keys_curr_[VK_LBUTTON] = g_pump.mouse.left; + keys_curr_[VK_RBUTTON] = g_pump.mouse.right; + keys_curr_[VK_MBUTTON] = g_pump.mouse.middle; + keys_curr_[VK_XBUTTON1] = g_pump.mouse.x1; + keys_curr_[VK_XBUTTON2] = g_pump.mouse.x2; + + // Drain key edges + typed text, replacing whatever the previous frame + // left unconsumed (stale input from frames where the OSD wasn't fed must + // not burst into ImGui when the menu opens). + g_pump.key_events.clear(); + vrto3d::input::KeyEvent ev_buf[64]; + for (;;) { + const int n = vrto3d::input::DrainKeyEvents(ev_buf, 64); + for (int i = 0; i < n; ++i) g_pump.key_events.push_back(ev_buf[i]); + if (n < 64) break; + } + g_pump.typed_utf8.clear(); + char text_buf[512]; + if (vrto3d::input::DrainTypedUtf8(text_buf, sizeof(text_buf)) > 0) { + g_pump.typed_utf8.assign(text_buf); + } + + // Gamepad poll — merged state across connected pads; matches the win + // version's XInput port 0 scope. `pad_curr_` keeps the raw 16-bit + // wButtons mask so the chord-capture iteration stays valid; triggers and + // stick deflection are tracked separately for ImGui analog nav. + pad_prev_ = pad_curr_; + const vrto3d::input::GamepadState pad = vrto3d::input::GetGamepadState(); + if (pad.connected) { + pad_connected_ = true; + pad_curr_ = pad.wButtons; + auto norm_stick = [](int16_t v, int16_t dz) { + int a = (v >= 0) ? v : -v; + if (a <= dz) return 0.0f; + float n = (a - dz) / static_cast(32767 - dz); + if (n > 1.0f) n = 1.0f; + return (v < 0) ? -n : n; + }; + pad_lx_ = norm_stick(pad.sThumbLX, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE); + pad_ly_ = norm_stick(pad.sThumbLY, XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE); + pad_rx_ = norm_stick(pad.sThumbRX, XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE); + pad_ry_ = norm_stick(pad.sThumbRY, XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE); + pad_lt_ = pad.bLeftTrigger / 255.0f; + pad_rt_ = pad.bRightTrigger / 255.0f; + } else { + pad_connected_ = false; + pad_curr_ = 0; + pad_lx_ = pad_ly_ = pad_rx_ = pad_ry_ = 0.0f; + pad_lt_ = pad_rt_ = 0.0f; + } +} + +void OsdInput::FeedImGui(ImGuiIO& io, const OsdSurface& surface) { + // The evdev virtual cursor already lives in per-eye pixel space — just + // keep its clamp region synced to the OSD surface and use it directly. + // (The Win32 build has to normalize against the headset window's client + // rect and fold SbS/TaB halves; none of that applies here.) + if (surface.eye_w > 0 && surface.eye_h > 0 && + (surface.eye_w != g_pump.region_w || surface.eye_h != g_pump.region_h)) { + vrto3d::input::SetMouseRegion(surface.eye_w, surface.eye_h); + g_pump.region_w = surface.eye_w; + g_pump.region_h = surface.eye_h; + } + io.AddMousePosEvent(static_cast(g_pump.mouse.x), + static_cast(g_pump.mouse.y)); + + io.AddMouseButtonEvent(0, g_pump.mouse.left); + io.AddMouseButtonEvent(1, g_pump.mouse.right); + io.AddMouseButtonEvent(2, g_pump.mouse.middle); + + if (g_pump.mouse.wheel != 0) { + io.AddMouseWheelEvent(0.0f, static_cast(g_pump.mouse.wheel)); + g_pump.mouse.wheel = 0; // consumed — don't repeat if fed again + } + + // Modifiers (level-triggered, like the win version). + io.AddKeyEvent(ImGuiMod_Ctrl, keys_curr_[VK_CONTROL]); + io.AddKeyEvent(ImGuiMod_Shift, keys_curr_[VK_SHIFT]); + io.AddKeyEvent(ImGuiMod_Alt, keys_curr_[VK_MENU]); + io.AddKeyEvent(ImGuiMod_Super, keys_curr_[VK_LWIN] || keys_curr_[VK_RWIN]); + + // Key edges from the evdev queue. Generic modifier codes are covered by + // the AddKeyEvent(ImGuiMod_*) calls above; mouse buttons by the + // AddMouseButtonEvent calls. + for (const auto& ev : g_pump.key_events) { + if (ev.vk <= 0) continue; + if (ev.vk == VK_LBUTTON || ev.vk == VK_RBUTTON || ev.vk == VK_MBUTTON || + ev.vk == VK_CONTROL || ev.vk == VK_SHIFT || ev.vk == VK_MENU) + continue; + ImGuiKey ik = VkToImGuiKey(ev.vk); + if (ik != ImGuiKey_None) io.AddKeyEvent(ik, ev.down); + } + g_pump.key_events.clear(); + + // Typed text (xkbcommon-translated by the evdev pump). Suppress while + // Ctrl/Alt are held, mirroring the win version's ToUnicodeEx gate, and + // filter ASCII control bytes (single bytes in UTF-8, so a byte-wise + // filter can't split a multi-byte sequence). + if (!g_pump.typed_utf8.empty() && + !keys_curr_[VK_CONTROL] && !keys_curr_[VK_MENU]) { + std::string filtered; + filtered.reserve(g_pump.typed_utf8.size()); + for (char c : g_pump.typed_utf8) { + const unsigned char u = static_cast(c); + if (u >= 0x20 && u != 0x7F) filtered.push_back(c); + } + if (!filtered.empty()) io.AddInputCharactersUTF8(filtered.c_str()); + } + g_pump.typed_utf8.clear(); + + // Gamepad → ImGui nav keys. ImGui ignores gamepad input unless both + // ConfigFlags_NavEnableGamepad AND BackendFlags_HasGamepad are set — + // toggle the backend bit with controller-connected so keyboard-only + // users don't get phantom nav highlights when there's no pad attached. + if (pad_connected_) io.BackendFlags |= ImGuiBackendFlags_HasGamepad; + else io.BackendFlags &= ~ImGuiBackendFlags_HasGamepad; + { + auto pad_bit = [&](uint16_t mask) -> bool { return (pad_curr_ & mask) != 0; }; + io.AddKeyEvent(ImGuiKey_GamepadFaceDown, pad_bit(XINPUT_GAMEPAD_A)); + io.AddKeyEvent(ImGuiKey_GamepadFaceRight, pad_bit(XINPUT_GAMEPAD_B)); + io.AddKeyEvent(ImGuiKey_GamepadFaceLeft, pad_bit(XINPUT_GAMEPAD_X)); + io.AddKeyEvent(ImGuiKey_GamepadFaceUp, pad_bit(XINPUT_GAMEPAD_Y)); + io.AddKeyEvent(ImGuiKey_GamepadDpadLeft, pad_bit(XINPUT_GAMEPAD_DPAD_LEFT)); + io.AddKeyEvent(ImGuiKey_GamepadDpadRight, pad_bit(XINPUT_GAMEPAD_DPAD_RIGHT)); + io.AddKeyEvent(ImGuiKey_GamepadDpadUp, pad_bit(XINPUT_GAMEPAD_DPAD_UP)); + io.AddKeyEvent(ImGuiKey_GamepadDpadDown, pad_bit(XINPUT_GAMEPAD_DPAD_DOWN)); + io.AddKeyEvent(ImGuiKey_GamepadL1, pad_bit(XINPUT_GAMEPAD_LEFT_SHOULDER)); + io.AddKeyEvent(ImGuiKey_GamepadR1, pad_bit(XINPUT_GAMEPAD_RIGHT_SHOULDER)); + io.AddKeyEvent(ImGuiKey_GamepadL3, pad_bit(XINPUT_GAMEPAD_LEFT_THUMB)); + io.AddKeyEvent(ImGuiKey_GamepadR3, pad_bit(XINPUT_GAMEPAD_RIGHT_THUMB)); + io.AddKeyEvent(ImGuiKey_GamepadStart, pad_bit(XINPUT_GAMEPAD_START)); + io.AddKeyEvent(ImGuiKey_GamepadBack, pad_bit(XINPUT_GAMEPAD_BACK)); + + auto axis = [&](ImGuiKey k, float v) { + io.AddKeyAnalogEvent(k, v > 0.0f, v); + }; + axis(ImGuiKey_GamepadL2, pad_lt_); + axis(ImGuiKey_GamepadR2, pad_rt_); + axis(ImGuiKey_GamepadLStickLeft, pad_lx_ < 0.0f ? -pad_lx_ : 0.0f); + axis(ImGuiKey_GamepadLStickRight, pad_lx_ > 0.0f ? pad_lx_ : 0.0f); + axis(ImGuiKey_GamepadLStickUp, pad_ly_ > 0.0f ? pad_ly_ : 0.0f); + axis(ImGuiKey_GamepadLStickDown, pad_ly_ < 0.0f ? -pad_ly_ : 0.0f); + axis(ImGuiKey_GamepadRStickLeft, pad_rx_ < 0.0f ? -pad_rx_ : 0.0f); + axis(ImGuiKey_GamepadRStickRight, pad_rx_ > 0.0f ? pad_rx_ : 0.0f); + axis(ImGuiKey_GamepadRStickUp, pad_ry_ > 0.0f ? pad_ry_ : 0.0f); + axis(ImGuiKey_GamepadRStickDown, pad_ry_ < 0.0f ? -pad_ry_ : 0.0f); + } + + // Capture sampling — identical flow to the Win32 implementation. + if (capturing_) { + if (capture_combo_) { + // Combo mode: accumulate everything held until release-all. + bool any_held = false; + for (int vk = 8; vk < 256; ++vk) { + if (vk == VK_LBUTTON || vk == VK_RBUTTON || vk == VK_MBUTTON) continue; + if (keys_curr_[vk]) { + any_held = true; + std::string name = NameForVk(vk); + if (!name.empty()) capture_set_.insert(std::move(name)); + } + } + for (uint32_t bit = 1; bit; bit <<= 1) { + if (pad_curr_ & bit) { + any_held = true; + std::string name = NameForPadMask(bit); + if (!name.empty()) capture_set_.insert(std::move(name)); + } + } + if (any_held) had_any_press_ = true; + if (had_any_press_ && !any_held && !capture_set_.empty()) { + std::string out; + for (const auto& n : capture_set_) { + if (!out.empty()) out += '+'; + out += n; + } + captured_.valid = true; + captured_.key_name = std::move(out); + capturing_ = false; + capture_set_.clear(); + had_any_press_ = false; + } + } else { + // Single-key mode: first newly-pressed key/button wins. + for (int vk = 8; vk < 256; ++vk) { + if (vk == VK_LBUTTON || vk == VK_RBUTTON || vk == VK_MBUTTON || + vk == VK_SHIFT || vk == VK_CONTROL || vk == VK_MENU || + vk == VK_LSHIFT || vk == VK_RSHIFT || vk == VK_LCONTROL|| + vk == VK_RCONTROL|| vk == VK_LMENU || vk == VK_RMENU) + continue; + if (keys_curr_[vk] && !keys_prev_[vk]) { + captured_.valid = true; + captured_.key_name = NameForVk(vk); + capturing_ = false; + break; + } + } + const uint16_t newly_down = static_cast(pad_curr_ & ~pad_prev_); + if (capturing_ && newly_down) { + for (uint32_t bit = 1; bit; bit <<= 1) { + if (newly_down & bit) { + std::string name = NameForPadMask(bit); + if (!name.empty()) { + captured_.valid = true; + captured_.key_name = std::move(name); + capturing_ = false; + break; + } + } + } + } + } + } +} + +bool OsdInput::WasPressed(int vk) const { + if (vk <= 0 || vk >= 256) return false; + return keys_curr_[vk] && !keys_prev_[vk]; +} + +bool OsdInput::IsHeld(int vk) const { + if (vk <= 0 || vk >= 256) return false; + return keys_curr_[vk]; +} + +void OsdInput::BeginCapture(bool combo) { + capturing_ = true; + capture_combo_ = combo; + captured_ = {}; + capture_set_.clear(); + had_any_press_ = false; +} + +void OsdInput::CancelCapture() { + capturing_ = false; + capture_combo_ = false; + captured_ = {}; + capture_set_.clear(); + had_any_press_ = false; +} + +CapturedKey OsdInput::PollCapture() { + if (captured_.valid) { + auto out = captured_; + captured_ = {}; + return out; + } + return {}; +} + +} // namespace vrto3d::osd + +#endif // !_WIN32 diff --git a/vrto3d/src/osd/osd_menu.cpp b/vrto3d/src/osd/osd_menu.cpp new file mode 100644 index 00000000..650ffe25 --- /dev/null +++ b/vrto3d/src/osd/osd_menu.cpp @@ -0,0 +1,1095 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#include "osd/osd_menu.h" + +#include +#include +#include +#include + +#include "imgui.h" + +#include "osd/osd_input.h" +#include "hmd_device_driver.h" +#include "platform.h" +#include "vrto3dlib/stereo_config.h" +#include "vrto3dlib/key_codes.h" +#include "vrto3dlib/key_names.h" + +namespace vrto3d::osd { + +struct OsdMenu::Impl { + StereoDisplayComponent* component = nullptr; + MenuCallbacks callbacks; + std::atomic visible{false}; + + std::string version; + std::string app_name; + + // Cached monitor enumeration for the System tab combo. Refreshed whenever + // the user opens the menu (cheap, but not free). + std::vector monitors; + std::vector monitor_labels; + bool monitors_refreshed = false; + void RefreshMonitors(); + + void DrawStereoTab(); + void DrawUserHotkeysTab(OsdInput& input); + void DrawTrackingTab(OsdInput& input); + void DrawShaderTab(); + void DrawSystemTab(); + void DrawFooter(); + void DrawTitleChrome(); + + // Re-derive every runtime bind field from the symbolic *_str strings, then + // push the config to the driver. Every tab's dirty handler funnels through + // this so an OSD capture/edit takes effect immediately. + void ApplyConfig(StereoDisplayDriverConfiguration& cfg); + + // State for the click-to-capture key picker. -1 row = none. + int capture_row_ = -1; + bool capture_combo_pending_ = false; + + // Click-to-capture state for the Tracking-tab single-key pickers. + // 0 = none, 1 = pose_reset, 2 = ctrl_toggle. + int tracking_capture_target_ = 0; + bool tracking_capture_combo_ = false; + + // Footer height measured on the previous frame, used to reserve the + // pinned bottom strip on the current frame. Zero on the first frame — + // BuildUI falls back to a one-row estimate until we have a real number. + float footer_h_measured = 0.0f; +}; + +OsdMenu::OsdMenu(StereoDisplayComponent* component, MenuCallbacks callbacks) + : impl_(std::make_unique()) { + impl_->component = component; + impl_->callbacks = std::move(callbacks); +} + +OsdMenu::~OsdMenu() = default; + +void OsdMenu::SetVisible(bool visible) { + impl_->visible.store(visible); + if (visible) impl_->monitors_refreshed = false; +} +void OsdMenu::Toggle() { + bool now = !impl_->visible.load(); + impl_->visible.store(now); + if (now) impl_->monitors_refreshed = false; +} +bool OsdMenu::Visible() const { return impl_->visible.load(); } + +void OsdMenu::SetVersion(std::string v) { impl_->version = std::move(v); } +void OsdMenu::SetAppName(std::string a) { impl_->app_name = std::move(a); } + +void OsdMenu::BuildUI(OsdInput& input) { + auto& s = *impl_; + if (!s.visible.load() || !s.component) return; + + // Default the menu to roughly half the OSD surface — comfortable at any + // render resolution, leaves plenty of game visible around it, and the + // user can still drag-resize after. Min-size keeps a floor so the menu + // remains usable on very small surfaces; max-size caps it to the OSD. + // (std::clamp)/(std::max) paren-wraps suppress windows.h min/max macros. + const ImVec2 disp = ImGui::GetIO().DisplaySize; + const float kPad = 20.0f; + const float kFloorW = 320.0f; + const float kFloorH = 240.0f; + const float min_w = (std::clamp)(disp.x - 2 * kPad, kFloorW, 720.0f); + const float min_h = (std::clamp)(disp.y - 2 * kPad, kFloorH, 520.0f); + const float max_w = (std::max)(disp.x, min_w); + const float max_h = (std::max)(disp.y, min_h); + const float init_w = (std::clamp)(disp.x * 0.5f, min_w, max_w); + const float init_h = (std::clamp)(disp.y * 0.5f, min_h, max_h); + ImGui::SetNextWindowSizeConstraints(ImVec2(min_w, min_h), ImVec2(max_w, max_h)); + ImGui::SetNextWindowSize(ImVec2(init_w, init_h), ImGuiCond_FirstUseEver); + // Center the initial position so the menu sits roughly mid-screen. + const float init_x = (std::max)(0.0f, (disp.x - init_w) * 0.5f); + const float init_y = (std::max)(0.0f, (disp.y - init_h) * 0.5f); + ImGui::SetNextWindowPos(ImVec2(init_x, init_y), ImGuiCond_FirstUseEver); + + const std::string title = "VRto3D " + s.version + "###vrto3d_menu"; + ImGuiWindowFlags wflags = ImGuiWindowFlags_NoCollapse; + bool open = true; + if (!ImGui::Begin(title.c_str(), &open, wflags)) { + ImGui::End(); + if (!open) s.visible.store(false); + return; + } + if (!open) s.visible.store(false); + + s.DrawTitleChrome(); + + // Reserve space at the bottom so the footer action buttons stay pinned + // when a tab's content overflows the viewport. The footer wraps across + // 1–2 rows depending on window width, so use the previous frame's + // measured height and fall back to a one-row estimate on the first + // frame. Snap to a small minimum so the very first frame doesn't render + // with the child eating the entire window. + const float footer_h_fallback = ImGui::GetFrameHeightWithSpacing() + + ImGui::GetStyle().ItemSpacing.y; + const float footer_h = s.footer_h_measured > 0.0f + ? s.footer_h_measured + : footer_h_fallback; + + if (ImGui::BeginTabBar("##vrto3d_tabs")) { + auto draw_tab = [&](const char* label, const char* child_id, auto&& fn) { + if (ImGui::BeginTabItem(label)) { + // Per-tab child so scroll state is preserved independently + // and the tab content can scroll without dragging the tab + // bar or footer along with it. + ImGui::BeginChild(child_id, ImVec2(0, -footer_h), false, + ImGuiWindowFlags_HorizontalScrollbar); + fn(); + ImGui::EndChild(); + ImGui::EndTabItem(); + } + }; + draw_tab("Stereo", "##scroll_stereo", [&]{ s.DrawStereoTab(); }); + draw_tab("User Hotkeys", "##scroll_user", [&]{ s.DrawUserHotkeysTab(input); }); + draw_tab("Tracking", "##scroll_track", [&]{ s.DrawTrackingTab(input); }); + draw_tab("Shader", "##scroll_shader", [&]{ s.DrawShaderTab(); }); + draw_tab("System", "##scroll_system", [&]{ s.DrawSystemTab(); }); + ImGui::EndTabBar(); + } + + // Measure the footer's actual rendered height for next frame's + // reservation. Captured around DrawFooter so it includes the leading + // separator and any wrap-induced extra rows. + const float footer_top = ImGui::GetCursorPosY(); + s.DrawFooter(); + s.footer_h_measured = ImGui::GetCursorPosY() - footer_top; + + ImGui::End(); +} + +void OsdMenu::Impl::RefreshMonitors() { + monitors.clear(); + monitor_labels.clear(); + monitors = platform::EnumerateMonitors(); + // Index 0 = "primary / auto"; non-zero = explicit selection. + monitor_labels.reserve(monitors.size() + 1); + monitor_labels.emplace_back("0: Primary (auto)"); + for (const auto& m : monitors) { + char buf[160]; + std::snprintf(buf, sizeof(buf), "%d: %s (%ux%u @%.0fHz)%s", + m.index, m.device_name.c_str(), + m.width, m.height, m.refresh_hz, + m.is_primary ? " *" : ""); + monitor_labels.emplace_back(buf); + } + monitors_refreshed = true; +} + +// --------------------------------------------------------------------------- +// Title bar chrome — rendered as a small row at the top of the menu showing +// the current profile + the always-on-top toggle. +// --------------------------------------------------------------------------- +void OsdMenu::Impl::DrawTitleChrome() { + ImGui::TextDisabled("Profile:"); + ImGui::SameLine(); + ImGui::TextUnformatted(app_name.empty() ? "(default)" : app_name.c_str()); + + if (callbacks.always_on_top && callbacks.toggle_always_on_top) { + const char* label = "Always on Top"; + const char* hint = "(Ctrl + F8)"; + const ImGuiStyle& style = ImGui::GetStyle(); + const float box_w = ImGui::CalcTextSize(label).x + + ImGui::GetFrameHeight() + + style.ItemInnerSpacing.x; + const float hint_w = ImGui::CalcTextSize(hint).x + + style.ItemSpacing.x; + const float target_x = ImGui::GetWindowWidth() - box_w - hint_w - + style.WindowPadding.x; + // Only right-align when there's room past the current cursor. On + // narrow windows the profile name plus checkbox overlap; let the + // checkbox fall to its own line in that case. + if (target_x > ImGui::GetCursorPosX()) { + ImGui::SameLine(); + ImGui::SetCursorPosX(target_x); + } + bool on = callbacks.always_on_top(); + if (ImGui::Checkbox(label, &on)) { + callbacks.toggle_always_on_top(); + } + ImGui::SameLine(); + ImGui::TextDisabled("%s", hint); + } + ImGui::Separator(); +} + +// --------------------------------------------------------------------------- +// Footer — global save/reload buttons present on every tab. +// --------------------------------------------------------------------------- +void OsdMenu::Impl::DrawFooter() { + ImGui::Separator(); + + const bool have_app = !app_name.empty(); + bool first = true; + + // SameLine before the next button only when it actually fits in the + // remaining content region — otherwise let the default vertical layout + // wrap it to a new row. Without this, narrow OSD surfaces clip the + // rightmost footer buttons. + auto try_sameline = [&](const char* next_label) { + if (first) { first = false; return; } + const float btn_w = ImGui::CalcTextSize(next_label).x + + ImGui::GetStyle().FramePadding.x * 2.0f; + const float needed = btn_w + ImGui::GetStyle().ItemSpacing.x; + if (ImGui::GetContentRegionAvail().x >= needed) { + ImGui::SameLine(); + } + }; + + if (callbacks.save_game_profile) { + const std::string label = have_app + ? "Save " + app_name + "_config.json" + : std::string("Save Game Cfg"); + try_sameline(label.c_str()); + ImGui::BeginDisabled(!have_app); + if (ImGui::Button(label.c_str())) { + callbacks.save_game_profile("Saved " + app_name + "_config.json"); + } + ImGui::EndDisabled(); + } + if (callbacks.save_default_profile) { + const char* label = "Save Default Cfg"; + try_sameline(label); + if (ImGui::Button(label)) { + callbacks.save_default_profile("Saved default_config.json"); + } + } + if (callbacks.reload_game_profile) { + const char* label = "Reload Game Cfg"; + try_sameline(label); + ImGui::BeginDisabled(!have_app); + if (ImGui::Button(label)) { + callbacks.reload_game_profile("Reloaded " + app_name + "_config.json"); + } + ImGui::EndDisabled(); + } + if (callbacks.reload_default_profile) { + const char* label = "Reload Default Cfg"; + try_sameline(label); + if (ImGui::Button(label)) { + callbacks.reload_default_profile("Reloaded default_config.json"); + } + } +} + +// --------------------------------------------------------------------------- +// Stereo tab — depth, convergence, FoV, projection helpers. +// --------------------------------------------------------------------------- +void OsdMenu::Impl::DrawStereoTab() { + auto cfg = component->GetConfig(); + + // While auto-depth is on, the slider edits the user's ceiling (manual_depth_) + // rather than the live, auto-modulated depth — otherwise nudging the slider + // would clobber the ceiling with whatever value the auto loop has currently + // attenuated to. The live value is surfaced separately in the Auto-Depth + // section below. + const bool auto_on = callbacks.get_auto_depth_enabled + && callbacks.get_auto_depth_enabled(); + float depth = auto_on ? component->GetManualDepth() : component->GetDepth(); + if (ImGui::SliderFloat("Depth", &depth, 0.0f, 0.5f, "%.3f")) { + component->AdjustDepth(depth, false); + // Mirror Ctrl+F3/F4 — re-sync projection so the new depth + // takes effect immediately rather than waiting for the next + // convergence change or profile reload. + if (callbacks.reset_projection) callbacks.reset_projection(); + } + ImGui::SameLine(); ImGui::TextDisabled("(Ctrl+F3 / Ctrl+F4)"); + + float conv = component->GetConvergence(); + // Reversed range — left = higher numeric (less stereo separation), right + // = lower numeric (more separation). Matches the Ctrl+F5/F6 convention + // where "Decrease Convergence" actually raises the numeric value. + if (ImGui::SliderFloat("Convergence", &conv, 4.0f, 0.001f, "%.3f")) { + component->AdjustConvergence(conv, false); + } + ImGui::SameLine(); ImGui::TextDisabled("(Ctrl+F5 / Ctrl+F6)"); + + int fov = static_cast(component->GetFoV() + 0.5f); + if (ImGui::SliderInt("FoV", &fov, 30, 120)) { + component->AdjustFoV(static_cast(fov)); + } + + if (callbacks.get_auto_depth_enabled && callbacks.set_auto_depth_enabled) { + ImGui::Separator(); + bool ad = callbacks.get_auto_depth_enabled(); + if (ImGui::Checkbox("Auto-Depth", &ad)) { + callbacks.set_auto_depth_enabled(ad); + } + ImGui::SameLine(); ImGui::TextDisabled("(Ctrl+F11)"); + + ImGui::Text("Current Auto Depth: %.3f", component->GetDepth()); + + if (callbacks.get_auto_depth_target && callbacks.set_auto_depth_target) { + float target = callbacks.get_auto_depth_target(); + if (ImGui::SliderFloat("Comfort Target", &target, 0.001f, 0.02f, "%.3f")) { + callbacks.set_auto_depth_target(target); + } + ImGui::SameLine(); ImGui::TextDisabled("(fraction of eye width)"); + } + if (callbacks.get_auto_depth_smoothing && callbacks.set_auto_depth_smoothing) { + float smoothing = callbacks.get_auto_depth_smoothing(); + if (ImGui::SliderFloat("Smoothing", &smoothing, 0.005f, 0.25f, "%.3f")) { + callbacks.set_auto_depth_smoothing(smoothing); + } + ImGui::SameLine(); ImGui::TextDisabled("(higher = snappier)"); + } + if (callbacks.get_auto_depth_logging && callbacks.set_auto_depth_logging) { + bool log_on = callbacks.get_auto_depth_logging(); + if (ImGui::Checkbox("Log Disparity Samples", &log_on)) { + callbacks.set_auto_depth_logging(log_on); + } + ImGui::SameLine(); ImGui::TextDisabled("(periodic histogram to vrto3d.txt)"); + } + ImGui::Separator(); + } + + bool eye_swap = cfg.eye_swap; + if (ImGui::Checkbox("Swap Eyes", &eye_swap)) { + cfg.eye_swap = eye_swap; + ApplyConfig(cfg); + } + +} + +// Helpers for the User Hotkeys tab. +namespace { + +std::string FloatsToCsv(const std::vector& v, int prec = 3) { + std::string out; + char buf[32]; + for (size_t i = 0; i < v.size(); ++i) { + std::snprintf(buf, sizeof(buf), "%.*f", prec, v[i]); + if (!out.empty()) out += ", "; + out += buf; + } + return out; +} + +// Re-derive a hotkey row's runtime fields (user_load_key + load_xinput) from +// its symbolic string. Mirrors the parser in JsonManager::LoadProfileFromJson +// so edits made through the OSD take effect without needing a save+reload. +void ReparseHotkey(const std::string& s, int32_t& code, bool* xinput) { + // Shared cross-platform parser (key_names accepts both the portable + // vocabulary and legacy VK_*/XINPUT_* spellings, so OSD-entered names parse + // identically to JsonManager). migrate=false: this only re-derives the + // runtime code for a live edit; the string is rewritten to canonical + // spelling on the next profile save. + std::string name = s; + int32_t parsed = 0; + bool xi = false; + vrto3d::keys::ParseBind(name, parsed, xi, /*migrate=*/false); + code = parsed; + if (xinput) *xinput = xi; +} + +std::vector CsvToFloats(const char* s) { + std::vector out; + if (!s) return out; + const char* p = s; + while (*p) { + char* end = nullptr; + float v = std::strtof(p, &end); + if (end == p) { ++p; continue; } + out.push_back(v); + p = end; + while (*p == ',' || *p == ' ' || *p == '\t') ++p; + } + return out; +} + +} // namespace + +// Re-derive the runtime bind fields (load keys, xinput flags, bind types) from +// the symbolic *_str strings for every user-preset row and the pose_reset / +// ctrl_toggle keys, then push the config to the driver. All tab dirty handlers +// funnel through here: previously only the System tab re-derived, so a gamepad +// bind captured in the User Hotkeys or Tracking tab kept load_xinput=false and +// user_load_key=0 and never fired until a profile save+reload. +void OsdMenu::Impl::ApplyConfig(StereoDisplayDriverConfiguration& cfg) { + for (size_t i = 0; i < cfg.num_user_settings; ++i) { + bool xi = false; + ReparseHotkey(cfg.user_load_str[i], cfg.user_load_key[i], &xi); + cfg.load_xinput[i] = xi; + const int kt = vrto3d::keys::KeyBindTypeFromName(cfg.user_type_str[i]); + if (kt >= 0) cfg.user_key_type[i] = kt; + } + ReparseHotkey(cfg.pose_reset_str, cfg.pose_reset_key, &cfg.reset_xinput); + ReparseHotkey(cfg.ctrl_toggle_str, cfg.ctrl_toggle_key, &cfg.ctrl_xinput); + const int kt = vrto3d::keys::KeyBindTypeFromName(cfg.ctrl_type_str); + if (kt >= 0) cfg.ctrl_type = kt; + component->LoadSettings(cfg); +} + +// --------------------------------------------------------------------------- +// User Hotkeys tab — editable rows + click-to-capture picker. +// --------------------------------------------------------------------------- +void OsdMenu::Impl::DrawUserHotkeysTab(OsdInput& input) { + auto cfg = component->GetConfig(); + bool dirty = false; + + ImGui::TextWrapped( + "Each row maps a Load key to a (Depth, Convergence, FoV) preset. " + "Only \"toggle\" mode supports comma-separated values that cycle on each press; " + "\"switch\" and \"hold\" keep only the first value. " + "FoV = 0 means \"use the active profile FoV\". Use Copy Current to fill the " + "row from the current settings."); + ImGui::Spacing(); + + // Active capture session — drain the input pump. + if (capture_row_ >= 0) { + ImGui::TextColored(ImVec4(1, 0.85f, 0.2f, 1), + capture_combo_pending_ + ? "Capturing combo for Load on row %d — press a chord, then release all to commit. (Esc to cancel)" + : "Capturing key for Load on row %d — press a key or controller button. (Esc to cancel)", + capture_row_ + 1); + + CapturedKey c = input.PollCapture(); + if (c.valid) { + const size_t r = static_cast(capture_row_); + if (r < cfg.num_user_settings) { + cfg.user_load_str[r] = c.key_name; + dirty = true; + } + capture_row_ = -1; + capture_combo_pending_ = false; + input.CancelCapture(); + } + if (input.WasPressed(VK_ESCAPE)) { + capture_row_ = -1; + capture_combo_pending_ = false; + input.CancelCapture(); + } + ImGui::Separator(); + } + + static const char* kModes[] = { "switch", "toggle", "hold" }; + + // Only "toggle" mode is allowed to keep multi-element preset cycles. For + // "switch" and "hold" we collapse depth/conv/fov to the first entry. Used + // both when the mode dropdown changes away from toggle and when the user + // commits a multi-value CSV in a non-toggle row. + auto trimRowToFirst = [&](size_t row) { + if (cfg.user_depth[row].size() > 1) cfg.user_depth[row].resize(1); + if (cfg.user_convergence[row].size() > 1) cfg.user_convergence[row].resize(1); + if (cfg.user_fov[row].size() > 1) cfg.user_fov[row].resize(1); + if (row < cfg.user_preset_index.size()) cfg.user_preset_index[row] = 0; + }; + + if (ImGui::BeginTable("##user_hotkeys", 5, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | + ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("#", ImGuiTableColumnFlags_WidthFixed, 28.0f); + ImGui::TableSetupColumn("Load", ImGuiTableColumnFlags_WidthStretch, 1.2f); + ImGui::TableSetupColumn("Mode", ImGuiTableColumnFlags_WidthFixed, 100.0f); + ImGui::TableSetupColumn("Depth / Conv / FoV (comma = cycle, toggle only)", ImGuiTableColumnFlags_WidthStretch, 3.0f); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 28.0f); + ImGui::TableHeadersRow(); + + for (size_t i = 0; i < cfg.num_user_settings; ++i) { + ImGui::TableNextRow(); + ImGui::PushID(static_cast(i)); + + ImGui::TableNextColumn(); + ImGui::Text("%zu", i + 1); + + // ----- Load key ----- + ImGui::TableNextColumn(); + { + char buf[256]; + std::snprintf(buf, sizeof(buf), "%s", cfg.user_load_str[i].c_str()); + ImGui::SetNextItemWidth(-FLT_MIN); + if (ImGui::InputText("##load", buf, sizeof(buf))) { + cfg.user_load_str[i] = buf; + dirty = true; + } + if (ImGui::Button("Set##load")) { + capture_row_ = static_cast(i); + capture_combo_pending_ = false; + input.BeginCapture(false); + } + ImGui::SameLine(); + if (ImGui::Button("Combo##load")) { + capture_row_ = static_cast(i); + capture_combo_pending_ = true; + input.BeginCapture(true); + } + } + + // ----- Mode ----- + ImGui::TableNextColumn(); + { + int sel = 0; + for (int k = 0; k < IM_ARRAYSIZE(kModes); ++k) + if (cfg.user_type_str[i] == kModes[k]) { sel = k; break; } + ImGui::SetNextItemWidth(-FLT_MIN); + if (ImGui::Combo("##mode", &sel, kModes, IM_ARRAYSIZE(kModes))) { + cfg.user_type_str[i] = kModes[sel]; + if (cfg.user_type_str[i] != "toggle") { + trimRowToFirst(i); + } + dirty = true; + } + } + + // ----- Preset CSVs ----- + ImGui::TableNextColumn(); + { + const bool toggle_mode = (cfg.user_type_str[i] == "toggle"); + auto editCsv = [&](const char* label_id, std::vector& vec, int prec) { + char buf[256]; + std::string csv = FloatsToCsv(vec, prec); + std::snprintf(buf, sizeof(buf), "%s", csv.c_str()); + ImGui::SetNextItemWidth(-FLT_MIN); + if (ImGui::InputText(label_id, buf, sizeof(buf), + ImGuiInputTextFlags_EnterReturnsTrue)) { + auto parsed = CsvToFloats(buf); + if (!parsed.empty()) { + // Non-toggle modes never cycle, so commit only the + // first value regardless of how many the user typed. + if (!toggle_mode && parsed.size() > 1) parsed.resize(1); + vec = std::move(parsed); + dirty = true; + } + } + }; + ImGui::Text("Depth"); ImGui::SameLine(80); + editCsv("##depth", cfg.user_depth[i], 3); + ImGui::Text("Conv"); ImGui::SameLine(80); + editCsv("##conv", cfg.user_convergence[i], 3); + ImGui::Text("FoV"); ImGui::SameLine(80); + editCsv("##fov", cfg.user_fov[i], 1); + + if (ImGui::Button("Copy Current")) { + // Replace the row's preset list with a single-element list + // containing the current depth/convergence/FoV. Edit the + // CSV by hand if you want to add more presets to cycle. + cfg.user_depth[i] = { component->GetDepth() }; + cfg.user_convergence[i] = { component->GetConvergence() }; + cfg.user_fov[i] = { component->GetFoV() }; + if (i < cfg.user_preset_index.size()) + cfg.user_preset_index[i] = 0; + dirty = true; + } + ImGui::SameLine(); + + const size_t presets = cfg.user_depth[i].size(); + if (presets > 1) { + const size_t cur = i < cfg.user_preset_index.size() + ? cfg.user_preset_index[i] : 0; + ImGui::TextDisabled("Cycle: preset %zu of %zu (Enter to apply edits)", + cur + 1, presets); + } else { + ImGui::TextDisabled("Enter to apply edits"); + } + } + + // ----- Remove row ----- + ImGui::TableNextColumn(); + if (ImGui::SmallButton("X")) { + cfg.user_load_key.erase(cfg.user_load_key.begin() + i); + cfg.user_key_type.erase(cfg.user_key_type.begin() + i); + cfg.user_load_str.erase(cfg.user_load_str.begin() + i); + cfg.user_type_str.erase(cfg.user_type_str.begin() + i); + cfg.user_depth.erase(cfg.user_depth.begin() + i); + cfg.user_convergence.erase(cfg.user_convergence.begin() + i); + cfg.user_fov.erase(cfg.user_fov.begin() + i); + if (i < cfg.user_preset_index.size()) + cfg.user_preset_index.erase(cfg.user_preset_index.begin() + i); + if (i < cfg.prev_depth.size()) cfg.prev_depth.erase(cfg.prev_depth.begin() + i); + if (i < cfg.prev_convergence.size()) cfg.prev_convergence.erase(cfg.prev_convergence.begin() + i); + if (i < cfg.prev_fov.size()) cfg.prev_fov.erase(cfg.prev_fov.begin() + i); + if (i < cfg.was_held.size()) cfg.was_held.erase(cfg.was_held.begin() + i); + if (i < cfg.load_xinput.size()) cfg.load_xinput.erase(cfg.load_xinput.begin() + i); + if (i < cfg.sleep_count.size()) cfg.sleep_count.erase(cfg.sleep_count.begin() + i); + cfg.num_user_settings = cfg.user_load_key.size(); + dirty = true; + ImGui::PopID(); + break; // bail out; next frame will re-render with the new size + } + + ImGui::PopID(); + } + ImGui::EndTable(); + } + + ImGui::Spacing(); + if (ImGui::Button("+ Add Preset Row")) { + cfg.user_load_key.push_back(0); + cfg.user_key_type.push_back(0); // SWITCH + cfg.user_load_str.push_back(""); + cfg.user_type_str.push_back("switch"); + cfg.user_depth.push_back({ component->GetDepth() }); + cfg.user_convergence.push_back({ component->GetConvergence() }); + cfg.user_fov.push_back({ component->GetFoV() }); + cfg.user_preset_index.push_back(0); + cfg.prev_depth.push_back(0.0f); + cfg.prev_convergence.push_back(1.0f); + cfg.prev_fov.push_back(component->GetFoV()); + cfg.was_held.push_back(false); + cfg.load_xinput.push_back(false); + cfg.sleep_count.push_back(0); + cfg.num_user_settings = cfg.user_load_key.size(); + dirty = true; + } + ImGui::SameLine(); + ImGui::TextDisabled("Save profile from the footer to persist changes."); + + if (dirty) { + ApplyConfig(cfg); + } +} + +// --------------------------------------------------------------------------- +// Tracking tab — HMD pose, controller, OpenTrack, track filter, LeiaSR. +// --------------------------------------------------------------------------- +void OsdMenu::Impl::DrawTrackingTab(OsdInput& input) { + auto cfg = component->GetConfig(); + bool dirty = false; + + // Helper: trigger a recenter on a true→false checkbox transition so + // stale pitch / yaw / OpenTrack accumulation doesn't bleed into the + // pose the next time tracking is enabled or composed. + auto recenter_on_disable = [&](bool was_on, bool now_on) { + if (was_on && !now_on && callbacks.recenter_pose) callbacks.recenter_pose(); + }; + + if (ImGui::CollapsingHeader("HMD Pose", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::DragFloat("Origin X", &cfg.hmd_x, 0.01f, -10.0f, 10.0f, "%.2f")) dirty = true; + if (ImGui::DragFloat("Origin Y (fwd)",&cfg.hmd_y, 0.01f, -10.0f, 10.0f, "%.2f")) dirty = true; + if (ImGui::DragFloat("Height", &cfg.hmd_height, 0.01f, 0.0f, 3.0f, "%.2f")) dirty = true; + if (ImGui::DragFloat("Yaw", &cfg.hmd_yaw, 0.1f, -360.0f, 360.0f, "%.2f")) dirty = true; + ImGui::TextDisabled("HMD pose persists with 'Save Default Cfg' in the footer."); + } + + if (ImGui::CollapsingHeader("XInput (Xbox) Controller")) { + const bool was_pitch = cfg.pitch_enable; + if (ImGui::Checkbox("Pitch (right stick)", &cfg.pitch_enable)) { + dirty = true; + recenter_on_disable(was_pitch, cfg.pitch_enable); + } + const bool was_yaw = cfg.yaw_enable; + if (ImGui::Checkbox("Yaw (right stick)", &cfg.yaw_enable)) { + dirty = true; + recenter_on_disable(was_yaw, cfg.yaw_enable); + } + if (ImGui::SliderFloat("Pitch Radius", &cfg.pitch_radius, 0.0f, 1.0f, "%.3f")) dirty = true; + if (ImGui::SliderFloat("Sensitivity", &cfg.ctrl_sensitivity,0.0f, 5.0f, "%.2f")) dirty = true; + if (ImGui::SliderFloat("Stick Deadzone", &cfg.ctrl_deadzone, 0.0f, 1.0f, "%.3f")) dirty = true; + + ImGui::Separator(); + + // Drain the input pump while a tracking-tab capture is active. + // Combo capture supports XInput chord bindings (e.g. + // LBUMPER+RBUMPER) — keyboard combos won't resolve through + // json_manager's parser and will silently no-op, matching the + // User Hotkeys tab's existing behavior. + if (tracking_capture_target_ != 0) { + ImGui::TextColored(ImVec4(1, 0.85f, 0.2f, 1), + tracking_capture_combo_ + ? "Capturing combo — press a chord, then release all to commit. (Esc to cancel)" + : "Capturing key — press a key or controller button. (Esc to cancel)"); + CapturedKey c = input.PollCapture(); + if (c.valid) { + if (tracking_capture_target_ == 1) { + cfg.pose_reset_str = c.key_name; + } else if (tracking_capture_target_ == 2) { + cfg.ctrl_toggle_str = c.key_name; + } + dirty = true; + tracking_capture_target_ = 0; + tracking_capture_combo_ = false; + input.CancelCapture(); + } else if (input.WasPressed(VK_ESCAPE)) { + tracking_capture_target_ = 0; + tracking_capture_combo_ = false; + input.CancelCapture(); + } + ImGui::Separator(); + } + + auto hotkey_row = [&](const char* label, int target, std::string& out_str) { + ImGui::PushID(label); + char buf[256]; + std::snprintf(buf, sizeof(buf), "%s", out_str.c_str()); + if (ImGui::InputText(label, buf, sizeof(buf))) { + out_str = buf; + dirty = true; + } + if (ImGui::Button("Set")) { + tracking_capture_target_ = target; + tracking_capture_combo_ = false; + input.BeginCapture(false); + } + ImGui::SameLine(); + if (ImGui::Button("Combo")) { + tracking_capture_target_ = target; + tracking_capture_combo_ = true; + input.BeginCapture(true); + } + ImGui::SameLine(); + ImGui::TextDisabled("(VK_* or XINPUT_GAMEPAD_*)"); + ImGui::PopID(); + }; + + hotkey_row("Pose Reset Key", 1, cfg.pose_reset_str); + hotkey_row("Toggle Key", 2, cfg.ctrl_toggle_str); + + static const char* kModes[] = { "toggle", "hold" }; + int sel = 0; + for (int k = 0; k < IM_ARRAYSIZE(kModes); ++k) + if (cfg.ctrl_type_str == kModes[k]) { sel = k; break; } + if (ImGui::Combo("Toggle Mode", &sel, kModes, IM_ARRAYSIZE(kModes))) { + cfg.ctrl_type_str = kModes[sel]; + dirty = true; + } + } + + if (ImGui::CollapsingHeader("OpenTrack")) { + const bool was_open_track = cfg.use_open_track; + if (ImGui::Checkbox("Enable OpenTrack", &cfg.use_open_track)) { + // LeiaSR's pose pipeline depends on the AccelaHamilton track + // filter being active; the startup loader auto-enables it for + // LeiaSR + use_open_track, but a runtime toggle has to do the + // same coupling so the filter actually runs. + if (cfg.use_open_track && cfg.output_mode == OutputMode::LeiaSR && + !cfg.use_track_filter) { + cfg.use_track_filter = true; + } + dirty = true; + recenter_on_disable(was_open_track, cfg.use_open_track); + } + if (ImGui::InputInt("UDP Port", &cfg.open_track_port)) dirty = true; + ImGui::SameLine(); ImGui::TextDisabled("(Requires Restart)"); + } + + if (ImGui::CollapsingHeader("Track Filter")) { + if (ImGui::Checkbox("Enable Filter", &cfg.use_track_filter)) dirty = true; + ImGui::BeginDisabled(!cfg.use_track_filter); + if (ImGui::SliderFloat("Rotation Sens", &cfg.trk_flt_rot_sens, 0.0f, 5.0f, "%.2f")) dirty = true; + if (ImGui::SliderFloat("Position Sens", &cfg.trk_flt_pos_sens, 0.0f, 5.0f, "%.2f")) dirty = true; + if (ImGui::SliderFloat("Rotation DZ", &cfg.trk_flt_rot_dz, 0.0f, 0.5f, "%.3f")) dirty = true; + if (ImGui::SliderFloat("Position DZ", &cfg.trk_flt_pos_dz, 0.0f, 0.5f, "%.3f")) dirty = true; + if (ImGui::SliderFloat("Zoom Smoothing", &cfg.trk_flt_zoom_smooth, 0.0f, 1.0f, "%.2f")) dirty = true; + if (ImGui::SliderFloat("Max Zoom", &cfg.trk_flt_max_zoom, 1.0f, 50.0f, "%.1f")) dirty = true; + ImGui::EndDisabled(); + } + +#ifdef _WIN32 + // LeiaSR head tracking (SR SDK) is Windows-only — the presenter is compiled + // out on Linux, so this whole section is hidden there. + if (cfg.output_mode == OutputMode::LeiaSR && + ImGui::CollapsingHeader("LeiaSR Head Tracking")) { + if (ImGui::Checkbox("Enable LeiaSR Tracking", &cfg.sr_tracking_enabled)) dirty = true; + ImGui::TextWrapped("Disable to drive OpenTrack from another source " + "(e.g. the OpenTrack app) instead of the built-in " + "SR head tracker."); + ImGui::BeginDisabled(!cfg.sr_tracking_enabled); + if (callbacks.calibrate_leiasr_head) { + if (ImGui::Button("Calibrate (snap current head pose to neutral)")) { + callbacks.calibrate_leiasr_head(); + } + ImGui::Separator(); + } + // Filter cutoffs / betas + if (ImGui::SliderFloat("Pos MinCutoff", &cfg.sr_filter_pos_mincutoff, 0.0f, 5.0f, "%.3f")) dirty = true; + if (ImGui::SliderFloat("Pos Beta", &cfg.sr_filter_pos_beta, 0.0f, 1.0f, "%.3f")) dirty = true; + if (ImGui::SliderFloat("Rot MinCutoff", &cfg.sr_filter_rot_mincutoff, 0.0f, 5.0f, "%.3f")) dirty = true; + if (ImGui::SliderFloat("Rot Beta", &cfg.sr_filter_rot_beta, 0.0f, 1.0f, "%.3f")) dirty = true; + if (ImGui::SliderFloat("Angle DZ (deg)", &cfg.sr_angle_deadzone_deg, 0.0f, 5.0f, "%.2f")) dirty = true; + ImGui::Separator(); + if (ImGui::SliderFloat("Sens Yaw", &cfg.sr_sens_yaw, 0.0f, 5.0f, "%.2f")) dirty = true; + if (ImGui::SliderFloat("Sens Pitch", &cfg.sr_sens_pitch, 0.0f, 5.0f, "%.2f")) dirty = true; + if (ImGui::SliderFloat("Sens Roll", &cfg.sr_sens_roll, 0.0f, 5.0f, "%.2f")) dirty = true; + if (ImGui::SliderFloat("Max Yaw", &cfg.sr_max_yaw, 0.0f, 180.0f, "%.1f")) dirty = true; + if (ImGui::SliderFloat("Max Pitch", &cfg.sr_max_pitch, 0.0f, 180.0f, "%.1f")) dirty = true; + if (ImGui::SliderFloat("Max Roll", &cfg.sr_max_roll, 0.0f, 180.0f, "%.1f")) dirty = true; + ImGui::Separator(); + static const char* track_modes[] = { + "XYZ_YawPitch", "XYZ", "YawPitch", "Full6DOF", "YawPitchRoll" }; + int sel = 0; + for (int i = 0; i < IM_ARRAYSIZE(track_modes); ++i) { + if (cfg.sr_track_mode == track_modes[i]) { sel = i; break; } + } + if (ImGui::Combo("Track Mode", &sel, track_modes, IM_ARRAYSIZE(track_modes))) { + cfg.sr_track_mode = track_modes[sel]; + dirty = true; + } + ImGui::EndDisabled(); + } +#endif // _WIN32 (LeiaSR head tracking) + + if (dirty) { + ApplyConfig(cfg); + } +} + +// --------------------------------------------------------------------------- +// Shader tab — display-correction post-process pass (Lift / Gamma / Gain + +// extended S-Curve). All defaults are pass-through; pipeline is skipped +// entirely when disabled, so a user who never opens this tab pays nothing. +// Intended for users with crosstalk-prone 3D displays (e.g. passive row- +// interlaced monitors), where lowering Gain and reducing the curve below +// 1.0 trades some brightness/contrast for sharply less visible crosstalk. +// --------------------------------------------------------------------------- +void OsdMenu::Impl::DrawShaderTab() { + auto cfg = component->GetConfig(); + bool dirty = false; + + if (ImGui::Checkbox("Enable Display Correction Shader", &cfg.shader_enabled)) { + dirty = true; + } + ImGui::TextWrapped("Reduces high-contrast crosstalk on some 3D displays " + "(e.g. passive row-interlaced monitors). Sacrifices " + "brightness and contrast — disable when not needed."); + + if (ImGui::CollapsingHeader("Lift / Gamma / Gain", + ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::SliderFloat3("RGB Lift", cfg.shader_lift, 0.0f, 2.0f, "%.3f")) dirty = true; + ImGui::SameLine(); ImGui::TextDisabled("(shadows)"); + if (ImGui::SliderFloat3("RGB Gamma", cfg.shader_gamma, 0.0f, 2.0f, "%.3f")) dirty = true; + ImGui::SameLine(); ImGui::TextDisabled("(midtones)"); + if (ImGui::SliderFloat3("RGB Gain", cfg.shader_gain, 0.0f, 2.0f, "%.3f")) dirty = true; + ImGui::SameLine(); ImGui::TextDisabled("(highlights)"); + } + + if (ImGui::CollapsingHeader("S-Curve", + ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::SliderFloat("Curve", &cfg.shader_curve, 0.33f, 3.0f, "%.3f")) dirty = true; + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("1.0 = pass-through.\n" + "Below 1.0: reduces midtone contrast (helps with crosstalk).\n" + "Above 1.0: increases midtone contrast."); + } + if (ImGui::SliderFloat("Curve Offset (Low)", &cfg.shader_curve_off_low, -1.0f, 1.0f, "%.3f")) dirty = true; + if (ImGui::SliderFloat("Curve Offset (High)", &cfg.shader_curve_off_high, -1.0f, 1.0f, "%.3f")) dirty = true; + if (ImGui::SliderFloat("Curve Offset (Both)", &cfg.shader_curve_off_both, -1.0f, 1.0f, "%.3f")) dirty = true; + } + + if (ImGui::Button("Reset to Defaults")) { + cfg.shader_lift[0] = cfg.shader_lift[1] = cfg.shader_lift[2] = 1.0f; + cfg.shader_gamma[0] = cfg.shader_gamma[1] = cfg.shader_gamma[2] = 1.0f; + cfg.shader_gain[0] = cfg.shader_gain[1] = cfg.shader_gain[2] = 1.0f; + cfg.shader_curve = 1.0f; + cfg.shader_curve_off_low = 0.0f; + cfg.shader_curve_off_high = 0.0f; + cfg.shader_curve_off_both = 0.0f; + dirty = true; + } + + if (dirty) { + ApplyConfig(cfg); + } +} + +// --------------------------------------------------------------------------- +// System tab — display/output, misc, about. +// --------------------------------------------------------------------------- +void OsdMenu::Impl::DrawSystemTab() { + auto cfg = component->GetConfig(); + bool dirty = false; + + if (!monitors_refreshed) RefreshMonitors(); + + if (ImGui::CollapsingHeader("Display & Output (Requires Restart)", + ImGuiTreeNodeFlags_DefaultOpen)) { + // Display Index combo populated from the monitor enumeration. Item 0 + // is "Primary (auto)"; items 1..N are explicit \\.\DISPLAYn entries + // tagged with resolution and refresh. + int sel = cfg.display_index; + if (sel < 0 || sel > static_cast(monitors.size())) sel = 0; + std::vector ptrs; + ptrs.reserve(monitor_labels.size()); + for (auto& s : monitor_labels) ptrs.push_back(s.c_str()); + if (ImGui::Combo("Display Index", &sel, ptrs.data(), + static_cast(ptrs.size()))) { + cfg.display_index = sel; + dirty = true; + } + ImGui::SameLine(); + if (ImGui::SmallButton("Refresh##displays")) RefreshMonitors(); + + // Output modes with a per-entry "Windows-only" flag. LeiaSR (SR SDK) + // and NvidiaDX9 (NVAPI/D3D9 3D Vision) have no Linux presenter, so + // they're filtered out there — building the visible list this way + // keeps the label->enum mapping correct regardless of what's hidden. + struct ModeEntry { const char* label; OutputMode mode; bool win_only; }; + static const ModeEntry kModeEntries[] = { + {"SbS", OutputMode::SbS, false}, + {"TaB", OutputMode::TaB, false}, + {"RowInterlaced", OutputMode::RowInterlaced, false}, + {"ColInterlaced", OutputMode::ColInterlaced, false}, + {"Checkerboard", OutputMode::Checkerboard, false}, + {"LeiaSR", OutputMode::LeiaSR, true}, + {"NvidiaDX9", OutputMode::NvidiaDX9, true}, + {"WibbleWobble", OutputMode::WibbleWobble, false}, + {"VirtualDesktop", OutputMode::VirtualDesktop, false}, + {"FramePacked720p60", OutputMode::FramePacked720p60, false}, + {"FramePacked1080p24", OutputMode::FramePacked1080p24, false}, + {"FramePacked1080p60", OutputMode::FramePacked1080p60, false}, + {"FramePacked1080p60CVT", OutputMode::FramePacked1080p60CVT, false}, + {"DualDisplay", OutputMode::DualDisplay, false}, + {"DualDisplayFlip", OutputMode::DualDisplayFlip, false}, + {"AnaglyphRedCyan", OutputMode::AnaglyphRedCyan, false}, + {"AnaglyphRedCyanDubois", OutputMode::AnaglyphRedCyanDubois, false}, + {"AnaglyphRedCyanDeghosted", OutputMode::AnaglyphRedCyanDeghosted, false}, + {"AnaglyphRedCyanCompromise", OutputMode::AnaglyphRedCyanCompromise, false}, + {"AnaglyphGreenMagenta", OutputMode::AnaglyphGreenMagenta, false}, + {"AnaglyphGreenMagentaDubois", OutputMode::AnaglyphGreenMagentaDubois, false}, + {"AnaglyphGreenMagentaDeghosted", OutputMode::AnaglyphGreenMagentaDeghosted, false}, + {"AnaglyphBlueAmber", OutputMode::AnaglyphBlueAmber, false}, + {"Mono", OutputMode::Mono, false}, + }; + std::vector mode_labels; + std::vector mode_vals; + int mode_sel = 0; + for (const auto& e : kModeEntries) { +#ifndef _WIN32 + if (e.win_only) continue; +#endif + if (e.mode == cfg.output_mode) mode_sel = static_cast(mode_labels.size()); + mode_labels.push_back(e.label); + mode_vals.push_back(e.mode); + } + if (ImGui::Combo("Output Mode", &mode_sel, mode_labels.data(), + static_cast(mode_labels.size()))) { + cfg.output_mode = mode_vals[mode_sel]; + dirty = true; + } + + if (ImGui::InputInt("Render Width", &cfg.render_width)) dirty = true; + if (ImGui::InputInt("Render Height", &cfg.render_height)) dirty = true; + if (ImGui::InputFloat("Display Frequency", &cfg.display_frequency, 0.0f, 0.0f, "%.2f")) dirty = true; + ImGui::SameLine(); ImGui::TextDisabled("(0.0 = use current)"); + if (ImGui::DragFloat("Aspect Ratio", &cfg.aspect_ratio, 0.001f, 0.5f, 4.0f, "%.3f")) dirty = true; + } + + if (ImGui::CollapsingHeader("Misc")) { + if (ImGui::Checkbox("Dashboard Enable", &cfg.dash_enable)) dirty = true; + if (ImGui::Checkbox("Async Reprojection", &cfg.async_enable)) { + dirty = true; + if (callbacks.set_async) callbacks.set_async(cfg.async_enable); + } +#ifdef _WIN32 + // Auto Focus (game-window focus juggling) and cursor hide/lock have no + // effect on Linux: the overlay is a non-focusable click-through surface + // (input reaches the game directly, OSD reads evdev), and the game + // manages its own cursor. Hidden there to avoid dead controls. + if (ImGui::Checkbox("Auto Focus", &cfg.auto_focus)) { + dirty = true; + if (callbacks.set_auto_focus) callbacks.set_auto_focus(cfg.auto_focus); + } +#endif + if (ImGui::Checkbox("Auto Exit SteamVR", &cfg.auto_exit)) dirty = true; +#ifdef _WIN32 + if (ImGui::Checkbox("Hide Cursor", &cfg.hide_cursor)) { + dirty = true; + if (callbacks.set_hide_cursor) callbacks.set_hide_cursor(cfg.hide_cursor); + } + ImGui::SameLine(); + ImGui::TextDisabled("(hide OS cursor over the game window)"); + if (ImGui::Checkbox("Lock Cursor to Game", &cfg.lock_cursor)) { + dirty = true; + if (callbacks.set_lock_cursor) callbacks.set_lock_cursor(cfg.lock_cursor); + } + ImGui::SameLine(); + ImGui::TextDisabled("(clip cursor to game window while focused)"); + if (ImGui::Checkbox("Stereo Cursor", &cfg.stereo_cursor)) { + dirty = true; + if (callbacks.set_stereo_cursor) callbacks.set_stereo_cursor(cfg.stereo_cursor); + } + ImGui::SameLine(); + ImGui::TextDisabled("(hide OS cursor, draw a per-eye 3D cursor instead)"); + if (cfg.stereo_cursor) { + if (ImGui::DragFloat("Cursor Depth", &cfg.cursor_depth, 0.1f, -30.0f, 30.0f, "%.1f px")) { + dirty = true; + if (callbacks.set_cursor_depth) callbacks.set_cursor_depth(cfg.cursor_depth); + } + ImGui::SameLine(); + ImGui::TextDisabled("(0 = screen plane, + = into screen)"); + if (ImGui::DragInt("Cursor Size", &cfg.cursor_size, 1, 8, 256, "%d px")) { + dirty = true; + if (callbacks.set_cursor_size) callbacks.set_cursor_size(cfg.cursor_size); + } + } +#endif + char buf[512]; + std::snprintf(buf, sizeof(buf), "%s", cfg.launch_script.c_str()); + if (ImGui::InputText("Launch Script", buf, sizeof(buf))) { + cfg.launch_script = buf; + dirty = true; + } + if (ImGui::Checkbox("Disable Hotkeys (menu: Ctrl+Home / Start+DPad-Down; " + "on-top: Ctrl+F8 / Start+DPad-Up still work)", + &cfg.disable_hotkeys)) { + dirty = true; + } + if (callbacks.reset_defaults) { + if (ImGui::Button("Reset Defaults")) { + callbacks.reset_defaults("Reset to factory defaults"); + } + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 0.85f, 0.2f, 1), + "Restores factory stereo/shader/tracking settings " + "(display/output kept). Reload or Save to persist."); + } + if (callbacks.download_latest_profiles) { + if (ImGui::Button("Download Latest Profiles")) { + callbacks.download_latest_profiles(); + } + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 0.85f, 0.2f, 1), + "Warning: overwrites existing profiles"); + } + if (callbacks.take_screenshot) { + if (ImGui::Button("Take Screenshot")) { + callbacks.take_screenshot(); + } + ImGui::SameLine(); + ImGui::TextDisabled("(also Ctrl+F12)"); + } + if (callbacks.open_config_folder) { + if (ImGui::Button("Open Profile Folder")) { + callbacks.open_config_folder(); + } + } + if (callbacks.open_screenshot_folder) { + if (callbacks.open_config_folder) ImGui::SameLine(); + if (ImGui::Button("Open Screenshot Folder")) { + callbacks.open_screenshot_folder(); + } + } + } + + if (ImGui::CollapsingHeader("About", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("VRto3D %s", version.c_str()); + ImGui::Text("Build: %s %s", __DATE__, __TIME__); + ImGui::Text("Profile: %s", + app_name.empty() ? "(default)" : app_name.c_str()); + } + + if (dirty) { + ApplyConfig(cfg); + } +} + +} // namespace vrto3d::osd diff --git a/vrto3d/src/osd/osd_menu.h b/vrto3d/src/osd/osd_menu.h new file mode 100644 index 00000000..0121a304 --- /dev/null +++ b/vrto3d/src/osd/osd_menu.h @@ -0,0 +1,141 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include +#include +#include +#include + +class StereoDisplayComponent; + +namespace vrto3d::osd { + +class OsdInput; + +// Callbacks the menu uses to perform actions that live outside the +// StereoDisplayComponent's API surface (profile save/load, file-manager +// shell-out, etc). All are invoked on the OSD render thread. +struct MenuCallbacks { + // Save the live config to "{app_name}_config.json". Empty app_name means + // there is no game-specific profile (button should be disabled). + std::function save_game_profile; + // Save the live config to "default_config.json". + std::function save_default_profile; + // Reload "{app_name}_config.json" into the live config. + std::function reload_game_profile; + // Reload "default_config.json" into the live config. + std::function reload_default_profile; + // Reset the live config to VRto3D's built-in factory defaults (stereo / + // shader / tracking tunables), preserving the display/output hardware + // fields. Distinct from reload_default_profile, which re-reads the user's + // (possibly customized) default_config.json. + std::function reset_defaults; + // Reset projection (mirrors Ctrl+F3). + std::function reset_projection; + // Snap the current LeiaSR head pose as the neutral zero. No-op when the + // active presenter isn't LeiaSR. + std::function calibrate_leiasr_head; + // Mirror of the pose-reset hotkey: zero XInput pitch/yaw, clear the + // cached OpenTrack pose, then ask SteamVR to take the cleaned HMD pose + // as the new seated/standing zero. Surfaced as the Recenter button at + // the top of the Tracking tab. + std::function recenter_pose; + // Toggle "always on top" (mirrors Ctrl+F8). + std::function toggle_always_on_top; + // True iff the headset window is currently always-on-top. + std::function always_on_top; + // Open the vrto3d Steam config folder in the OS file manager. + std::function open_config_folder; + // Open the SteamVR screenshots folder in the OS file manager. + std::function open_screenshot_folder; + // Capture a stereo screenshot pair now (mirrors the Ctrl+F12 hotkey). The + // driver resolves the current app name and hands off to the renderer, which + // drains the request on the next composited frame. + std::function take_screenshot; + // Re-assert input focus on the connected game window. Called when the + // OSD menu closes — while the menu was open the VR window held focus, + // and the user expects keystrokes to land back in the game. + std::function request_game_focus; + // Kick off a download of the latest vrto3d_profiles.zip from GitHub + // releases and extract into the config folder. Reports progress via toast. + std::function download_latest_profiles; + // Push the Async Reprojection toggle to SteamVR for every known AppID so + // it takes effect without waiting for a profile reload or driver restart. + std::function set_async; + // Mirror the Auto Focus checkbox to the driver's live atomic so the + // presenter's focus loop sees the toggle on its next iteration instead + // of being stuck with the value cached at presenter Init(). + std::function set_auto_focus; + // Cross-process cursor controls (3DVision4All-style hide_cursor / + // confine_cursor). Mirrored to the driver's CursorControlThread atomics + // so toggles take effect on the next tick without a restart. + std::function set_hide_cursor; + std::function set_lock_cursor; + // Stereo cursor: per-eye software cursor drawn into the composited frame + // while the game is focused (hardware cursor suppressed). Mirrored to + // driver atomics like the hide/lock toggles above. + std::function set_stereo_cursor; + std::function set_cursor_depth; + std::function set_cursor_size; + + // Auto-depth feature: toggle + comfort-target slider (fraction of one + // eye's width). The Stereo tab binds these to read/write the live + // StereoDisplayComponent state. + std::function get_auto_depth_enabled; + std::function set_auto_depth_enabled; + std::function get_auto_depth_target; + std::function set_auto_depth_target; + std::function get_auto_depth_smoothing; + std::function set_auto_depth_smoothing; + std::function get_auto_depth_logging; + std::function set_auto_depth_logging; +}; + +// Renders the 5-tab configuration menu and the persistent chrome (title bar + +// footer). All ImGui calls happen between OsdRenderer's NewFrame/Render +// bracket — this class only contributes widget code. +class OsdMenu { +public: + OsdMenu(StereoDisplayComponent* component, MenuCallbacks callbacks); + ~OsdMenu(); + + void SetVisible(bool visible); + void Toggle(); + bool Visible() const; + + // Returns the current toast string set by SetText, or empty if expired. + // Owned + decayed by OsdRenderer; menu doesn't draw the toast itself. + + // Build the menu's ImGui widgets. Called once per frame between + // ImGui::NewFrame and ImGui::Render. + void BuildUI(OsdInput& input); + + // Provide the full version string for the title bar + System tab. + void SetVersion(std::string version); + + // Provide the current loaded-app name (`app_name_` from + // hmd_device_driver.cpp). Used for the footer's game-profile save button + // label and the Stereo tab's read-only profile readout. + void SetAppName(std::string app_name); + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace vrto3d::osd diff --git a/vrto3d/src/osd/osd_renderer.cpp b/vrto3d/src/osd/osd_renderer.cpp new file mode 100644 index 00000000..b246d4fe --- /dev/null +++ b/vrto3d/src/osd/osd_renderer.cpp @@ -0,0 +1,934 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#include "osd/osd_renderer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +#endif +#include + +#include "imgui.h" +#include "backends/imgui_impl_dx11.h" + +#include "osd/osd_input.h" +#include "osd/osd_menu.h" + +#include "hmd_device_driver.h" +#include "vrto3dlib/stereo_config.h" +#include "vrto3dlib/win32_helper.hpp" // GetSteamInstallPath + +#ifndef VRTO3D_LOG +# include +# define VRTO3D_LOG() (std::cerr) +#endif + +using Microsoft::WRL::ComPtr; +using namespace std::chrono; + +namespace vrto3d::osd { + +namespace { + +// Geometric-mean font scale based on the per-eye OSD area. Treats SbS and +// TaB layouts identically at the same source resolution (both have the same +// per-eye area), so users get consistent text size regardless of output +// mode. Baseline area = 720p SbS half (640x720 = 460800 px) so 1080p SbS +// still resolves to 1.5x, matching the previous eye_h/720 formula on that +// case while bumping TaB up from its old 0.75x floor. +float ComputeFontScale(UINT eye_w, UINT eye_h) { + constexpr float baseline_area = 640.0f * 720.0f; + const float area = static_cast(eye_w) * static_cast(eye_h); + if (area <= 0.0f) return 1.0f; + return std::clamp(std::sqrt(area / baseline_area), 0.75f, 3.0f); +} + +// Full-screen triangle VS — emits a single triangle that covers the viewport +// using vertex IDs 0/1/2. UVs are derived so that (0,0) lands at top-left. +const char* kCompositeVs = R"hlsl( +struct VsOut { + float4 pos : SV_Position; + float2 uv : TEXCOORD0; +}; + +VsOut main(uint id : SV_VertexID) { + VsOut o; + float2 uv = float2((id << 1) & 2, id & 2); + o.uv = uv; + o.pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); + return o; +} +)hlsl"; + +// Composite PS — samples the offscreen OSD texture and outputs straight RGBA. +// Blend state handles SrcAlpha/InvSrcAlpha so transparent UI regions pass +// through the underlying SbS pixels unchanged. +const char* kCompositePs = R"hlsl( +Texture2D osd_tex : register(t0); +SamplerState osd_smp : register(s0); + +struct VsOut { + float4 pos : SV_Position; + float2 uv : TEXCOORD0; +}; + +float4 main(VsOut i) : SV_Target { + return osd_tex.Sample(osd_smp, i.uv); +} +)hlsl"; + +// Stereo-cursor PS — procedural navigation arrow (tip/hotspot at hotspot_uv, +// body extending down-right), drawn once per eye half with a per-eye +// horizontal shift baked into hotspot_uv by the caller. Ported from +// UEVR-3D's Flat3D stereo cursor: solid silhouette + dark outline identical +// in both eyes (stable fusion at the chosen depth), two-facet shading with a +// crease highlight so it reads as a folded 3D arrow. Output is +// premultiplied alpha — pair with the ONE/INV_SRC_ALPHA blend state. +const char* kCursorPs = R"hlsl( +cbuffer CursorCb : register(b0) { + float2 hotspot_uv; // arrow tip in this eye's UV (depth shift included) + float2 eye_px; // per-eye dimensions in pixels + float size_px; // arrow height in pixels + float3 _pad; +}; + +struct VsOut { + float4 pos : SV_Position; + float2 uv : TEXCOORD0; +}; + +float cross2(float2 a, float2 b) { return a.x * b.y - a.y * b.x; } +bool in_tri(float2 p, float2 a, float2 b, float2 c) { + float c1 = cross2(b - a, p - a); + float c2 = cross2(c - b, p - b); + float c3 = cross2(a - c, p - c); + return (c1 <= 0.0 && c2 <= 0.0 && c3 <= 0.0) || (c1 >= 0.0 && c2 >= 0.0 && c3 >= 0.0); +} + +// Arrow points UP-LEFT like a normal cursor, split by a diagonal centre +// crease (the arrow axis). kAxis = tip->back, kPerp = across the crease. +static const float2 kAxis = float2(0.70711, 0.70711); +static const float2 kPerp = float2(-0.70711, 0.70711); +static const float2 kNavTip = float2(0.0, 0.0); +static const float2 kNavNotch = float2(0.495, 0.495); +static const float2 kNavWingL = float2(0.375, 0.969); +static const float2 kNavWingR = float2(0.969, 0.375); +bool nav_any(float2 p) { + return in_tri(p, kNavTip, kNavWingL, kNavNotch) || in_tri(p, kNavTip, kNavNotch, kNavWingR); +} + +float4 main(VsOut input) : SV_Target { + float2 d = input.uv - hotspot_uv; + float inv_size = 1.0 / max(size_px, 1.0); + float pxu = inv_size; // one output pixel in arrow units + float2 p = float2(d.x * eye_px.x, d.y * max(eye_px.y, 1.0)) * inv_size; + + // Solid silhouette + outline, 4x rotated-grid supersample for smooth edges. + const float2 J[4] = { float2(0.125, 0.375), float2(0.375, -0.125), + float2(-0.125, -0.375), float2(-0.375, 0.125) }; + float fill = 0.0, edge = 0.0; + [unroll] + for (int i = 0; i < 4; ++i) { + float2 s = p + J[i] * (pxu * 2.0); + if (nav_any(s)) fill += 0.25; + if (nav_any(s * (1.0 / 1.32))) edge += 0.25; // enlarged silhouette = fill+outline + } + float outline = saturate(edge - fill); + + // Two-facet shading + a crease highlight — a folded LOOK, drawn + // identically in both eyes (the 3D placement comes from hotspot_uv). + float side = dot(p, kPerp); // signed distance across the crease + float axial = dot(p, kAxis); // 0 at tip -> ~0.95 at wings + float crease = saturate(1.0 - abs(side) / 0.42) + * smoothstep(0.0, 0.18, axial) * (1.0 - smoothstep(0.62, 0.95, axial)); + + float t = saturate(abs(side) * 2.4); // 0 at crease -> 1 at wing + float vv = saturate(axial / 0.95); // 0 at tip -> 1 at base + float shade = ((side > 0.0) ? 0.98 : 0.80) - 0.10 * t - 0.12 * vv; + shade = saturate(shade + smoothstep(0.06, 0.0, abs(side)) * crease * 0.12); + + // Premultiplied over: outline (behind) then the facet fill. + float3 col = float3(0.03, 0.03, 0.03) * outline; + float a = outline; + col = float3(shade, shade, shade) * fill + col * (1.0 - fill); + a = fill + a * (1.0 - fill); + return float4(col, a); +} +)hlsl"; + +bool CompileShader(const char* src, const char* entry, const char* profile, + ComPtr& out_blob) { + ComPtr err; + HRESULT hr = D3DCompile(src, std::strlen(src), nullptr, nullptr, nullptr, + entry, profile, 0, 0, &out_blob, &err); + if (FAILED(hr)) { + VRTO3D_LOG() << "OsdRenderer: shader compile (" << profile << ") failed: " + << (err ? static_cast(err->GetBufferPointer()) : "(no log)") + << std::endl; + return false; + } + return true; +} + +} // namespace + +struct OsdRenderer::Impl { + // D3D11 handles (non-owning device/context — owned by Dx11Renderer). + ID3D11Device* device = nullptr; + ID3D11DeviceContext* context = nullptr; + + // Offscreen OSD render target (per-eye sized). + UINT eye_w = 0; + UINT eye_h = 0; + ComPtr osd_tex; + ComPtr osd_rtv; + ComPtr osd_srv; + + // Composite pipeline state — used to blend osd_tex into out_sbs. + ComPtr vs; + ComPtr ps; + ComPtr blend; + ComPtr raster; + ComPtr sampler; + + // Stereo-cursor pipeline (procedural arrow, premultiplied-alpha blend). + ComPtr cursor_ps; + ComPtr cursor_cb; + ComPtr blend_premul; + + // Stereo-cursor state pushed by the driver's CursorControlThread (any + // thread) and consumed by RenderFrame (window thread). + std::atomic cursor_active{false}; + std::atomic cursor_depth_px{0.0f}; + std::atomic cursor_size_px{32.0f}; + std::atomic cursor_game_hwnd{nullptr}; + + // ImGui state. + ImGuiContext* imgui_ctx = nullptr; + bool imgui_dx11_ready = false; + // Backing storage for io.IniFilename — ImGui only holds the const char*, + // so the std::string must outlive the ImGui context. + std::string ini_path; + + // Input pump + menu (created in Init). + std::unique_ptr input; + std::unique_ptr menu; + + // Toast text (replacement for old GDI+ overlay). + std::mutex toast_mu; + std::string toast_msg; + steady_clock::time_point toast_expiry; + + // Frame timing for ImGui's IO.DeltaTime. + steady_clock::time_point last_frame_time = steady_clock::now(); + + // For mapping mouse coords into client space. + void* headset_hwnd = nullptr; + StereoDisplayComponent* component = nullptr; + + // OutputMode active at the time the presenter (and thus this OsdRenderer) + // was constructed. The System-tab combo can mutate the live config's + // output_mode mid-session, but the real presenter pipeline doesn't switch + // until the driver restarts — so the cursor-fold layout has to stay + // pinned to whatever mode actually owns the SBS frame we're compositing + // into. Refreshed only on Shutdown/re-Init. + OutputMode active_output_mode = OutputMode::SbS; + + // WS_EX_LAYERED|WS_EX_TRANSPARENT toggling. The headset window is normally + // click-through (so the user can interact with apps behind it). When the + // menu opens we strip those bits so clicks land on the OSD; on close we + // restore whatever was there before. + bool prev_menu_visible = false; + bool styles_overridden = false; + LONG_PTR saved_ex_style = 0; + HWND styled_hwnd = nullptr; + std::function on_menu_closed; + + HWND DiscoverHwnd() { + if (headset_hwnd && IsWindow(static_cast(headset_hwnd))) + return static_cast(headset_hwnd); + if (cached_hwnd && IsWindow(cached_hwnd)) + return cached_hwnd; + cached_hwnd = FindWindowW(L"VRto3D_PresentWindow", nullptr); + return cached_hwnd; + } + HWND cached_hwnd = nullptr; + + void ApplyMenuVisibility(bool now_visible) { + if (now_visible == prev_menu_visible) return; + // NvidiaDX9: NV3D-Lib owns the FSE popup and its click-through state, + // and toggles it via InterfaceDX11::SetInteractive (driven by the + // presenter from this same menu-visibility signal). Manipulating + // WS_EX_TRANSPARENT here would be a cross-thread style change on the + // FSE D3D9Ex window (DWM-wedge risk) and wouldn't even stop click + // pass-through, since the library's WndProc HTTRANSPARENT path stays + // active. So leave the window alone in this mode. + if (active_output_mode == OutputMode::NvidiaDX9) { + prev_menu_visible = now_visible; + return; + } + HWND hwnd = DiscoverHwnd(); + if (!hwnd) { prev_menu_visible = now_visible; return; } + // Only toggle WS_EX_TRANSPARENT; WS_EX_LAYERED stays set for the + // window's lifetime to avoid DWM rebuilding the layered surface. + // SWP_FRAMECHANGED is intentionally omitted — it can trigger + // WM_DPICHANGED, which would rescale the window on non-100% displays. + if (now_visible && !styles_overridden) { + saved_ex_style = GetWindowLongPtrW(hwnd, GWL_EXSTYLE); + if (saved_ex_style & WS_EX_TRANSPARENT) { + SetWindowLongPtrW(hwnd, GWL_EXSTYLE, saved_ex_style & ~WS_EX_TRANSPARENT); + SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | + SWP_NOACTIVATE); + } + styles_overridden = true; + styled_hwnd = hwnd; + // Steal foreground so gamepad / keyboard / mouse routed by the + // OS go to our window instead of the game underneath. Skip the + // NvidiaDX9 presenter — it holds the D3D9Ex device in FSE on + // this same HWND, and a foreground change there drops the + // exclusive cooperative level (and risks a TDR). ForceFocus + // does ~100ms of sleeps + AttachThreadInput, so dispatch to a + // detached thread so we don't block presentation. + if (active_output_mode != OutputMode::NvidiaDX9) { + std::thread([hwnd]() { + ForceFocus(hwnd, + GetCurrentThreadId(), + GetWindowThreadProcessId(hwnd, nullptr)); + }).detach(); + } + } else if (!now_visible && styles_overridden) { + HWND target = styled_hwnd && IsWindow(styled_hwnd) ? styled_hwnd : hwnd; + if (target) { + SetWindowLongPtrW(target, GWL_EXSTYLE, saved_ex_style); + SetWindowPos(target, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | + SWP_NOACTIVATE); + } + styles_overridden = false; + styled_hwnd = nullptr; + // Menu just closed — hand input focus back to the game. + if (on_menu_closed) on_menu_closed(); + } + prev_menu_visible = now_visible; + } + + bool CreateOffscreen(UINT w, UINT h) { + if (osd_tex && eye_w == w && eye_h == h) return true; + osd_tex.Reset(); + osd_rtv.Reset(); + osd_srv.Reset(); + + D3D11_TEXTURE2D_DESC d{}; + d.Width = w; + d.Height = h; + d.MipLevels = 1; + d.ArraySize = 1; + d.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + d.SampleDesc.Count = 1; + d.Usage = D3D11_USAGE_DEFAULT; + d.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + if (FAILED(device->CreateTexture2D(&d, nullptr, &osd_tex))) return false; + if (FAILED(device->CreateRenderTargetView(osd_tex.Get(), nullptr, &osd_rtv))) return false; + if (FAILED(device->CreateShaderResourceView(osd_tex.Get(), nullptr, &osd_srv))) return false; + eye_w = w; + eye_h = h; + return true; + } + + bool CreatePipelineState() { + ComPtr vs_blob, ps_blob; + if (!CompileShader(kCompositeVs, "main", "vs_5_0", vs_blob)) return false; + if (!CompileShader(kCompositePs, "main", "ps_5_0", ps_blob)) return false; + if (FAILED(device->CreateVertexShader(vs_blob->GetBufferPointer(), vs_blob->GetBufferSize(), + nullptr, &vs))) return false; + if (FAILED(device->CreatePixelShader(ps_blob->GetBufferPointer(), ps_blob->GetBufferSize(), + nullptr, &ps))) return false; + + D3D11_BLEND_DESC bd{}; + bd.RenderTarget[0].BlendEnable = TRUE; + bd.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; + bd.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; + bd.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; + bd.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; + bd.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO; + bd.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + bd.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + if (FAILED(device->CreateBlendState(&bd, &blend))) return false; + + D3D11_RASTERIZER_DESC rd{}; + rd.FillMode = D3D11_FILL_SOLID; + rd.CullMode = D3D11_CULL_NONE; + rd.DepthClipEnable = FALSE; + if (FAILED(device->CreateRasterizerState(&rd, &raster))) return false; + + D3D11_SAMPLER_DESC sd{}; + sd.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + sd.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.MaxLOD = D3D11_FLOAT32_MAX; + if (FAILED(device->CreateSamplerState(&sd, &sampler))) return false; + + // Stereo-cursor pipeline. Failure is non-fatal — the OSD works + // without it; the cursor pass just stays disabled. + ComPtr cursor_blob; + if (CompileShader(kCursorPs, "main", "ps_5_0", cursor_blob)) { + device->CreatePixelShader(cursor_blob->GetBufferPointer(), + cursor_blob->GetBufferSize(), + nullptr, &cursor_ps); + } + D3D11_BUFFER_DESC cbd{}; + cbd.ByteWidth = 32; // float2 + float2 + float + float3 pad + cbd.Usage = D3D11_USAGE_DEFAULT; + cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + device->CreateBuffer(&cbd, nullptr, &cursor_cb); + + D3D11_BLEND_DESC pbd{}; + pbd.RenderTarget[0].BlendEnable = TRUE; + pbd.RenderTarget[0].SrcBlend = D3D11_BLEND_ONE; // premultiplied + pbd.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; + pbd.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; + pbd.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; + pbd.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA; + pbd.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + pbd.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + device->CreateBlendState(&pbd, &blend_premul); + return true; + } + + // Layout of the presenter's window surface — governs how the physical + // cursor position folds into per-eye coordinates. Must match the switch + // in RenderFrame's input block (both are pinned to active_output_mode). + StereoLayout SurfaceLayout() const { + switch (active_output_mode) { + case OutputMode::SbS: + case OutputMode::VirtualDesktop: + case OutputMode::DualDisplay: + case OutputMode::DualDisplayFlip: + return StereoLayout::HorizontalSbs; + case OutputMode::TaB: + case OutputMode::FramePacked720p60: + case OutputMode::FramePacked1080p24: + case OutputMode::FramePacked1080p60: + case OutputMode::FramePacked1080p60CVT: + return StereoLayout::VerticalTab; + default: + return StereoLayout::Mono; + } + } + + // Map the OS cursor position into per-eye UV. Preferred path: normalize + // against the GAME window's client rect — games place their software + // cursor / UI hit-testing from their own client coords, and their frame + // fills the whole per-eye image, so this tracks the in-game cursor 1:1 + // regardless of where/how large the game window is. Fallback (no game + // window): the presenter window with the same eye-fold as + // OsdInput::FeedImGui. Returns false when the cursor is outside (no + // arrow drawn). + bool MapCursorToEyeUV(float& out_u, float& out_v) { + POINT p; + if (!GetCursorPos(&p)) return false; + + HWND game = static_cast(cursor_game_hwnd.load(std::memory_order_relaxed)); + if (game && IsWindow(game)) { + RECT rc{}; + POINT cp = p; + if (GetClientRect(game, &rc) && rc.right > 0 && rc.bottom > 0 + && ScreenToClient(game, &cp)) { + const float u = static_cast(cp.x) / static_cast(rc.right); + const float v = static_cast(cp.y) / static_cast(rc.bottom); + if (u < 0.0f || u > 1.0f || v < 0.0f || v > 1.0f) return false; + out_u = u; + out_v = v; + return true; + } + } + + HWND hwnd = DiscoverHwnd(); + if (!hwnd) return false; + RECT client{}; + if (!GetClientRect(hwnd, &client)) return false; + ScreenToClient(hwnd, &p); + const int cw = client.right - client.left; + const int ch = client.bottom - client.top; + if (cw <= 0 || ch <= 0) return false; + float u = static_cast(p.x) / static_cast(cw); + float v = static_cast(p.y) / static_cast(ch); + if (u < 0.0f || u > 1.0f || v < 0.0f || v > 1.0f) return false; + switch (SurfaceLayout()) { + case StereoLayout::HorizontalSbs: + u = (u >= 0.5f) ? (u - 0.5f) * 2.0f : u * 2.0f; + break; + case StereoLayout::VerticalTab: + v = (v >= 0.5f) ? (v - 0.5f) * 2.0f : v * 2.0f; + break; + default: + break; + } + out_u = u; + out_v = v; + return true; + } + + // Draw the per-eye arrow into both halves of out_sbs at (u, v), with the + // depth shift applied symmetrically (+depth = into the screen). Sign was + // set empirically against VRto3D's eye layout — left half shifts right, + // right half shifts left for uncrossed (behind-screen) disparity. + void DrawCursorPass(ID3D11Texture2D* out_sbs, float u, float v) { + if (!cursor_ps || !cursor_cb || !blend_premul) return; + + ComPtr sbs_rtv; + if (FAILED(device->CreateRenderTargetView(out_sbs, nullptr, &sbs_rtv))) return; + + D3D11_TEXTURE2D_DESC sbs_desc{}; + out_sbs->GetDesc(&sbs_desc); + const UINT half_w = sbs_desc.Width / 2; + const UINT full_h = sbs_desc.Height; + if (half_w == 0 || full_h == 0) return; + + ID3D11RenderTargetView* rtv = sbs_rtv.Get(); + context->OMSetRenderTargets(1, &rtv, nullptr); + + const float blend_factor[4] = { 1, 1, 1, 1 }; + context->OMSetBlendState(blend_premul.Get(), blend_factor, 0xFFFFFFFF); + context->RSSetState(raster.Get()); + context->IASetInputLayout(nullptr); + context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + context->VSSetShader(vs.Get(), nullptr, 0); + context->PSSetShader(cursor_ps.Get(), nullptr, 0); + ID3D11Buffer* cbs[] = { cursor_cb.Get() }; + context->PSSetConstantBuffers(0, 1, cbs); + + const float depth_px = cursor_depth_px.load(std::memory_order_relaxed); + const float size_px = cursor_size_px.load(std::memory_order_relaxed); + + D3D11_VIEWPORT vp{}; + vp.Width = static_cast(half_w); + vp.Height = static_cast(full_h); + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + + struct CursorCb { + float hotspot_uv[2]; + float eye_px[2]; + float size_px; + float pad[3]; + } cb{}; + cb.eye_px[0] = static_cast(half_w); + cb.eye_px[1] = static_cast(full_h); + cb.size_px = (size_px > 1.0f) ? size_px : 1.0f; + + for (int eye = 0; eye < 2; ++eye) { + const float dir = (eye == 0) ? 1.0f : -1.0f; + cb.hotspot_uv[0] = u + dir * depth_px / static_cast(half_w); + cb.hotspot_uv[1] = v; + context->UpdateSubresource(cursor_cb.Get(), 0, nullptr, &cb, 0, 0); + + vp.TopLeftX = static_cast(eye) * static_cast(half_w); + vp.TopLeftY = 0.0f; + context->RSSetViewports(1, &vp); + context->Draw(3, 0); + } + + ID3D11RenderTargetView* null_rtv = nullptr; + context->OMSetRenderTargets(1, &null_rtv, nullptr); + } +}; + +OsdRenderer::OsdRenderer() : impl_(std::make_unique()) {} +OsdRenderer::~OsdRenderer() { Shutdown(); } + +bool OsdRenderer::Init(ID3D11Device* device, + ID3D11DeviceContext* context, + UINT eye_w, UINT eye_h, + void* headset_hwnd, + StereoDisplayComponent* component, + MenuCallbacks callbacks) { + auto& s = *impl_; + s.device = device; + s.context = context; + s.headset_hwnd = headset_hwnd; + s.component = component; + + if (!s.CreateOffscreen(eye_w, eye_h)) { + VRTO3D_LOG() << "OsdRenderer: failed to create offscreen RT" << std::endl; + return false; + } + if (!s.CreatePipelineState()) { + VRTO3D_LOG() << "OsdRenderer: failed to create composite pipeline" << std::endl; + return false; + } + + IMGUI_CHECKVERSION(); + s.imgui_ctx = ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + // Persist window pos/size, table column widths, etc. to the VRto3D + // config folder so the user's layout survives across sessions. The ini + // path string is held on the Impl so the pointer stays valid for the + // lifetime of the ImGui context. If Steam isn't installed (CI / debug + // shells) we fall through with IniFilename = nullptr — ImGui then runs + // ephemerally instead of writing to an unknown location. + io.IniFilename = nullptr; + io.LogFilename = nullptr; + if (const std::string steam = GetSteamInstallPath(); !steam.empty()) { + const std::string folder = steam + "\\config\\vrto3d"; + std::error_code ec; + std::filesystem::create_directories(folder, ec); + s.ini_path = folder + "\\imgui_osd.ini"; + io.IniFilename = s.ini_path.c_str(); + } + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + + // Scale fonts to the per-eye area so SbS and TaB read the same at the + // same source resolution; the formula clamps below 1.0 for very small + // surfaces and above 1.0 for high-res so the menu stays legible. + io.FontGlobalScale = ComputeFontScale(eye_w, eye_h); + io.DisplaySize = ImVec2(static_cast(eye_w), static_cast(eye_h)); + + if (!ImGui_ImplDX11_Init(device, context)) { + VRTO3D_LOG() << "OsdRenderer: ImGui_ImplDX11_Init failed" << std::endl; + ImGui::DestroyContext(s.imgui_ctx); + s.imgui_ctx = nullptr; + return false; + } + s.imgui_dx11_ready = true; + + s.input = std::make_unique(); + s.on_menu_closed = callbacks.request_game_focus; + s.menu = std::make_unique(component, std::move(callbacks)); + + // Pin the active output mode to whatever the presenter was constructed + // for. The System-tab combo can mutate the live config mid-session, but + // the cursor-mapping fold has to keep matching the actual SBS layout + // until the driver restarts. + if (component) { + s.active_output_mode = component->GetConfig().output_mode; + } + + return true; +} + +void OsdRenderer::OnResize(UINT eye_w, UINT eye_h) { + auto& s = *impl_; + if (!s.device) return; + if (s.eye_w == eye_w && s.eye_h == eye_h) return; + s.CreateOffscreen(eye_w, eye_h); + if (s.imgui_ctx) { + ImGui::SetCurrentContext(s.imgui_ctx); + ImGui::GetIO().DisplaySize = ImVec2(static_cast(eye_w), static_cast(eye_h)); + ImGui::GetIO().FontGlobalScale = ComputeFontScale(eye_w, eye_h); + } +} + +void OsdRenderer::SetHeadsetHwnd(void* hwnd) { + impl_->headset_hwnd = hwnd; + impl_->cached_hwnd = nullptr; // force re-discovery on next access +} + +void OsdRenderer::Shutdown() { + auto& s = *impl_; + s.ApplyMenuVisibility(false); // restore window styles before tearing down + s.menu.reset(); + s.input.reset(); + if (s.imgui_dx11_ready) { + ImGui_ImplDX11_Shutdown(); + s.imgui_dx11_ready = false; + } + if (s.imgui_ctx) { + // Force-flush any pending layout changes — ImGui's auto-save is + // throttled by IniSavingRate (5s by default) so a quick edit-then- + // exit could otherwise lose the user's window pos/size. + if (!s.ini_path.empty()) { + ImGui::SaveIniSettingsToDisk(s.ini_path.c_str()); + } + ImGui::DestroyContext(s.imgui_ctx); + s.imgui_ctx = nullptr; + } + s.osd_rtv.Reset(); + s.osd_srv.Reset(); + s.osd_tex.Reset(); + s.vs.Reset(); + s.ps.Reset(); + s.blend.Reset(); + s.raster.Reset(); + s.sampler.Reset(); + s.cursor_ps.Reset(); + s.cursor_cb.Reset(); + s.blend_premul.Reset(); + s.cursor_active.store(false); + s.device = nullptr; + s.context = nullptr; +} + +bool OsdRenderer::HasContent() const { + auto& s = *impl_; + if (s.menu && s.menu->Visible()) return true; + std::lock_guard lk(s.toast_mu); + return !s.toast_msg.empty() && steady_clock::now() < s.toast_expiry; +} + +bool OsdRenderer::MenuVisible() const { + auto& s = *impl_; + return s.menu && s.menu->Visible(); +} + +void OsdRenderer::ToggleMenu() { + auto& s = *impl_; + if (s.menu) s.menu->Toggle(); +} + +void OsdRenderer::SetText(const std::string& msg, milliseconds ttl) { + auto& s = *impl_; + std::lock_guard lk(s.toast_mu); + s.toast_msg = msg; + s.toast_expiry = steady_clock::now() + ttl; +} + +void OsdRenderer::SetAppName(const std::string& app_name) { + auto& s = *impl_; + if (s.menu) s.menu->SetAppName(app_name); +} + +void OsdRenderer::SetVersion(const std::string& version) { + auto& s = *impl_; + if (s.menu) s.menu->SetVersion(version); +} + +void OsdRenderer::RenderFrame(ID3D11Texture2D* out_sbs) { + auto& s = *impl_; + if (!s.imgui_ctx || !out_sbs || !s.osd_tex) return; + + // Toggle the headset window's WS_EX_LAYERED|WS_EX_TRANSPARENT bits in + // sync with menu visibility so clicks reach the OSD when it's open. + s.ApplyMenuVisibility(MenuVisible()); + + // Gate the global LL mouse hook on menu visibility (or active capture). + // Keeping it always-on routes every system-wide mouse event through this + // process, which adds latency / cursor stutter when our pump thread is + // busy. Clicks/wheel are only consumed by the OSD when it's actually + // visible, so the hook is unnecessary the rest of the time. + bool has_content = HasContent(); + if (s.input) { + s.input->SetMouseHookActive(MenuVisible() || s.input->IsCapturing()); + } + + // Stereo cursor: the driver pushes `cursor_active` while the feature is + // on and the game (or our menu) owns the foreground — the hardware + // cursor is hidden then, so we're the only visible cursor. With the menu + // open the ImGui software cursor takes over (correctly folded per-eye); + // otherwise draw the arrow pass directly into the SbS frame. + const bool stereo_cursor = s.cursor_active.load(std::memory_order_relaxed); + float cursor_u = 0.0f, cursor_v = 0.0f; + const bool cursor_draw = stereo_cursor && !MenuVisible() + && s.MapCursorToEyeUV(cursor_u, cursor_v); + + // Cheap early-out if there's nothing to draw and no toast pending. + if (!has_content && (!s.input || !s.input->IsCapturing())) { + // Still pump input edges so a future Ctrl+Home press is detected. + if (s.input) s.input->Poll(); + if (cursor_draw) s.DrawCursorPass(out_sbs, cursor_u, cursor_v); + return; + } + + // ----- Set up ImGui frame ----- + ImGui::SetCurrentContext(s.imgui_ctx); + ImGuiIO& io = ImGui::GetIO(); + + auto now = steady_clock::now(); + float dt = duration_cast>(now - s.last_frame_time).count(); + if (dt <= 0.0f || dt > 0.25f) dt = 1.0f / 60.0f; + io.DeltaTime = dt; + s.last_frame_time = now; + + if (s.input) { + s.input->Poll(); + OsdSurface surface; + surface.eye_w = static_cast(s.eye_w); + surface.eye_h = static_cast(s.eye_h); + surface.hwnd = s.headset_hwnd; + // Use the snapshot taken at Init — NOT the live config — so a + // System-tab output_mode change doesn't break cursor mapping until + // the driver restarts and the new presenter takes over. + surface.layout = s.SurfaceLayout(); + s.input->FeedImGui(io, surface); + } + + // With the stereo cursor active and the menu open, the hardware cursor + // is hidden — let ImGui draw its software cursor at the folded per-eye + // position so the pointer lines up with the widgets in both eyes. + io.MouseDrawCursor = stereo_cursor && MenuVisible(); + + ImGui_ImplDX11_NewFrame(); + ImGui::NewFrame(); + + // Menu UI. + if (s.menu && s.menu->Visible() && s.input) { + s.menu->BuildUI(*s.input); + } + + // Toast widget — bottom-left, no decoration, bright green to match the + // old GDI+ overlay's look. + { + std::string msg; + bool active = false; + { + std::lock_guard lk(s.toast_mu); + if (!s.toast_msg.empty() && steady_clock::now() < s.toast_expiry) { + msg = s.toast_msg; + active = true; + } + } + if (active) { + const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_NoBackground; + const float pad_x = 50.0f; + const float pad_y = 30.0f; + // Toast text is 2x the menu font so it's legible at headset + // distance even when the menu chrome is hidden. + const float toast_scale = 2.0f; + // Bound text wrap to the eye width so long messages wrap instead + // of running past the right edge. Pass the wrap position as an + // explicit window-local pixel value rather than 0.0f — with + // AlwaysAutoResize, a wrap-at-content-right behaves circularly + // (window width depends on content, content wrap depends on + // window width) and collapses to one glyph per line on first + // frame, producing a vertical column of characters. + const float wrap_local = (std::max)(64.0f, + static_cast(s.eye_w) - 2.0f * pad_x - + 2.0f * ImGui::GetStyle().WindowPadding.x); + // Anchor by bottom-left pivot so wrapped multi-line toasts grow + // upward into the screen rather than falling off the bottom. + ImGui::SetNextWindowPos( + ImVec2(pad_x, static_cast(s.eye_h) - pad_y), + ImGuiCond_Always, + ImVec2(0.0f, 1.0f)); + if (ImGui::Begin("##vrto3d_toast", nullptr, flags)) { + ImGui::SetWindowFontScale(toast_scale); + ImGui::PushTextWrapPos(wrap_local); + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 255, 0, 255)); + ImGui::TextUnformatted(msg.c_str()); + ImGui::PopStyleColor(); + ImGui::PopTextWrapPos(); + } + ImGui::End(); + } + } + + ImGui::Render(); + + // ----- Pass 1: render ImGui draw lists into osd_tex ----- + { + ID3D11RenderTargetView* rtv = s.osd_rtv.Get(); + const float clear[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; // transparent + s.context->OMSetRenderTargets(1, &rtv, nullptr); + s.context->ClearRenderTargetView(rtv, clear); + + D3D11_VIEWPORT vp{}; + vp.Width = static_cast(s.eye_w); + vp.Height = static_cast(s.eye_h); + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + s.context->RSSetViewports(1, &vp); + + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); + } + + // ----- Pass 2: composite osd_tex into both halves of out_sbs ----- + { + ComPtr sbs_rtv; + if (FAILED(s.device->CreateRenderTargetView(out_sbs, nullptr, &sbs_rtv))) { + return; + } + + D3D11_TEXTURE2D_DESC sbs_desc{}; + out_sbs->GetDesc(&sbs_desc); + const UINT half_w = sbs_desc.Width / 2; + const UINT full_h = sbs_desc.Height; + + ID3D11RenderTargetView* rtv = sbs_rtv.Get(); + s.context->OMSetRenderTargets(1, &rtv, nullptr); + + const float blend_factor[4] = { 1, 1, 1, 1 }; + s.context->OMSetBlendState(s.blend.Get(), blend_factor, 0xFFFFFFFF); + s.context->RSSetState(s.raster.Get()); + s.context->IASetInputLayout(nullptr); + s.context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + s.context->VSSetShader(s.vs.Get(), nullptr, 0); + s.context->PSSetShader(s.ps.Get(), nullptr, 0); + ID3D11SamplerState* samps[] = { s.sampler.Get() }; + s.context->PSSetSamplers(0, 1, samps); + ID3D11ShaderResourceView* srvs[] = { s.osd_srv.Get() }; + s.context->PSSetShaderResources(0, 1, srvs); + + D3D11_VIEWPORT vp{}; + vp.Width = static_cast(half_w); + vp.Height = static_cast(full_h); + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + + // Left eye + vp.TopLeftX = 0.0f; + vp.TopLeftY = 0.0f; + s.context->RSSetViewports(1, &vp); + s.context->Draw(3, 0); + + // Right eye + vp.TopLeftX = static_cast(half_w); + s.context->RSSetViewports(1, &vp); + s.context->Draw(3, 0); + + // Unbind SRV so the next frame's CopyResource on out_sbs (which the + // SRV points at indirectly) doesn't trigger a debug-layer warning. + ID3D11ShaderResourceView* null_srv[] = { nullptr }; + s.context->PSSetShaderResources(0, 1, null_srv); + ID3D11RenderTargetView* null_rtv = nullptr; + s.context->OMSetRenderTargets(1, &null_rtv, nullptr); + } + + // ----- Pass 3: stereo cursor arrow (topmost, over the OSD/toast) ----- + if (cursor_draw) { + s.DrawCursorPass(out_sbs, cursor_u, cursor_v); + } +} + +void OsdRenderer::SetStereoCursor(bool active, float depth_px, float size_px, void* game_hwnd) { + auto& s = *impl_; + s.cursor_active.store(active, std::memory_order_relaxed); + s.cursor_depth_px.store(depth_px, std::memory_order_relaxed); + s.cursor_size_px.store(size_px, std::memory_order_relaxed); + s.cursor_game_hwnd.store(game_hwnd, std::memory_order_relaxed); +} + +} // namespace vrto3d::osd diff --git a/vrto3d/src/osd/osd_renderer.h b/vrto3d/src/osd/osd_renderer.h new file mode 100644 index 00000000..af9c485f --- /dev/null +++ b/vrto3d/src/osd/osd_renderer.h @@ -0,0 +1,147 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +namespace vrto3d::vk { struct DeviceCtx; } +#endif + +class StereoDisplayComponent; + +namespace vrto3d::osd { + +class OsdMenu; +class OsdInput; +struct MenuCallbacks; + +// OsdRenderer owns the ImGui context, an offscreen RGBA render target sized +// per-eye, and a two-pass composite shader that overlays the OSD into both +// halves of the SbS frame produced by the SteamVR compositor. +// +// Lifetime: created lazily by Dx11Renderer once it knows the per-eye +// dimensions; destroyed when Dx11Renderer is shut down. +// +// Thread model: all methods are called from the window thread (the same +// thread that owns the D3D11 immediate context). +class OsdRenderer { +public: + OsdRenderer(); + ~OsdRenderer(); + +#ifdef _WIN32 + // Initialize ImGui + composite resources. `eye_w`/`eye_h` are the per-eye + // dimensions of the SbS frame (i.e. width = full_sbs_width / 2). + // `headset_hwnd` is used for mouse coordinate mapping. + bool Init(ID3D11Device* device, + ID3D11DeviceContext* context, + UINT eye_w, UINT eye_h, + void* headset_hwnd, + StereoDisplayComponent* component, + MenuCallbacks callbacks); + + // Re-create the offscreen RT if the per-eye dimensions changed (e.g. + // user switched output_mode and the SbS frame size moved). Cheap no-op + // when the dims match the current target. + void OnResize(UINT eye_w, UINT eye_h); +#else + // Vulkan/Linux variant. `sbs_format` is out_sbs_'s VkFormat (the OSD + // renders straight into out_sbs_ via a LOAD render pass — no offscreen + // RT + composite pass like the D3D11 path needs). + bool Init(vrto3d::vk::DeviceCtx* ctx, + VkFormat sbs_format, + uint32_t eye_w, uint32_t eye_h, + void* native_window, + StereoDisplayComponent* component, + MenuCallbacks callbacks); + + void OnResize(uint32_t eye_w, uint32_t eye_h); +#endif + + void Shutdown(); + + // Update the headset_hwnd used for mouse coordinate mapping. Used by + // presenters whose display surface is owned by an external process and + // only becomes available some time after Init() ran. + void SetHeadsetHwnd(void* hwnd); + + // Returns true if the menu OR a live toast is currently visible. + // Dx11Renderer can use this to skip the composite pass when there's + // nothing to draw. + bool HasContent() const; + + // True when the menu is open. Used by hmd_device_driver to gate the + // hotkey poll loop (so number keys / arrows reach ImGui instead of + // adjusting depth/conv). + bool MenuVisible() const; + void ToggleMenu(); + + // Replaces the old GDI+ setOverlay lambda. `ttl` defaults to 3 seconds. + void SetText(const std::string& msg, + std::chrono::milliseconds ttl = std::chrono::milliseconds(3000)); + + // Per-frame entry point. Called by Dx11Renderer right after the SbS + // CopyResource and before handing the frame to the presenter. + // + // Performs: + // 1. input poll + ImGui::NewFrame + // 2. menu BuildUI() + // 3. toast text widget + // 4. ImGui::Render → render draw lists into offscreen osd_tex_ + // 5. Two-pass blend onto out_sbs (left half + right half) +#ifdef _WIN32 + void RenderFrame(ID3D11Texture2D* out_sbs); +#else + // Vulkan variant: records the ImGui draw data twice (left/right eye + // viewports) into out_sbs via a loadOp=LOAD render pass, inside the + // caller's command buffer. `out_sbs` must be in COLOR_ATTACHMENT_OPTIMAL. + void RenderFrame(VkCommandBuffer cmd, VkImage out_sbs, VkImageView out_sbs_view, + uint32_t sbs_w, uint32_t sbs_h); +#endif + + // Plumbing for the menu's app-name display + version strings. + void SetAppName(const std::string& app_name); + void SetVersion(const std::string& version); + + // Stereo cursor state, pushed by the driver's CursorControlThread each + // tick (thread-safe — atomics). While `active`, RenderFrame draws a + // per-eye arrow at the OS cursor position into both halves of the SbS + // frame (menu closed), or switches to ImGui's software cursor (menu + // open). The hardware cursor is hidden by the driver while active, so + // this is the only cursor the user sees. `depth_px` is the per-eye + // horizontal shift (positive = into the screen); `size_px` the arrow + // height in per-eye pixels. `game_hwnd` is the game's foreground window + // (or null): games place their software cursor from THEIR window's + // client coords, so the arrow is normalized against that rect to track + // 1:1 — the presenter-window fold is only a fallback. No-op on the + // Vulkan/Linux path. + void SetStereoCursor(bool active, float depth_px, float size_px, void* game_hwnd); + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace vrto3d::osd diff --git a/vrto3d/src/osd/osd_renderer_vk.cpp b/vrto3d/src/osd/osd_renderer_vk.cpp new file mode 100644 index 00000000..ba84d365 --- /dev/null +++ b/vrto3d/src/osd/osd_renderer_vk.cpp @@ -0,0 +1,614 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#ifndef _WIN32 + +// Linux/Vulkan implementation of OsdRenderer. Mirrors the D3D11 version's +// structure (osd_renderer.cpp) with one architectural difference: the D3D11 +// path renders ImGui into an offscreen per-eye texture and then runs a +// composite blend pass twice (left/right viewport); here the OSD renders +// straight into out_sbs via a loadOp=LOAD render pass, and the left/right +// duplication is achieved by recording ImGui's draw data twice with a +// temporarily adjusted ImDrawData::DisplayPos/DisplaySize — the vendored +// imgui_impl_vulkan backend derives both its viewport and its projection +// push-constants from those fields, so offsetting DisplayPos by -half_w +// lands the identical draw list in the right half (ImGui's own blend state +// already handles the SrcAlpha/InvSrcAlpha pass-through). + +#include "osd/osd_renderer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "imgui.h" +#include "backends/imgui_impl_vulkan.h" + +#include "osd/osd_input.h" +#include "osd/osd_menu.h" + +#include "vk/vk_context.h" +#include "vrto3dlib/debug_log.hpp" // LOG() + GetSteamInstallPath() + +using namespace std::chrono; + +namespace vrto3d::osd { + +namespace { + +// Geometric-mean font scale based on the per-eye OSD area. Same formula as +// the D3D11 implementation so text reads identically across platforms. +// Baseline area = 720p SbS half (640x720 = 460800 px). +float ComputeFontScale(uint32_t eye_w, uint32_t eye_h) { + constexpr float baseline_area = 640.0f * 720.0f; + const float area = static_cast(eye_w) * static_cast(eye_h); + if (area <= 0.0f) return 1.0f; + return std::clamp(std::sqrt(area / baseline_area), 0.75f, 3.0f); +} + +// Routed into the imgui vulkan backend via InitInfo::CheckVkResultFn. +void CheckVkResult(VkResult err) { + if (err != VK_SUCCESS) { + LOG() << "OsdRenderer(vk): imgui backend VkResult " << static_cast(err); + } +} + +} // namespace + +struct OsdRenderer::Impl { + // Vulkan handles (non-owning ctx — owned by VkRenderer). + vrto3d::vk::DeviceCtx* ctx = nullptr; + VkFormat sbs_format = VK_FORMAT_UNDEFINED; + + // Per-eye dimensions (== half of the SbS frame width). + uint32_t eye_w = 0; + uint32_t eye_h = 0; + + // loadOp=LOAD pass rendering straight into out_sbs. + VkRenderPass osd_pass = VK_NULL_HANDLE; + VkDescriptorPool desc_pool = VK_NULL_HANDLE; + + // Single-entry framebuffer cache keyed by (view, w, h). out_sbs is + // recreated on resize, so the cached framebuffer is rebuilt whenever the + // caller hands us a different view or extent (the caller idles the + // device around out_sbs recreation, so destroying the old framebuffer + // here is safe). + VkFramebuffer fb = VK_NULL_HANDLE; + VkImageView fb_view = VK_NULL_HANDLE; + uint32_t fb_w = 0; + uint32_t fb_h = 0; + + // ImGui state. + ImGuiContext* imgui_ctx = nullptr; + bool imgui_vk_ready = false; + // Backing storage for io.IniFilename — ImGui only holds the const char*, + // so the std::string must outlive the ImGui context. + std::string ini_path; + + // Input pump + menu (created in Init). + std::unique_ptr input; + std::unique_ptr menu; + + // Toast text (replacement for old GDI+ overlay). + std::mutex toast_mu; + std::string toast_msg; + steady_clock::time_point toast_expiry; + + // Frame timing for ImGui's IO.DeltaTime. + steady_clock::time_point last_frame_time = steady_clock::now(); + + // Kept for signature parity with the Windows path; the Linux input pump + // is virtual-cursor based and never dereferences it. + void* native_window = nullptr; + StereoDisplayComponent* component = nullptr; + + // Menu open/close edge tracking. Linux has no WS_EX_TRANSPARENT click- + // through toggling (evdev input is global), but the close edge still + // hands focus back to the game like the Windows version does. + bool prev_menu_visible = false; + std::function on_menu_closed; + + void ApplyMenuVisibility(bool now_visible) { + if (now_visible == prev_menu_visible) return; + if (!now_visible && on_menu_closed) on_menu_closed(); + prev_menu_visible = now_visible; + } + + bool CreateRenderPass() { + VkAttachmentDescription att{}; + att.format = sbs_format; + att.samples = VK_SAMPLE_COUNT_1_BIT; + att.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + att.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + att.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + att.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + att.initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + att.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkAttachmentReference color_ref{}; + color_ref.attachment = 0; + color_ref.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkSubpassDescription sub{}; + sub.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + sub.colorAttachmentCount = 1; + sub.pColorAttachments = &color_ref; + + // in: order the loadOp against the caller's preceding SbS writes; + // out: order our overlay writes against the repack/present sampling. + VkSubpassDependency deps[2]{}; + deps[0].srcSubpass = VK_SUBPASS_EXTERNAL; + deps[0].dstSubpass = 0; + deps[0].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + deps[0].dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + deps[0].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + deps[0].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + deps[1].srcSubpass = 0; + deps[1].dstSubpass = VK_SUBPASS_EXTERNAL; + deps[1].srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + deps[1].dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | + VK_PIPELINE_STAGE_TRANSFER_BIT; + deps[1].srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + deps[1].dstAccessMask = VK_ACCESS_SHADER_READ_BIT | + VK_ACCESS_TRANSFER_READ_BIT; + + VkRenderPassCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + ci.attachmentCount = 1; + ci.pAttachments = &att; + ci.subpassCount = 1; + ci.pSubpasses = ⊂ + ci.dependencyCount = 2; + ci.pDependencies = deps; + return vrto3d::vk::LogIfFailed( + vkCreateRenderPass(ctx->device, &ci, nullptr, &osd_pass), + "OsdRenderer(vk): vkCreateRenderPass") == VK_SUCCESS; + } + + bool CreateDescriptorPool() { + // The backend needs one combined-image-sampler set for the font + // atlas (see IMGUI_IMPL_VULKAN_MINIMUM_IMAGE_SAMPLER_POOL_SIZE); + // leave headroom for a few AddTexture calls. + VkDescriptorPoolSize pool_size{}; + pool_size.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + pool_size.descriptorCount = 8; + + VkDescriptorPoolCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + ci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + ci.maxSets = 8; + ci.poolSizeCount = 1; + ci.pPoolSizes = &pool_size; + return vrto3d::vk::LogIfFailed( + vkCreateDescriptorPool(ctx->device, &ci, nullptr, &desc_pool), + "OsdRenderer(vk): vkCreateDescriptorPool") == VK_SUCCESS; + } + + VkFramebuffer GetFramebuffer(VkImageView view, uint32_t w, uint32_t h) { + if (fb != VK_NULL_HANDLE && fb_view == view && fb_w == w && fb_h == h) { + return fb; + } + DestroyFramebuffer(); + + VkFramebufferCreateInfo ci{}; + ci.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + ci.renderPass = osd_pass; + ci.attachmentCount = 1; + ci.pAttachments = &view; + ci.width = w; + ci.height = h; + ci.layers = 1; + if (vrto3d::vk::LogIfFailed( + vkCreateFramebuffer(ctx->device, &ci, nullptr, &fb), + "OsdRenderer(vk): vkCreateFramebuffer") != VK_SUCCESS) { + fb = VK_NULL_HANDLE; + return VK_NULL_HANDLE; + } + fb_view = view; + fb_w = w; + fb_h = h; + return fb; + } + + void DestroyFramebuffer() { + if (fb != VK_NULL_HANDLE) { + vkDestroyFramebuffer(ctx->device, fb, nullptr); + fb = VK_NULL_HANDLE; + } + fb_view = VK_NULL_HANDLE; + fb_w = fb_h = 0; + } +}; + +OsdRenderer::OsdRenderer() : impl_(std::make_unique()) {} +OsdRenderer::~OsdRenderer() { Shutdown(); } + +bool OsdRenderer::Init(vrto3d::vk::DeviceCtx* ctx, + VkFormat sbs_format, + uint32_t eye_w, uint32_t eye_h, + void* native_window, + StereoDisplayComponent* component, + MenuCallbacks callbacks) { + auto& s = *impl_; + if (!ctx || ctx->device == VK_NULL_HANDLE) return false; + s.ctx = ctx; + s.sbs_format = sbs_format; + s.eye_w = eye_w; + s.eye_h = eye_h; + s.native_window = native_window; + s.component = component; + + if (!s.CreateRenderPass()) { + LOG() << "OsdRenderer(vk): failed to create OSD render pass"; + return false; + } + if (!s.CreateDescriptorPool()) { + LOG() << "OsdRenderer(vk): failed to create descriptor pool"; + Shutdown(); + return false; + } + + IMGUI_CHECKVERSION(); + s.imgui_ctx = ImGui::CreateContext(); + ImGui::SetCurrentContext(s.imgui_ctx); + ImGuiIO& io = ImGui::GetIO(); + // Persist window pos/size, table column widths, etc. to the VRto3D + // config folder so the user's layout survives across sessions. The ini + // path string is held on the Impl so the pointer stays valid for the + // lifetime of the ImGui context. If Steam can't be located (CI / debug + // shells) we fall through with IniFilename = nullptr — ImGui then runs + // ephemerally instead of writing to an unknown location. + io.IniFilename = nullptr; + io.LogFilename = nullptr; + if (const std::string steam = GetSteamInstallPath(); !steam.empty()) { + const std::string folder = steam + "/config/vrto3d"; + std::error_code ec; + std::filesystem::create_directories(folder, ec); + s.ini_path = folder + "/imgui_osd.ini"; + io.IniFilename = s.ini_path.c_str(); + } + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + + // Scale fonts to the per-eye area so SbS and TaB read the same at the + // same source resolution; the formula clamps below 1.0 for very small + // surfaces and above 1.0 for high-res so the menu stays legible. + io.FontGlobalScale = ComputeFontScale(eye_w, eye_h); + io.DisplaySize = ImVec2(static_cast(eye_w), static_cast(eye_h)); + + ImGui_ImplVulkan_InitInfo info{}; + info.ApiVersion = VK_API_VERSION_1_1; + info.Instance = ctx->instance; + info.PhysicalDevice = ctx->phys; + info.Device = ctx->device; + info.QueueFamily = ctx->queue_family; + info.Queue = ctx->queue; + info.DescriptorPool = s.desc_pool; + info.RenderPass = s.osd_pass; + info.MinImageCount = 2; + // RenderFrame calls ImGui_ImplVulkan_RenderDrawData twice per frame + // (left + right eye), and each call advances the backend's internal + // vertex/index ring by one slot. VkRenderer keeps 2 frames in flight, so + // 2 calls x 2 frames = 4 slots are needed before a buffer can be safely + // reused. + info.ImageCount = 4; + info.MSAASamples = VK_SAMPLE_COUNT_1_BIT; + info.CheckVkResultFn = &CheckVkResult; + if (!ImGui_ImplVulkan_Init(&info)) { + LOG() << "OsdRenderer(vk): ImGui_ImplVulkan_Init failed"; + Shutdown(); + return false; + } + s.imgui_vk_ready = true; + + // Upload the font atlas now rather than letting ImGui_ImplVulkan_NewFrame + // do it lazily mid-frame: this vendored backend's CreateFontsTexture + // allocates its own one-shot command buffer and vkQueueSubmit+WaitIdle's + // it internally, so the shared-queue mutex must be held around the call. + { + std::lock_guard qlk(ctx->queue_mutex); + if (!ImGui_ImplVulkan_CreateFontsTexture()) { + LOG() << "OsdRenderer(vk): ImGui_ImplVulkan_CreateFontsTexture failed"; + Shutdown(); + return false; + } + } + + s.input = std::make_unique(); + s.on_menu_closed = callbacks.request_game_focus; + s.menu = std::make_unique(component, std::move(callbacks)); + + return true; +} + +void OsdRenderer::OnResize(uint32_t eye_w, uint32_t eye_h) { + auto& s = *impl_; + if (!s.ctx) return; + if (s.eye_w == eye_w && s.eye_h == eye_h) return; + s.eye_w = eye_w; + s.eye_h = eye_h; + // No offscreen RT to rebuild here — the framebuffer cache keys on the + // (view, w, h) the caller passes to RenderFrame and refreshes lazily. + if (s.imgui_ctx) { + ImGui::SetCurrentContext(s.imgui_ctx); + ImGui::GetIO().DisplaySize = ImVec2(static_cast(eye_w), static_cast(eye_h)); + ImGui::GetIO().FontGlobalScale = ComputeFontScale(eye_w, eye_h); + } +} + +void OsdRenderer::SetHeadsetHwnd(void* hwnd) { + // Kept for cross-platform call-site parity; the Linux mouse pump is + // virtual-cursor based and never consults a native window handle. + impl_->native_window = hwnd; +} + +void OsdRenderer::Shutdown() { + auto& s = *impl_; + s.ApplyMenuVisibility(false); // fire the menu-closed callback if open + s.menu.reset(); + s.input.reset(); + if (s.ctx && s.ctx->device != VK_NULL_HANDLE) { + // The caller doesn't guarantee a vkDeviceWaitIdle before tearing us + // down — drain our queue (under the shared mutex) so no in-flight + // command buffer still references the backend's buffers/framebuffer. + if (s.ctx->queue != VK_NULL_HANDLE) { + std::lock_guard qlk(s.ctx->queue_mutex); + vkQueueWaitIdle(s.ctx->queue); + } + if (s.imgui_vk_ready) { + ImGui::SetCurrentContext(s.imgui_ctx); + ImGui_ImplVulkan_Shutdown(); + s.imgui_vk_ready = false; + } + s.DestroyFramebuffer(); + if (s.desc_pool != VK_NULL_HANDLE) { + vkDestroyDescriptorPool(s.ctx->device, s.desc_pool, nullptr); + s.desc_pool = VK_NULL_HANDLE; + } + if (s.osd_pass != VK_NULL_HANDLE) { + vkDestroyRenderPass(s.ctx->device, s.osd_pass, nullptr); + s.osd_pass = VK_NULL_HANDLE; + } + } + if (s.imgui_ctx) { + ImGui::SetCurrentContext(s.imgui_ctx); + // Force-flush any pending layout changes — ImGui's auto-save is + // throttled by IniSavingRate (5s by default) so a quick edit-then- + // exit could otherwise lose the user's window pos/size. + if (!s.ini_path.empty()) { + ImGui::SaveIniSettingsToDisk(s.ini_path.c_str()); + } + ImGui::DestroyContext(s.imgui_ctx); + s.imgui_ctx = nullptr; + } + s.ctx = nullptr; +} + +bool OsdRenderer::HasContent() const { + auto& s = *impl_; + if (s.menu && s.menu->Visible()) return true; + std::lock_guard lk(s.toast_mu); + return !s.toast_msg.empty() && steady_clock::now() < s.toast_expiry; +} + +bool OsdRenderer::MenuVisible() const { + auto& s = *impl_; + return s.menu && s.menu->Visible(); +} + +void OsdRenderer::ToggleMenu() { + auto& s = *impl_; + if (s.menu) s.menu->Toggle(); +} + +void OsdRenderer::SetText(const std::string& msg, milliseconds ttl) { + auto& s = *impl_; + std::lock_guard lk(s.toast_mu); + s.toast_msg = msg; + s.toast_expiry = steady_clock::now() + ttl; +} + +void OsdRenderer::SetAppName(const std::string& app_name) { + auto& s = *impl_; + if (s.menu) s.menu->SetAppName(app_name); +} + +void OsdRenderer::SetVersion(const std::string& version) { + auto& s = *impl_; + if (s.menu) s.menu->SetVersion(version); +} + +void OsdRenderer::SetStereoCursor(bool, float, float, void*) { + // Cursor control isn't implementable from an external process on + // Wayland (see CursorControlThread) — the stereo cursor rides the same + // feature gate, so this is a Windows-only path. +} + +void OsdRenderer::RenderFrame(VkCommandBuffer cmd, VkImage out_sbs, VkImageView out_sbs_view, + uint32_t sbs_w, uint32_t sbs_h) { + auto& s = *impl_; + if (!s.imgui_ctx || !s.imgui_vk_ready || cmd == VK_NULL_HANDLE || + out_sbs == VK_NULL_HANDLE || out_sbs_view == VK_NULL_HANDLE || + sbs_w == 0 || sbs_h == 0) { + return; + } + + // Fire the game-refocus callback on the menu's close edge (the Windows + // version additionally toggles window styles here; evdev input is global + // so there is no click-through state to manage on Linux). + s.ApplyMenuVisibility(MenuVisible()); + + // API parity with the Windows LL-hook gating; a no-op for evdev. + if (s.input) { + s.input->SetMouseHookActive(MenuVisible() || s.input->IsCapturing()); + } + + // Cheap early-out if there's nothing to draw and no toast pending — + // returns without touching the command buffer. + const bool has_content = HasContent(); + if (!has_content && (!s.input || !s.input->IsCapturing())) { + // Still pump input edges so a future Ctrl+Home press is detected. + if (s.input) s.input->Poll(); + return; + } + + // ----- Set up ImGui frame ----- + ImGui::SetCurrentContext(s.imgui_ctx); + ImGuiIO& io = ImGui::GetIO(); + + auto now = steady_clock::now(); + float dt = duration_cast>(now - s.last_frame_time).count(); + if (dt <= 0.0f || dt > 0.25f) dt = 1.0f / 60.0f; + io.DeltaTime = dt; + s.last_frame_time = now; + + if (s.input) { + s.input->Poll(); + OsdSurface surface; + surface.eye_w = static_cast(s.eye_w); + surface.eye_h = static_cast(s.eye_h); + surface.hwnd = s.native_window; + // The evdev virtual cursor already lives in per-eye space, so no + // SbS/TaB cursor folding is needed (unlike the Win32 window-rect + // mapping) — the layout field is ignored by the Linux OsdInput. + surface.layout = StereoLayout::Mono; + s.input->FeedImGui(io, surface); + } + + ImGui_ImplVulkan_NewFrame(); + ImGui::NewFrame(); + + // Menu UI. + if (s.menu && s.menu->Visible() && s.input) { + s.menu->BuildUI(*s.input); + } + + // Toast widget — bottom-left, no decoration, bright green to match the + // old GDI+ overlay's look. + { + std::string msg; + bool active = false; + { + std::lock_guard lk(s.toast_mu); + if (!s.toast_msg.empty() && steady_clock::now() < s.toast_expiry) { + msg = s.toast_msg; + active = true; + } + } + if (active) { + const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_NoBackground; + const float pad_x = 50.0f; + const float pad_y = 30.0f; + // Toast text is 2x the menu font so it's legible at headset + // distance even when the menu chrome is hidden. + const float toast_scale = 2.0f; + // Bound text wrap to the eye width so long messages wrap instead + // of running past the right edge. Pass the wrap position as an + // explicit window-local pixel value rather than 0.0f — with + // AlwaysAutoResize, a wrap-at-content-right behaves circularly + // (window width depends on content, content wrap depends on + // window width) and collapses to one glyph per line on first + // frame, producing a vertical column of characters. + const float wrap_local = (std::max)(64.0f, + static_cast(s.eye_w) - 2.0f * pad_x - + 2.0f * ImGui::GetStyle().WindowPadding.x); + // Anchor by bottom-left pivot so wrapped multi-line toasts grow + // upward into the screen rather than falling off the bottom. + ImGui::SetNextWindowPos( + ImVec2(pad_x, static_cast(s.eye_h) - pad_y), + ImGuiCond_Always, + ImVec2(0.0f, 1.0f)); + if (ImGui::Begin("##vrto3d_toast", nullptr, flags)) { + ImGui::SetWindowFontScale(toast_scale); + ImGui::PushTextWrapPos(wrap_local); + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 255, 0, 255)); + ImGui::TextUnformatted(msg.c_str()); + ImGui::PopStyleColor(); + ImGui::PopTextWrapPos(); + } + ImGui::End(); + } + } + + ImGui::Render(); + + ImDrawData* draw_data = ImGui::GetDrawData(); + if (!draw_data || draw_data->CmdListsCount == 0) { + return; // capture-only frame with nothing visible — skip the pass + } + + VkFramebuffer fb = s.GetFramebuffer(out_sbs_view, sbs_w, sbs_h); + if (fb == VK_NULL_HANDLE) return; + + // ----- Record the draw data into both halves of out_sbs ----- + // The caller guarantees out_sbs is in COLOR_ATTACHMENT_OPTIMAL; the pass + // LOADs the existing SbS pixels and ImGui's pipeline alpha-blends the + // overlay on top (mirroring the D3D11 composite blend state). + VkRenderPassBeginInfo rp{}; + rp.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rp.renderPass = s.osd_pass; + rp.framebuffer = fb; + rp.renderArea.offset = {0, 0}; + rp.renderArea.extent = {sbs_w, sbs_h}; + vkCmdBeginRenderPass(cmd, &rp, VK_SUBPASS_CONTENTS_INLINE); + + // The backend sets its own viewport/scissor and projection from the draw + // data's DisplayPos/DisplaySize, so per-half placement is done by + // temporarily rewriting those fields (the Vulkan analogue of the D3D11 + // impl's two composite viewports): + // - DisplaySize = full SbS extent -> the backend's viewport covers the + // whole image and 1 ImGui unit == 1 pixel, + // - DisplayPos 0 puts the eye-sized content in the left half, + // - DisplayPos.x = -half_w shifts the identical content (and the + // backend's scissor clamping, which subtracts DisplayPos from every + // clip rect) into the right half. + // Clip rects are bounded by io.DisplaySize (the per-eye size used during + // NewFrame), so neither pass bleeds across the seam. + const float half_w = static_cast(sbs_w / 2); + const ImVec2 orig_pos = draw_data->DisplayPos; + const ImVec2 orig_size = draw_data->DisplaySize; + draw_data->DisplaySize = ImVec2(static_cast(sbs_w), static_cast(sbs_h)); + + // Left eye. + draw_data->DisplayPos = ImVec2(0.0f, 0.0f); + ImGui_ImplVulkan_RenderDrawData(draw_data, cmd); + + // Right eye. + draw_data->DisplayPos = ImVec2(-half_w, 0.0f); + ImGui_ImplVulkan_RenderDrawData(draw_data, cmd); + + draw_data->DisplayPos = orig_pos; + draw_data->DisplaySize = orig_size; + + vkCmdEndRenderPass(cmd); +} + +} // namespace vrto3d::osd + +#endif // !_WIN32 diff --git a/vrto3d/src/platform.h b/vrto3d/src/platform.h new file mode 100644 index 00000000..a380b465 --- /dev/null +++ b/vrto3d/src/platform.h @@ -0,0 +1,125 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#endif + +#include "openvr_driver.h" + +namespace platform { + +struct MonitorInfo { + int32_t index = 0; // 1-based enumeration order; 0 = primary fallback + int32_t x = 0; + int32_t y = 0; + uint32_t width = 0; + uint32_t height = 0; + float refresh_hz = 60.0f; + std::string device_name; // OS display device name ("\\.\DISPLAY1" on Win) + bool is_primary = false; +}; + +// Enumerate all monitors in display-index order (1-based). +std::vector EnumerateMonitors(); + +// Resolve display_index (0 = primary/auto, 1..N = enumeration order). If +// multi_display is true, out_secondary is filled with the contiguous right +// neighbor when it has matching geometry; otherwise out_secondary.width == 0. +bool ResolveTargetMonitors(int32_t display_index, + bool multi_display, + MonitorInfo& out_primary, + MonitorInfo& out_secondary); + +// Refresh rate lookup for the resolved primary monitor. +float QueryRefreshHz(const MonitorInfo& monitor, float fallback_hz = 60.0f); + +// Monotonic clock in seconds (QPC on Windows, CLOCK_MONOTONIC on Linux). +double MonotonicSeconds(); + +// Short process name for a pid ("game.exe" on Windows, /proc comm on Linux). +std::string GetProcessNameByPid(uint32_t pid); + +#ifdef _WIN32 +// LUID of the default graphics adapter — compositor picks GPU off the HMD's +// Prop_GraphicsAdapterLuid_Uint64. +LUID PrimaryAdapterLuid(); + +// Create a D3D11 device on the adapter matching the given LUID. Returns true on success. +bool CreateD3D11Device(LUID adapter_luid, + Microsoft::WRL::ComPtr& out_device, + Microsoft::WRL::ComPtr& out_context, + Microsoft::WRL::ComPtr& out_adapter); + +// Import a SteamVR-provided SharedTextureHandle_t into a D3D11 texture on the +// given device. Uses OpenSharedResource1 (NT handle) with a fallback to the +// legacy OpenSharedResource path. +bool ImportSharedTexture(ID3D11Device* device, + vr::SharedTextureHandle_t handle, + Microsoft::WRL::ComPtr& out_texture); + + +// Borderless topmost present window. One per presenter. Owns its native handle. +class PresentWindow { +public: + virtual ~PresentWindow() = default; + + // Returns the underlying HWND (caller casts). + virtual void* NativeHandle() const = 0; + + // Monitor geometry that was used to size the window. + virtual uint32_t Width() const = 0; + virtual uint32_t Height() const = 0; + + // Drain the OS message/event queue; must be called from the thread that created the window. + virtual void PollEvents() = 0; + + // Set topmost / z-order. No-op if already in desired state. + virtual void BringToTop() = 0; + virtual void ReleaseTopmost() = 0; + + // For multi_display layouts: nudge the window right by one pixel then back + // to work around a two-monitor placement glitch on first show. + virtual void MultiDisplayNudge() = 0; + + virtual bool ShouldClose() const = 0; +}; + +std::unique_ptr CreatePresentWindow(const MonitorInfo& primary, + const MonitorInfo* secondary_for_multi_display, + const char* title); + +// Pin the calling thread to Per-Monitor V2 DPI awareness for its lifetime so +// any window it creates is V2-aware regardless of vrserver.exe's process-level +// awareness. Call from each presenter's window thread before CreatePresentWindow. +// No-op on pre-Win10-1607. +void EnablePerMonitorV2DpiAwareness(); +#endif // _WIN32 + + +// Lightweight process helpers (for focus-thread's "is the tracked app still running?" check). +bool IsProcessRunning(uint32_t pid); + +} // namespace platform diff --git a/vrto3d/src/platform_linux.cpp b/vrto3d/src/platform_linux.cpp new file mode 100644 index 00000000..a5d3ae36 --- /dev/null +++ b/vrto3d/src/platform_linux.cpp @@ -0,0 +1,346 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ + +// Linux implementation of the portable platform:: helpers (platform.h). +// Monitor enumeration prefers XRandR when DISPLAY is set (also covers +// XWayland), falling back to a minimal wl_output listing on pure Wayland +// sessions. Everything else is /proc + CLOCK_MONOTONIC. + +#include "platform.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include + +namespace platform { + +namespace { + +// --------------------------------------------------------------------------- +// XRandR enumeration +// --------------------------------------------------------------------------- +std::vector EnumerateMonitorsX11() +{ + std::vector out; + + Display* dpy = XOpenDisplay(nullptr); + if (!dpy) return out; + + Window root = DefaultRootWindow(dpy); + XRRScreenResources* res = XRRGetScreenResourcesCurrent(dpy, root); + if (!res) { + XCloseDisplay(dpy); + return out; + } + + RROutput primary = XRRGetOutputPrimary(dpy, root); + + for (int i = 0; i < res->noutput; ++i) { + XRROutputInfo* oi = XRRGetOutputInfo(dpy, res, res->outputs[i]); + if (!oi) continue; + if (oi->connection == RR_Connected && oi->crtc != 0) { + XRRCrtcInfo* ci = XRRGetCrtcInfo(dpy, res, oi->crtc); + if (ci && ci->width > 0 && ci->height > 0) { + MonitorInfo m; + m.index = static_cast(out.size()) + 1; + m.x = ci->x; + m.y = ci->y; + m.width = ci->width; + m.height = ci->height; + m.is_primary = (res->outputs[i] == primary); + if (oi->name) { + m.device_name.assign(oi->name, static_cast(oi->nameLen)); + } + + // Refresh from the CRTC's mode line: dotClock / (hTotal * vTotal). + for (int mm = 0; mm < res->nmode; ++mm) { + const XRRModeInfo& mi = res->modes[mm]; + if (mi.id == ci->mode && mi.hTotal > 0 && mi.vTotal > 0) { + m.refresh_hz = static_cast( + static_cast(mi.dotClock) + / (static_cast(mi.hTotal) * static_cast(mi.vTotal))); + break; + } + } + out.push_back(m); + } + if (ci) XRRFreeCrtcInfo(ci); + } + XRRFreeOutputInfo(oi); + } + + XRRFreeScreenResources(res); + XCloseDisplay(dpy); + return out; +} + +// --------------------------------------------------------------------------- +// Minimal Wayland wl_output enumeration (pure Wayland session, no XWayland). +// Connect, bind every wl_output, roundtrip until geometry/mode/name/done have +// arrived, then disconnect. +// --------------------------------------------------------------------------- +struct WlOutputState { + wl_output* output = nullptr; + int32_t x = 0; + int32_t y = 0; + int32_t width = 0; + int32_t height = 0; + int32_t refresh_mhz = 0; + std::string name; + std::string model; + bool done = false; +}; + +struct WlEnumState { + std::vector outputs; +}; + +void WlOutputGeometry(void* data, wl_output*, int32_t x, int32_t y, + int32_t, int32_t, int32_t, + const char*, const char* model, int32_t) +{ + auto* o = static_cast(data); + o->x = x; + o->y = y; + if (model) o->model = model; +} + +void WlOutputMode(void* data, wl_output*, uint32_t flags, + int32_t width, int32_t height, int32_t refresh) +{ + auto* o = static_cast(data); + if (flags & WL_OUTPUT_MODE_CURRENT) { + o->width = width; + o->height = height; + o->refresh_mhz = refresh; + } +} + +void WlOutputDone(void* data, wl_output*) +{ + static_cast(data)->done = true; +} + +void WlOutputScale(void*, wl_output*, int32_t) {} + +void WlOutputName(void* data, wl_output*, const char* name) +{ + auto* o = static_cast(data); + if (name) o->name = name; +} + +void WlOutputDescription(void*, wl_output*, const char*) {} + +const wl_output_listener kWlOutputListener = { + WlOutputGeometry, + WlOutputMode, + WlOutputDone, + WlOutputScale, + WlOutputName, + WlOutputDescription, +}; + +void WlRegistryGlobal(void* data, wl_registry* registry, uint32_t name, + const char* interface, uint32_t version) +{ + auto* state = static_cast(data); + if (std::strcmp(interface, wl_output_interface.name) == 0) { + uint32_t v = version < 4 ? version : 4; + auto* o = new WlOutputState(); + o->output = static_cast( + wl_registry_bind(registry, name, &wl_output_interface, v)); + wl_output_add_listener(o->output, &kWlOutputListener, o); + state->outputs.push_back(o); + } +} + +void WlRegistryGlobalRemove(void*, wl_registry*, uint32_t) {} + +const wl_registry_listener kWlRegistryListener = { + WlRegistryGlobal, + WlRegistryGlobalRemove, +}; + +std::vector EnumerateMonitorsWayland() +{ + std::vector out; + + wl_display* display = wl_display_connect(nullptr); + if (!display) return out; + + WlEnumState state; + wl_registry* registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, &kWlRegistryListener, &state); + + // First roundtrip binds the outputs, further ones drain their events. + wl_display_roundtrip(display); + for (int i = 0; i < 4; ++i) { + bool all_done = true; + for (const auto* o : state.outputs) { + if (!o->done) { all_done = false; break; } + } + if (all_done) break; + if (wl_display_roundtrip(display) < 0) break; + } + + for (auto* o : state.outputs) { + MonitorInfo m; + m.index = static_cast(out.size()) + 1; + m.x = o->x; + m.y = o->y; + m.width = o->width > 0 ? static_cast(o->width) : 0; + m.height = o->height > 0 ? static_cast(o->height) : 0; + if (o->refresh_mhz > 0) { + m.refresh_hz = static_cast(o->refresh_mhz) / 1000.0f; + } + m.device_name = !o->name.empty() ? o->name : o->model; + // Wayland has no "primary output" concept — treat the first as primary. + m.is_primary = out.empty(); + out.push_back(m); + + wl_output_destroy(o->output); + delete o; + } + state.outputs.clear(); + + wl_registry_destroy(registry); + wl_display_disconnect(display); + return out; +} + +bool EnvSet(const char* name) +{ + const char* v = getenv(name); + return v != nullptr && v[0] != '\0'; +} + +} // namespace + + +std::vector EnumerateMonitors() +{ + std::vector out; + if (EnvSet("DISPLAY")) { + out = EnumerateMonitorsX11(); + } else if (EnvSet("WAYLAND_DISPLAY")) { + out = EnumerateMonitorsWayland(); + } + + // Ensure primary is at index 1 if possible (mirrors the Windows helper). + auto primary_it = std::find_if(out.begin(), out.end(), + [](const MonitorInfo& m) { return m.is_primary; }); + if (primary_it != out.end() && primary_it != out.begin()) { + std::iter_swap(out.begin(), primary_it); + for (size_t i = 0; i < out.size(); ++i) out[i].index = static_cast(i) + 1; + } + return out; +} + + +bool ResolveTargetMonitors(int32_t display_index, + bool multi_display, + MonitorInfo& out_primary, + MonitorInfo& out_secondary) +{ + out_secondary = {}; + auto monitors = EnumerateMonitors(); + if (monitors.empty()) return false; + + // 0 or out-of-range -> primary (first). + if (display_index <= 0 || display_index > static_cast(monitors.size())) { + out_primary = monitors.front(); + } else { + out_primary = monitors[static_cast(display_index - 1)]; + } + + if (multi_display) { + // Find a contiguous right neighbor with matching width/height + same top Y. + const int32_t right_x = out_primary.x + static_cast(out_primary.width); + for (const auto& m : monitors) { + if (m.index == out_primary.index) continue; + if (m.y == out_primary.y + && m.x == right_x + && m.width == out_primary.width + && m.height == out_primary.height) { + out_secondary = m; + break; + } + } + if (out_secondary.width == 0) { + std::fprintf(stderr, "[vrto3d] multi_display=true but no contiguous right " + "neighbor matched; continuing single-monitor.\n"); + } + } + return true; +} + + +float QueryRefreshHz(const MonitorInfo& monitor, float fallback_hz) +{ + if (monitor.refresh_hz > 1.0f) return monitor.refresh_hz; + return fallback_hz; +} + + +double MonotonicSeconds() +{ + timespec ts{}; + clock_gettime(CLOCK_MONOTONIC, &ts); + return static_cast(ts.tv_sec) + static_cast(ts.tv_nsec) * 1e-9; +} + + +std::string GetProcessNameByPid(uint32_t pid) +{ + char path[64]; + std::snprintf(path, sizeof(path), "/proc/%u/comm", pid); + + FILE* f = std::fopen(path, "r"); + if (!f) return ""; + + char buf[256] = {}; + size_t n = std::fread(buf, 1, sizeof(buf) - 1, f); + std::fclose(f); + + std::string name(buf, n); + while (!name.empty() && (name.back() == '\n' || name.back() == '\r' + || name.back() == ' ' || name.back() == '\t')) { + name.pop_back(); + } + return name; +} + + +bool IsProcessRunning(uint32_t pid) +{ + char path[64]; + std::snprintf(path, sizeof(path), "/proc/%u", pid); + struct stat st{}; + return stat(path, &st) == 0; +} + +} // namespace platform diff --git a/vrto3d/src/platform_win32.cpp b/vrto3d/src/platform_win32.cpp new file mode 100644 index 00000000..677fbbcb --- /dev/null +++ b/vrto3d/src/platform_win32.cpp @@ -0,0 +1,432 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ + +#define WIN32_LEAN_AND_MEAN +#include "platform.h" + +#include +#include // ID3D10Multithread +#include +#include + +#include +#include + +#include "vrto3dlib/debug_log.hpp" +#include "vrto3dlib/win32_helper.hpp" + +#include + +namespace platform { + +namespace { + +struct EnumState { + std::vector* out; +}; + +BOOL CALLBACK MonitorEnumProc(HMONITOR hMonitor, HDC, LPRECT, LPARAM lParam) +{ + auto* state = reinterpret_cast(lParam); + + MONITORINFOEXW info{}; + info.cbSize = sizeof(info); + if (!GetMonitorInfoW(hMonitor, &info)) return TRUE; + + MonitorInfo m; + m.index = static_cast(state->out->size()) + 1; + m.x = info.rcMonitor.left; + m.y = info.rcMonitor.top; + m.width = static_cast(info.rcMonitor.right - info.rcMonitor.left); + m.height = static_cast(info.rcMonitor.bottom - info.rcMonitor.top); + m.is_primary = (info.dwFlags & MONITORINFOF_PRIMARY) != 0; + + // Narrow device name to UTF-8 for logs / config matching. + char narrow[CCHDEVICENAME * 2] = {}; + WideCharToMultiByte(CP_UTF8, 0, info.szDevice, -1, narrow, sizeof(narrow), nullptr, nullptr); + m.device_name = narrow; + + DEVMODEW dm{}; + dm.dmSize = sizeof(dm); + if (EnumDisplaySettingsW(info.szDevice, ENUM_CURRENT_SETTINGS, &dm)) { + m.refresh_hz = static_cast(dm.dmDisplayFrequency); + } + + state->out->push_back(m); + return TRUE; +} + +} // namespace + + +std::vector EnumerateMonitors() +{ + std::vector out; + EnumState state{ &out }; + EnumDisplayMonitors(nullptr, nullptr, MonitorEnumProc, reinterpret_cast(&state)); + + // Ensure primary is at index 1 if possible (Windows enumerates in unspecified order). + auto primary_it = std::find_if(out.begin(), out.end(), [](const MonitorInfo& m){ return m.is_primary; }); + if (primary_it != out.end() && primary_it != out.begin()) { + std::iter_swap(out.begin(), primary_it); + for (size_t i = 0; i < out.size(); ++i) out[i].index = static_cast(i) + 1; + } + return out; +} + + +bool ResolveTargetMonitors(int32_t display_index, + bool multi_display, + MonitorInfo& out_primary, + MonitorInfo& out_secondary) +{ + out_secondary = {}; + auto monitors = EnumerateMonitors(); + if (monitors.empty()) return false; + + // 0 or out-of-range -> primary (first). + if (display_index <= 0 || display_index > static_cast(monitors.size())) { + out_primary = monitors.front(); + } else { + out_primary = monitors[static_cast(display_index - 1)]; + } + + if (multi_display) { + // Find a contiguous right neighbor with matching width/height + same top Y. + const int32_t right_x = out_primary.x + static_cast(out_primary.width); + for (const auto& m : monitors) { + if (m.index == out_primary.index) continue; + if (m.y == out_primary.y + && m.x == right_x + && m.width == out_primary.width + && m.height == out_primary.height) { + out_secondary = m; + break; + } + } + if (out_secondary.width == 0) { + LOG() << "multi_display=true but no contiguous right neighbor matched; continuing single-monitor."; + } + } + return true; +} + + +float QueryRefreshHz(const MonitorInfo& monitor, float fallback_hz) +{ + if (monitor.refresh_hz > 1.0f) return monitor.refresh_hz; + + DEVMODEW dm{}; + dm.dmSize = sizeof(dm); + + std::wstring wide; + wide.resize(monitor.device_name.size() + 1); + int n = MultiByteToWideChar(CP_UTF8, 0, monitor.device_name.c_str(), -1, + wide.data(), static_cast(wide.size())); + if (n > 0 && EnumDisplaySettingsW(wide.c_str(), ENUM_CURRENT_SETTINGS, &dm)) { + if (dm.dmDisplayFrequency > 1) return static_cast(dm.dmDisplayFrequency); + } + return fallback_hz; +} + + +LUID PrimaryAdapterLuid() +{ + Microsoft::WRL::ComPtr factory; + if (FAILED(CreateDXGIFactory1(IID_PPV_ARGS(&factory)))) return LUID{}; + + Microsoft::WRL::ComPtr adapter; + if (FAILED(factory->EnumAdapters1(0, &adapter))) return LUID{}; + + DXGI_ADAPTER_DESC1 desc{}; + if (FAILED(adapter->GetDesc1(&desc))) return LUID{}; + return desc.AdapterLuid; +} + + +bool CreateD3D11Device(LUID adapter_luid, + Microsoft::WRL::ComPtr& out_device, + Microsoft::WRL::ComPtr& out_context, + Microsoft::WRL::ComPtr& out_adapter) +{ + Microsoft::WRL::ComPtr factory; + if (FAILED(CreateDXGIFactory1(IID_PPV_ARGS(&factory)))) return false; + + // Match on LUID; fall back to adapter 0 if no match. + for (UINT i = 0; ; ++i) { + Microsoft::WRL::ComPtr a; + if (factory->EnumAdapters1(i, &a) == DXGI_ERROR_NOT_FOUND) break; + DXGI_ADAPTER_DESC1 desc{}; + a->GetDesc1(&desc); + if (desc.AdapterLuid.LowPart == adapter_luid.LowPart + && desc.AdapterLuid.HighPart == adapter_luid.HighPart) { + out_adapter = a; + break; + } + } + if (!out_adapter) { + if (FAILED(factory->EnumAdapters1(0, &out_adapter))) return false; + } + + UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; +#ifdef _DEBUG + flags |= D3D11_CREATE_DEVICE_DEBUG; +#endif + const D3D_FEATURE_LEVEL levels[] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0 }; + D3D_FEATURE_LEVEL got{}; + HRESULT hr = D3D11CreateDevice(out_adapter.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, flags, + levels, static_cast(std::size(levels)), + D3D11_SDK_VERSION, &out_device, &got, &out_context); +#ifdef _DEBUG + if (FAILED(hr)) { + // Debug layer may not be installed; retry without it. + hr = D3D11CreateDevice(out_adapter.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, + D3D11_CREATE_DEVICE_BGRA_SUPPORT, + levels, static_cast(std::size(levels)), + D3D11_SDK_VERSION, &out_device, &got, &out_context); + } +#endif + if (FAILED(hr)) return false; + + // Turn on D3D11's per-call immediate-context lock. Our own code serializes + // context access with a mutex, but external present-chain hooks (RTSS, + // Discord overlay, Steam overlay, OBS game-capture) draw their overlay + // from inside IDXGISwapChain::Present using the *same* immediate context, + // outside our mutex. Without this flag the runtime assumes single-threaded + // context use and the overlay's draws can race our compositor thread's + // OnDirectModeFrame work — manifesting as a device-removed crash within + // seconds (reported against RTSS at 7680x2160 + Cyberpunk). + Microsoft::WRL::ComPtr mt; + if (SUCCEEDED(out_context.As(&mt)) && mt) { + mt->SetMultithreadProtected(TRUE); + } + return true; +} + + +bool ImportSharedTexture(ID3D11Device* device, + vr::SharedTextureHandle_t handle, + Microsoft::WRL::ComPtr& out_texture) +{ + if (!device || handle == 0) return false; + + HANDLE h = reinterpret_cast(handle); + + // Try the NT-handle path first (Prop_GraphicsAdapterLuid + OpenSharedResource1). + Microsoft::WRL::ComPtr dev1; + if (SUCCEEDED(device->QueryInterface(IID_PPV_ARGS(&dev1)))) { + if (SUCCEEDED(dev1->OpenSharedResource1(h, IID_PPV_ARGS(&out_texture)))) { + return true; + } + } + // Fall back to legacy shared handle. + if (SUCCEEDED(device->OpenSharedResource(h, IID_PPV_ARGS(&out_texture)))) { + return true; + } + return false; +} + + +//========================================================================== +// Win32 present window +//========================================================================== + +namespace { + +constexpr wchar_t kWndClassName[] = L"VRto3D_PresentWindow"; + +LRESULT CALLBACK PresentWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) +{ + switch (msg) { + case WM_CLOSE: { + // Alt+F4 (or the window's "X" button) on our present window + // routes here. If a game is currently connected to SteamVR, + // post WM_CLOSE to it first so SteamVR doesn't prompt "an + // application is using SteamVR, are you sure?" — then wait for + // the game process to exit before tearing SteamVR down. With + // nothing connected, exits SteamVR immediately. Detached so + // the poll loops inside the helper don't stall this pump. + const uint32_t pid = g_current_app_pid.load(); + LOG() << "VRto3D window WM_CLOSE — pid=" << pid; + SetPropW(hwnd, L"vrto3d_close", reinterpret_cast(1)); + std::thread([pid]{ RequestSteamVRShutdownWithApp(pid); }).detach(); + return 0; + } + case WM_ERASEBKGND: + return 1; // we paint everything via D3D + + // Eat DPI-resize messages. Our swap chain is sized at window creation + // and we never want Windows to grow our client rect to a DPI-scaled + // suggested size — DefWindowProc would do that on Per-Monitor-V2 + // windows and the swap chain would be stretched off the visible + // monitor. + case 0x02E0: // WM_DPICHANGED + case 0x02E4: // WM_GETDPISCALEDSIZE + return 0; + } + return DefWindowProcW(hwnd, msg, wp, lp); +} + +void EnsureClass() +{ + static bool registered = false; + if (registered) return; + + WNDCLASSEXW wc{}; + wc.cbSize = sizeof(wc); + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = PresentWndProc; + wc.hInstance = GetModuleHandleW(nullptr); + wc.hCursor = LoadCursorW(nullptr, IDC_ARROW); + wc.hbrBackground = reinterpret_cast(GetStockObject(BLACK_BRUSH)); + wc.lpszClassName = kWndClassName; + RegisterClassExW(&wc); + registered = true; +} + +class Win32PresentWindow : public PresentWindow { +public: + Win32PresentWindow(HWND h, uint32_t w, uint32_t ht) : hwnd_(h), w_(w), h_(ht) {} + ~Win32PresentWindow() override + { + if (hwnd_) DestroyWindow(hwnd_); + } + + void* NativeHandle() const override { return hwnd_; } + uint32_t Width() const override { return w_; } + uint32_t Height() const override { return h_; } + + void PollEvents() override + { + MSG msg; + while (PeekMessageW(&msg, hwnd_, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + + void BringToTop() override + { + // Always re-assert: even if we believe we're topmost, other apps can + // bump us with their own SetWindowPos. Re-applying is cheap. + if (!IsWindowVisible(hwnd_)) { + ShowWindow(hwnd_, SW_SHOWNOACTIVATE); + } + SetWindowPos(hwnd_, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW); + is_topmost_ = true; + } + + void ReleaseTopmost() override + { + if (!is_topmost_) return; + SetWindowPos(hwnd_, HWND_NOTOPMOST, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + is_topmost_ = false; + } + + void MultiDisplayNudge() override + { + RECT r{}; + if (!GetWindowRect(hwnd_, &r)) return; + SetWindowPos(hwnd_, nullptr, r.left + 1, r.top, 0, 0, + SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + SetWindowPos(hwnd_, nullptr, r.left, r.top, 0, 0, + SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + } + + bool ShouldClose() const override + { + return GetPropW(hwnd_, L"vrto3d_close") != nullptr; + } + +private: + HWND hwnd_ = nullptr; + uint32_t w_ = 0, h_ = 0; + bool is_topmost_ = false; +}; + +} // namespace + + +std::unique_ptr CreatePresentWindow(const MonitorInfo& primary, + const MonitorInfo* secondary_for_multi_display, + const char* title) +{ + EnsureClass(); + + int x = primary.x; + int y = primary.y; + uint32_t w = primary.width; + uint32_t h = primary.height; + + if (secondary_for_multi_display && secondary_for_multi_display->width > 0) { + w = primary.width + secondary_for_multi_display->width; + } + + std::wstring wtitle; + if (title) { + int n = MultiByteToWideChar(CP_UTF8, 0, title, -1, nullptr, 0); + wtitle.resize(n); + MultiByteToWideChar(CP_UTF8, 0, title, -1, wtitle.data(), n); + } else { + wtitle = L"VRto3D"; + } + + HWND hwnd = CreateWindowExW(WS_EX_APPWINDOW, + kWndClassName, wtitle.c_str(), + WS_POPUP | WS_VISIBLE, + x, y, static_cast(w), static_cast(h), + nullptr, nullptr, GetModuleHandleW(nullptr), nullptr); + if (!hwnd) { + LOG() << "CreateWindowExW failed; last-error=" << GetLastError(); + return nullptr; + } + // Window starts non-topmost. WindowPresenter::FocusThreadLoop asserts + // HWND_TOPMOST via BringToTop() when the focus flags request it + // (Ctrl+F8 toggle, auto_focus on tracked-app launch, UE3D IPC). + // WS_EX_APPWINDOW keeps us in the alt-tab list. + ShowWindow(hwnd, SW_SHOWNOACTIVATE); + LOG() << "Win32: present window created hwnd=" << hwnd + << " rect=(" << x << "," << y << " " << w << "x" << h << ")"; + return std::make_unique(hwnd, w, h); +} + + +void EnablePerMonitorV2DpiAwareness() +{ + using SetThreadDpiAwarenessContextFn = DPI_AWARENESS_CONTEXT (WINAPI*)(DPI_AWARENESS_CONTEXT); + if (HMODULE user32 = GetModuleHandleW(L"user32.dll")) { + if (auto set_ctx = reinterpret_cast( + GetProcAddress(user32, "SetThreadDpiAwarenessContext"))) { + set_ctx(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + } + } +} + + +bool IsProcessRunning(uint32_t pid) +{ + if (pid == 0) return false; + HANDLE h = OpenProcess(SYNCHRONIZE, FALSE, pid); + if (!h) return false; + DWORD wait = WaitForSingleObject(h, 0); + CloseHandle(h); + return wait == WAIT_TIMEOUT; +} + +} // namespace platform diff --git a/vrto3d/src/presenter/display_timing_helper.cpp b/vrto3d/src/presenter/display_timing_helper.cpp new file mode 100644 index 00000000..7385ce27 --- /dev/null +++ b/vrto3d/src/presenter/display_timing_helper.cpp @@ -0,0 +1,839 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ + + +#include "display_timing_helper.h" +#include "display_utils.h" + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "platform.h" +#include "vrto3dlib/debug_log.hpp" + +// NVAPI include must be at file scope (before any C++ namespace) so its +// typedefs (NvU32, NvAPI_Status, …) land in the global namespace, not in +// vrto3d. This is the only NVAPI consumer left in the driver itself — the +// NvidiaDX9 presenter's NVAPI usage now lives inside NV3D-Lib. +#include "nvapi.h" + +// AMD ADL — pure C headers, no extra namespace concerns. +#include "adl_sdk.h" +#include "adl_structures.h" +#include "adl_defines.h" + +namespace vrto3d { + +// =========================================================================== +// Shared helpers +// =========================================================================== + +namespace { + +using display_utils::DisplayPositionSnapshot; +using display_utils::SnapshotDisplayPositions; +using display_utils::RestoreDisplayPositions; +using display_utils::WaitForModesetSettle; + +// Resolve the GDI device name for a given display_index. +std::wstring ResolveDeviceName(int32_t display_index) +{ + auto monitors = platform::EnumerateMonitors(); + if (monitors.empty()) return {}; + + size_t idx = 0; + if (display_index > 0 && display_index <= static_cast(monitors.size())) { + idx = static_cast(display_index - 1); + } + + std::wstring wide; + const auto& name = monitors[idx].device_name; + int n = MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, nullptr, 0); + wide.resize(n); + MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, wide.data(), n); + return wide; +} + +// Snapshot the current DEVMODE so we can force the desktop back to its +// original resolution after a backend revert. NVAPI's RevertCustomDisplayTrial +// usually does this on its own, but if the user reboots / kills the process +// between Apply and Revert we want a stable fallback for next launch too. +bool SnapshotCurrentMode(const std::wstring& device_name, DEVMODEW& out_mode) +{ + out_mode = {}; + out_mode.dmSize = sizeof(out_mode); + return EnumDisplaySettingsExW(device_name.c_str(), ENUM_CURRENT_SETTINGS, &out_mode, 0) != 0; +} + +// Restore desktop to a saved DEVMODE. Returns true on success or no-op. +bool RestoreSavedMode(const std::wstring& device_name, DEVMODEW& saved_mode) +{ + if (device_name.empty() || saved_mode.dmSize == 0) return false; + + DEVMODEW current{}; + current.dmSize = sizeof(current); + if (EnumDisplaySettingsExW(device_name.c_str(), ENUM_CURRENT_SETTINGS, ¤t, 0)) { + // Skip the modeset if we're already back at the saved mode. + if (current.dmPelsWidth == saved_mode.dmPelsWidth + && current.dmPelsHeight == saved_mode.dmPelsHeight + && current.dmDisplayFrequency == saved_mode.dmDisplayFrequency) { + return true; + } + } + + LONG result = ChangeDisplaySettingsExW( + device_name.c_str(), &saved_mode, nullptr, CDS_FULLSCREEN, nullptr); + if (result != DISP_CHANGE_SUCCESSFUL) { + LOG() << "RestoreSavedMode: ChangeDisplaySettingsExW failed result=" << result + << " — falling back to driver default"; + ChangeDisplaySettingsExW(device_name.c_str(), nullptr, nullptr, 0, nullptr); + } + return result == DISP_CHANGE_SUCCESSFUL; +} + +// Compute pixel clock in 10 kHz units from timing spec. +uint32_t ComputePixelClock10kHz(const FramePackTimingSpec& spec) +{ + // pclk = h_total * v_total * refresh_hz, in 10 kHz units + double pclk_hz = static_cast(spec.h_total) + * static_cast(spec.v_total) + * static_cast(spec.refresh_hz); + return static_cast(std::round(pclk_hz / 10000.0)); +} + +} // namespace + + +// =========================================================================== +// Backend state structures +// =========================================================================== + +struct DisplayTimingHelper::NvidiaState { + std::vector display_ids; + std::wstring device_name; + DEVMODEW original_mode{}; +}; + +struct DisplayTimingHelper::AmdState { + int adapter_index = -1; + int display_index = -1; + std::wstring device_name; + DEVMODEW original_mode{}; +}; + +struct DisplayTimingHelper::CruState { + std::wstring device_name; + DEVMODEW original_mode{}; +}; + + +// =========================================================================== +// DisplayTimingHelper lifecycle +// =========================================================================== + +DisplayTimingHelper::~DisplayTimingHelper() +{ + Revert(); +} + + +bool DisplayTimingHelper::Apply(const FramePackTimingSpec& spec, + int32_t display_index) +{ + if (active_) { + LOG() << "DisplayTimingHelper::Apply: already active, reverting first"; + Revert(); + } + + LOG() << "DisplayTimingHelper::Apply: " + << spec.active_w << "x" << spec.active_h + << " @" << spec.refresh_hz << "Hz" + << " h_total=" << spec.h_total << " v_total=" << spec.v_total + << " display_index=" << display_index; + + // Try backends in priority order. + if (TryNvidia(spec, display_index)) { + backend_ = Backend::Nvidia; + active_ = true; + LOG() << "DisplayTimingHelper: NVIDIA backend succeeded"; + return true; + } + + if (TryAmd(spec, display_index)) { + backend_ = Backend::Amd; + active_ = true; + LOG() << "DisplayTimingHelper: AMD backend succeeded"; + return true; + } + + if (TryCruFallback(spec, display_index)) { + backend_ = Backend::CruFallback; + active_ = true; + LOG() << "DisplayTimingHelper: CRU fallback succeeded"; + return true; + } + + LOG() << "DisplayTimingHelper::Apply: all backends failed. " + << "The display is in its original mode. Frame-pack TaB rendering " + << "will still work visually, but the HDMI 3D InfoFrame may not be sent. " + << "Consider using CRU (Custom Resolution Utility) to pre-add the timing."; + return false; +} + + +void DisplayTimingHelper::Revert() +{ + if (!active_) return; + + LOG() << "DisplayTimingHelper::Revert (backend=" + << static_cast(backend_) << ")"; + + switch (backend_) { + case Backend::Nvidia: RevertNvidia(); break; + case Backend::Amd: RevertAmd(); break; + case Backend::CruFallback: RevertCruFallback(); break; + default: break; + } + + // Free backend state. + if (backend_state_) { + switch (backend_) { + case Backend::Nvidia: + delete static_cast(backend_state_); + break; + case Backend::Amd: + delete static_cast(backend_state_); + break; + case Backend::CruFallback: + delete static_cast(backend_state_); + break; + default: break; + } + backend_state_ = nullptr; + } + + active_ = false; + backend_ = Backend::None; +} + + +// =========================================================================== +// NVIDIA backend — NvAPI_DISP_TryCustomDisplay (delay-loaded via nvapi64.lib) +// =========================================================================== + +namespace { + +// SEH-guarded probe for delay-loaded nvapi64.dll — same pattern NV3D-Lib +// uses inside CreateInterfaceDX11 (a missing DLL raises a delay-load +// exception instead of returning an error). +bool NvApiAvailable() +{ + __try { + NvAPI_Status status = NvAPI_Initialize(); + return (status == NVAPI_OK); + } __except ( + (GetExceptionCode() == VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND) || + GetExceptionCode() == VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND)) + ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH + ) { + return false; + } +} + +} // namespace + + +bool DisplayTimingHelper::TryNvidia(const FramePackTimingSpec& spec, + int32_t display_index) +{ + if (!NvApiAvailable()) { + LOG() << "DisplayTimingHelper::TryNvidia: NVAPI not available"; + return false; + } + + // Map the GDI device name for display_index to an NVAPI displayId. + std::wstring w_name = ResolveDeviceName(display_index); + if (w_name.empty()) { + LOG() << "DisplayTimingHelper::TryNvidia: could not resolve device name for index " + << display_index; + return false; + } + + // GDI device names are pure ASCII (e.g. "\\.\DISPLAY1") — cast is safe. + char narrow_name[NVAPI_SHORT_STRING_MAX] = {}; + for (size_t i = 0; i < w_name.size() && i < sizeof(narrow_name) - 1; ++i) + narrow_name[i] = static_cast(w_name[i]); + + NvU32 displayId = 0; + NvAPI_Status status = NvAPI_DISP_GetDisplayIdByDisplayName(narrow_name, &displayId); + if (status != NVAPI_OK) { + LOG() << "DisplayTimingHelper::TryNvidia: NvAPI_DISP_GetDisplayIdByDisplayName failed" + << " name='" << narrow_name << "' status=" << status; + return false; + } + + // Check for active Surround/Mosaic topology — TryCustomDisplay cannot + // change individual displays while they are grouped. + { + NV_MOSAIC_TOPO_BRIEF topoBrief = {}; + NV_MOSAIC_DISPLAY_SETTING dispSetting = {}; + NvS32 overlapX = 0, overlapY = 0; + topoBrief.version = NVAPI_MOSAIC_TOPO_BRIEF_VER; + dispSetting.version = NVAPI_MOSAIC_DISPLAY_SETTING_VER; + NvAPI_Status ms = NvAPI_Mosaic_GetCurrentTopo(&topoBrief, &dispSetting, &overlapX, &overlapY); + if (ms == NVAPI_OK && topoBrief.enabled) { + LOG() << "DisplayTimingHelper::TryNvidia: Surround/Mosaic active — skipping."; + return false; + } + } + + // Snapshot the desktop mode before we touch anything so Revert can force + // a return to it even if the NVAPI trial revert leaves the desktop in an + // intermediate state. + DEVMODEW original_mode{}; + SnapshotCurrentMode(w_name, original_mode); + + // Fetch the live timing so we can merge polarity and interlaced flags. + NV_TIMING live_timing = {}; + { + NV_TIMING_INPUT ti = {}; + ti.version = NV_TIMING_INPUT_VER; + NvAPI_Status ts = NvAPI_DISP_GetTiming(displayId, &ti, &live_timing); + if (ts != NVAPI_OK) { + LOG() << "DisplayTimingHelper::TryNvidia: NvAPI_DISP_GetTiming failed status=" << ts + << " — proceeding with default polarity"; + } + } + + // Build NV_TIMING from the frame-pack spec, merging live polarity fields. + NV_TIMING t = {}; + t.pclk = static_cast(ComputePixelClock10kHz(spec)); + t.HTotal = spec.h_total; + t.HVisible = static_cast(spec.active_w); + t.HBorder = 0; + t.HFrontPorch = spec.h_front_porch; + t.HSyncWidth = spec.h_sync_width; + t.HSyncPol = live_timing.HSyncPol; + t.VTotal = spec.v_total; + t.VVisible = static_cast(spec.active_h); + t.VBorder = 0; + t.VFrontPorch = spec.v_front_porch; + t.VSyncWidth = spec.v_sync_width; + t.VSyncPol = live_timing.VSyncPol; + t.interlaced = 0; + t.etc.rr = static_cast(spec.refresh_hz + 0.5f); + t.etc.rrx1k = static_cast(spec.refresh_hz * 1000.0f + 0.5f); + + NV_CUSTOM_DISPLAY cd = {}; + cd.version = NV_CUSTOM_DISPLAY_VER; + cd.timing = t; + cd.srcPartition = { 0.0f, 0.0f, 1.0f, 1.0f }; + cd.width = spec.active_w; + cd.height = spec.active_h; + cd.colorFormat = NV_FORMAT_A8R8G8B8; + cd.xRatio = 1.0f; + cd.yRatio = 1.0f; + cd.depth = 32; + + LOG() << "DisplayTimingHelper::TryNvidia: applying " + << spec.active_w << "x" << spec.active_h << "@" << spec.refresh_hz << "Hz" + << " pclk=" << t.pclk << " HTotal=" << t.HTotal << " VTotal=" << t.VTotal + << " displayId=" << displayId; + + auto posSnapshot = SnapshotDisplayPositions(); + WaitForModesetSettle(100); + + NvU32 ids[1] = { displayId }; + NV_CUSTOM_DISPLAY cds[1] = { cd }; + status = NvAPI_DISP_TryCustomDisplay(ids, 1, cds); + if (status != NVAPI_OK) { + LOG() << "DisplayTimingHelper::TryNvidia: NvAPI_DISP_TryCustomDisplay failed status=" + << status << " — reverting trial"; + NvAPI_DISP_RevertCustomDisplayTrial(ids, 1); + WaitForModesetSettle(500); + RestoreDisplayPositions(posSnapshot); + return false; + } + + WaitForModesetSettle(500); + RestoreDisplayPositions(posSnapshot); + + auto* state = new NvidiaState(); + state->display_ids.push_back(displayId); + state->device_name = w_name; + state->original_mode = original_mode; + backend_state_ = state; + + LOG() << "DisplayTimingHelper::TryNvidia: succeeded displayId=" << displayId; + return true; +} + + +void DisplayTimingHelper::RevertNvidia() +{ + if (!backend_state_) return; + auto* state = static_cast(backend_state_); + + if (state->display_ids.empty()) return; + + LOG() << "DisplayTimingHelper::RevertNvidia: reverting " + << state->display_ids.size() << " display(s)"; + + auto posSnapshot = SnapshotDisplayPositions(); + WaitForModesetSettle(100); + + NvAPI_DISP_RevertCustomDisplayTrial(state->display_ids.data(), + static_cast(state->display_ids.size())); + + WaitForModesetSettle(500); + + // Force the desktop back to the original mode if the trial revert left it + // somewhere else (e.g. the driver picked a fallback mode after dropping + // the custom timing). + if (!state->device_name.empty()) { + RestoreSavedMode(state->device_name, state->original_mode); + WaitForModesetSettle(300); + } + + RestoreDisplayPositions(posSnapshot); +} + + +// =========================================================================== +// AMD backend — ADL_Display_ModeTimingOverride_Set +// =========================================================================== + +namespace { + +// ADL entry points we resolve at runtime. Names match the exports of +// atiadlxx.dll / atiadlxy.dll exactly. The DLL uses default (cdecl) calling +// convention for its exports; only the malloc callback is __stdcall. +using ADL_MAIN_CONTROL_CREATE_t = int (*)(ADL_MAIN_MALLOC_CALLBACK, int); +using ADL_MAIN_CONTROL_DESTROY_t = int (*)(); +using ADL_ADAPTER_NUMBEROFADAPTERS_GET_t = int (*)(int*); +using ADL_ADAPTER_ADAPTERINFO_GET_t = int (*)(LPAdapterInfo, int); +using ADL_DISPLAY_DISPLAYINFO_GET_t = int (*)(int, int*, ADLDisplayInfo**, int); +using ADL_DISPLAY_MODETIMINGOVERRIDE_SET_t = + int (*)(int iAdapterIndex, int iDisplayIndex, + ADLDisplayModeInfo* lpMode, int iForceUpdate); +using ADL_DISPLAY_MODETIMINGOVERRIDE_DELETE_t = + int (*)(int iAdapterIndex, int iDisplayIndex, + ADLDisplayMode* lpMode, int iForceUpdate); + +// ADL's required malloc callback. __stdcall on Windows. +void* __stdcall AdlMallocCallback(int sz) { return malloc(static_cast(sz)); } + +HMODULE LoadAdlLibrary() +{ + HMODULE hmod = LoadLibraryW(L"atiadlxx.dll"); + if (!hmod) hmod = LoadLibraryW(L"atiadlxy.dll"); + return hmod; +} + +// RAII wrapper for an ADL session so we always destroy on scope exit. +class AdlSession { +public: + AdlSession() = default; + ~AdlSession() { + if (destroy_) destroy_(); + if (dll_) FreeLibrary(dll_); + } + + bool Init() { + dll_ = LoadAdlLibrary(); + if (!dll_) return false; + + auto pfnCreate = reinterpret_cast( + GetProcAddress(dll_, "ADL_Main_Control_Create")); + destroy_ = reinterpret_cast( + GetProcAddress(dll_, "ADL_Main_Control_Destroy")); + if (!pfnCreate || !destroy_) { + FreeLibrary(dll_); dll_ = nullptr; + return false; + } + if (pfnCreate(AdlMallocCallback, 1) != ADL_OK) { + destroy_ = nullptr; + FreeLibrary(dll_); dll_ = nullptr; + return false; + } + + num_adapters_get_ = reinterpret_cast( + GetProcAddress(dll_, "ADL_Adapter_NumberOfAdapters_Get")); + adapter_info_get_ = reinterpret_cast( + GetProcAddress(dll_, "ADL_Adapter_AdapterInfo_Get")); + display_info_get_ = reinterpret_cast( + GetProcAddress(dll_, "ADL_Display_DisplayInfo_Get")); + timing_set_ = reinterpret_cast( + GetProcAddress(dll_, "ADL_Display_ModeTimingOverride_Set")); + timing_delete_ = reinterpret_cast( + GetProcAddress(dll_, "ADL_Display_ModeTimingOverride_Delete")); + + return num_adapters_get_ && adapter_info_get_ + && display_info_get_ && timing_set_; + } + + int NumAdapters(int* out) const { return num_adapters_get_(out); } + int AdapterInfoGet(LPAdapterInfo info, int size) const { return adapter_info_get_(info, size); } + int DisplayInfoGet(int adapter, int* num, ADLDisplayInfo** info, int force) const { + return display_info_get_(adapter, num, info, force); + } + int TimingSet(int adapter, int display, ADLDisplayModeInfo* mode, int force) const { + return timing_set_(adapter, display, mode, force); + } + int TimingDelete(int adapter, int display, ADLDisplayMode* mode, int force) const { + return timing_delete_ ? timing_delete_(adapter, display, mode, force) : ADL_ERR; + } + bool HasDelete() const { return timing_delete_ != nullptr; } + +private: + HMODULE dll_ = nullptr; + ADL_MAIN_CONTROL_DESTROY_t destroy_ = nullptr; + ADL_ADAPTER_NUMBEROFADAPTERS_GET_t num_adapters_get_ = nullptr; + ADL_ADAPTER_ADAPTERINFO_GET_t adapter_info_get_ = nullptr; + ADL_DISPLAY_DISPLAYINFO_GET_t display_info_get_ = nullptr; + ADL_DISPLAY_MODETIMINGOVERRIDE_SET_t timing_set_ = nullptr; + ADL_DISPLAY_MODETIMINGOVERRIDE_DELETE_t timing_delete_ = nullptr; +}; + +// Resolve (adapter_index, display_logical_index) for the target Windows +// display. AdapterInfo.iOSDisplayIndex is generated from EnumDisplayDevices, +// so display_index 1 (= "\\.\DISPLAY1") corresponds to iOSDisplayIndex 0. +bool FindAdlAdapterDisplay(const AdlSession& adl, + int32_t target_display_index, + int& out_adapter, + int& out_display) +{ + int num_adapters = 0; + if (adl.NumAdapters(&num_adapters) != ADL_OK || num_adapters <= 0) { + LOG() << "FindAdlAdapterDisplay: no ADL adapters"; + return false; + } + + std::vector adapters(static_cast(num_adapters)); + std::memset(adapters.data(), 0, adapters.size() * sizeof(AdapterInfo)); + if (adl.AdapterInfoGet(adapters.data(), + static_cast(adapters.size() * sizeof(AdapterInfo))) != ADL_OK) { + LOG() << "FindAdlAdapterDisplay: AdapterInfo_Get failed"; + return false; + } + + const int target_os_index = target_display_index - 1; // 1-based → 0-based + + // First pass: prefer an adapter whose iOSDisplayIndex matches our target. + // Fall back to the first present adapter so single-GPU systems still work + // when the OS index isn't populated. + int adapter_index = -1; + for (const auto& a : adapters) { + if (!a.iPresent) continue; + if (a.iOSDisplayIndex == target_os_index) { + adapter_index = a.iAdapterIndex; + break; + } + } + if (adapter_index < 0) { + for (const auto& a : adapters) { + if (a.iPresent) { adapter_index = a.iAdapterIndex; break; } + } + } + if (adapter_index < 0) { + LOG() << "FindAdlAdapterDisplay: no present adapter (likely no AMD GPU)"; + return false; + } + + // Enumerate displays on that adapter; pick the first connected+mapped one. + int num_displays = 0; + ADLDisplayInfo* display_info = nullptr; + if (adl.DisplayInfoGet(adapter_index, &num_displays, &display_info, 0) != ADL_OK + || num_displays <= 0 || !display_info) { + LOG() << "FindAdlAdapterDisplay: DisplayInfo_Get failed for adapter " << adapter_index; + return false; + } + + int display_index = -1; + constexpr int kMappedMask = + ADL_DISPLAY_DISPLAYINFO_DISPLAYCONNECTED | + ADL_DISPLAY_DISPLAYINFO_DISPLAYMAPPED; + for (int i = 0; i < num_displays; ++i) { + if ((display_info[i].iDisplayInfoValue & kMappedMask) == kMappedMask) { + display_index = display_info[i].displayID.iDisplayLogicalIndex; + break; + } + } + free(display_info); // allocated by AdlMallocCallback + + if (display_index < 0) { + LOG() << "FindAdlAdapterDisplay: no mapped display on adapter " << adapter_index; + return false; + } + + out_adapter = adapter_index; + out_display = display_index; + return true; +} + +// Build an ADLDisplayModeInfo from our timing spec. +void FillAdlModeInfo(const FramePackTimingSpec& spec, ADLDisplayModeInfo& out) +{ + out = {}; + out.iTimingStandard = ADL_DL_MODETIMING_STANDARD_CUSTOM; + out.iPossibleStandard = 0; + out.iRefreshRate = static_cast(spec.refresh_hz + 0.5f); + out.iPelsWidth = spec.active_w; + out.iPelsHeight = spec.active_h; + + ADLDetailedTiming& dt = out.sDetailedTiming; + dt.iSize = sizeof(dt); + dt.sTimingFlags = 0; // progressive, no double-scan, polarity default (+,+) + dt.sHTotal = static_cast(spec.h_total); + dt.sHDisplay = static_cast(spec.active_w); + dt.sHSyncStart = static_cast(spec.h_front_porch); // offset from active end + dt.sHSyncWidth = static_cast(spec.h_sync_width); + dt.sVTotal = static_cast(spec.v_total); + dt.sVDisplay = static_cast(spec.active_h); + dt.sVSyncStart = static_cast(spec.v_front_porch); + dt.sVSyncWidth = static_cast(spec.v_sync_width); + dt.sPixelClock = static_cast(ComputePixelClock10kHz(spec)); +} + +} // namespace + + +bool DisplayTimingHelper::TryAmd(const FramePackTimingSpec& spec, + int32_t display_index) +{ + AdlSession adl; + if (!adl.Init()) { + LOG() << "DisplayTimingHelper::TryAmd: ADL not available (not AMD GPU?)"; + return false; + } + + int adapter = -1, ddisplay = -1; + if (!FindAdlAdapterDisplay(adl, display_index, adapter, ddisplay)) { + return false; + } + + std::wstring w_name = ResolveDeviceName(display_index); + DEVMODEW original_mode{}; + SnapshotCurrentMode(w_name, original_mode); + + ADLDisplayModeInfo mode_info{}; + FillAdlModeInfo(spec, mode_info); + + LOG() << "DisplayTimingHelper::TryAmd: applying timing override " + << spec.active_w << "x" << spec.active_h << "@" << spec.refresh_hz << "Hz" + << " adapter=" << adapter << " display=" << ddisplay + << " HTotal=" << mode_info.sDetailedTiming.sHTotal + << " VTotal=" << mode_info.sDetailedTiming.sVTotal + << " pclk(10kHz)=" << static_cast(static_cast(mode_info.sDetailedTiming.sPixelClock)); + + auto posSnapshot = SnapshotDisplayPositions(); + WaitForModesetSettle(100); + + int rc = adl.TimingSet(adapter, ddisplay, &mode_info, /*iForceUpdate=*/1); + if (rc != ADL_OK && rc != ADL_OK_WARNING && rc != ADL_OK_MODE_CHANGE) { + LOG() << "DisplayTimingHelper::TryAmd: ADL_Display_ModeTimingOverride_Set failed rc=" + << rc; + return false; + } + + // ADL_Display_ModeTimingOverride_Set adds the timing to the driver's + // override list but doesn't necessarily switch into it; trigger the + // modeset via the OS. + if (!w_name.empty()) { + DEVMODEW dm{}; + dm.dmSize = sizeof(dm); + dm.dmPelsWidth = spec.active_w; + dm.dmPelsHeight = spec.active_h; + dm.dmDisplayFrequency = static_cast(spec.refresh_hz + 0.5f); + dm.dmBitsPerPel = 32; + dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_BITSPERPEL; + ChangeDisplaySettingsExW(w_name.c_str(), &dm, nullptr, CDS_FULLSCREEN, nullptr); + } + + WaitForModesetSettle(500); + RestoreDisplayPositions(posSnapshot); + + auto* state = new AmdState(); + state->adapter_index = adapter; + state->display_index = ddisplay; + state->device_name = w_name; + state->original_mode = original_mode; + backend_state_ = state; + + LOG() << "DisplayTimingHelper::TryAmd: succeeded (adapter=" << adapter + << " display=" << ddisplay << ")"; + return true; +} + + +void DisplayTimingHelper::RevertAmd() +{ + if (!backend_state_) return; + auto* state = static_cast(backend_state_); + + LOG() << "DisplayTimingHelper::RevertAmd: reverting (adapter=" + << state->adapter_index << " display=" << state->display_index << ")"; + + auto posSnapshot = SnapshotDisplayPositions(); + + // Restore the OS-side mode first so the desktop is back at the user's + // original resolution by the time we tear the override out. + if (!state->device_name.empty()) { + RestoreSavedMode(state->device_name, state->original_mode); + WaitForModesetSettle(300); + } + + // Now remove our override from the driver's list. Best effort; both + // Delete and a STANDARD_DRIVER_DEFAULT Set are valid revert strokes + // depending on the ADL version. + AdlSession adl; + if (adl.Init()) { + if (adl.HasDelete()) { + ADLDisplayMode mode{}; + mode.iPelsWidth = state->original_mode.dmPelsWidth; + mode.iPelsHeight = state->original_mode.dmPelsHeight; + mode.iDisplayFrequency = state->original_mode.dmDisplayFrequency; + mode.iBitsPerPel = 32; + adl.TimingDelete(state->adapter_index, state->display_index, &mode, 1); + } + ADLDisplayModeInfo info{}; + info.iTimingStandard = ADL_DL_MODETIMING_STANDARD_DRIVER_DEFAULT; + info.iPelsWidth = state->original_mode.dmPelsWidth; + info.iPelsHeight = state->original_mode.dmPelsHeight; + info.iRefreshRate = state->original_mode.dmDisplayFrequency; + info.sDetailedTiming.iSize = sizeof(info.sDetailedTiming); + adl.TimingSet(state->adapter_index, state->display_index, &info, 1); + } + + WaitForModesetSettle(300); + RestoreDisplayPositions(posSnapshot); +} + + +// =========================================================================== +// CRU fallback — ChangeDisplaySettingsExW +// +// This works when the user has pre-added the custom resolution via CRU +// (Custom Resolution Utility). The driver already knows the mode; we just +// need to switch to it. +// =========================================================================== + +bool DisplayTimingHelper::TryCruFallback(const FramePackTimingSpec& spec, + int32_t display_index) +{ + std::wstring device_name = ResolveDeviceName(display_index); + if (device_name.empty()) { + LOG() << "DisplayTimingHelper::TryCruFallback: could not resolve device name"; + return false; + } + + // Save the current display mode for revert. + DEVMODEW original{}; + original.dmSize = sizeof(original); + if (!EnumDisplaySettingsExW(device_name.c_str(), ENUM_CURRENT_SETTINGS, &original, 0)) { + LOG() << "DisplayTimingHelper::TryCruFallback: EnumDisplaySettingsExW(current) failed"; + return false; + } + + // Check if the target resolution exists in the mode list. + // ChangeDisplaySettingsExW will fail if the mode hasn't been pre-added via CRU. + bool mode_found = false; + DEVMODEW candidate{}; + candidate.dmSize = sizeof(candidate); + for (DWORD i = 0; EnumDisplaySettingsExW(device_name.c_str(), i, &candidate, 0); ++i) { + if (candidate.dmPelsWidth == spec.active_w && + candidate.dmPelsHeight == spec.active_h && + candidate.dmDisplayFrequency == static_cast(spec.refresh_hz + 0.5f)) { + mode_found = true; + break; + } + } + + if (!mode_found) { + LOG() << "DisplayTimingHelper::TryCruFallback: mode " + << spec.active_w << "x" << spec.active_h + << "@" << spec.refresh_hz << "Hz not found in display mode list. " + << "Use CRU to add it first."; + return false; + } + + LOG() << "DisplayTimingHelper::TryCruFallback: mode found, switching"; + + auto posSnapshot = SnapshotDisplayPositions(); + + DEVMODEW dm{}; + dm.dmSize = sizeof(dm); + dm.dmPelsWidth = spec.active_w; + dm.dmPelsHeight = spec.active_h; + dm.dmDisplayFrequency = static_cast(spec.refresh_hz + 0.5f); + dm.dmBitsPerPel = 32; + dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_BITSPERPEL; + + LONG result = ChangeDisplaySettingsExW( + device_name.c_str(), &dm, + nullptr, + CDS_FULLSCREEN, + nullptr); + + if (result != DISP_CHANGE_SUCCESSFUL) { + LOG() << "DisplayTimingHelper::TryCruFallback: ChangeDisplaySettingsExW failed result=" + << result; + return false; + } + + WaitForModesetSettle(500); + RestoreDisplayPositions(posSnapshot); + + // Save state for revert. + auto* state = new CruState(); + state->device_name = device_name; + state->original_mode = original; + backend_state_ = state; + + LOG() << "DisplayTimingHelper::TryCruFallback: successfully switched to " + << spec.active_w << "x" << spec.active_h + << "@" << spec.refresh_hz << "Hz"; + return true; +} + + +void DisplayTimingHelper::RevertCruFallback() +{ + if (!backend_state_) return; + auto* state = static_cast(backend_state_); + + LOG() << "DisplayTimingHelper::RevertCruFallback: reverting to original mode"; + + auto posSnapshot = SnapshotDisplayPositions(); + + RestoreSavedMode(state->device_name, state->original_mode); + + WaitForModesetSettle(500); + RestoreDisplayPositions(posSnapshot); +} + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/display_timing_helper.h b/vrto3d/src/presenter/display_timing_helper.h new file mode 100644 index 00000000..ee9177ce --- /dev/null +++ b/vrto3d/src/presenter/display_timing_helper.h @@ -0,0 +1,90 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + + +#include +#include + +#include "vrto3dlib/stereo_config.h" + +namespace vrto3d { + +// --------------------------------------------------------------------------- +// DisplayTimingHelper — applies and reverts custom display timings for +// HDMI 1.4 frame-packing modes. +// +// Strategy priority: +// 1. NVIDIA — NvAPI_DISP_TryCustomDisplay (delay-loaded, safe on non-NV) +// 2. AMD — ADL_Display_ModeTimingOverride_Set (delay-loaded) +// 3. Fallback — ChangeDisplaySettingsExW (for CRU pre-configured modes) +// +// Intel is intentionally not implemented: IGCL's ctlGetSetCustomMode only +// takes (width, height) and leaves timing derivation to the driver, so it +// can't produce HDMI 1.4 frame-pack timing on its own. Intel users should +// pre-configure via CRU and hit the CRU fallback. +// +// All three paths are non-fatal — if the modeset fails the caller can still +// create a window at the current desktop resolution and render TaB into it +// (the user just won't get the HDMI 3D InfoFrame). +// --------------------------------------------------------------------------- +class DisplayTimingHelper { +public: + DisplayTimingHelper() = default; + ~DisplayTimingHelper(); + + // Apply frame-pack timing for the given OutputMode on the monitor + // identified by display_index (0 = primary). Returns true on success. + // On failure the display is left in its original mode. + bool Apply(const FramePackTimingSpec& spec, + int32_t display_index); + + // Revert to the original display mode. Safe to call even if Apply was + // never called or failed. + void Revert(); + + // Was the custom timing successfully applied? + bool IsActive() const { return active_; } + + // Which backend was used? + enum class Backend { None, Nvidia, Amd, CruFallback }; + Backend GetBackend() const { return backend_; } + +private: + bool TryNvidia(const FramePackTimingSpec& spec, int32_t display_index); + bool TryAmd(const FramePackTimingSpec& spec, int32_t display_index); + bool TryCruFallback(const FramePackTimingSpec& spec, int32_t display_index); + + void RevertNvidia(); + void RevertAmd(); + void RevertCruFallback(); + + bool active_ = false; + Backend backend_ = Backend::None; + + // Saved state for revert — backend-specific opaque storage. + // NVIDIA: display IDs used with TryCustomDisplay + original DEVMODE. + // AMD: adapter/display indices + original DEVMODE. + // CRU fallback: device name + original DEVMODE. + struct NvidiaState; + struct AmdState; + struct CruState; + void* backend_state_ = nullptr; // owned, type depends on backend_ +}; + +} // namespace vrto3d + diff --git a/vrto3d/src/presenter/display_utils.h b/vrto3d/src/presenter/display_utils.h new file mode 100644 index 00000000..a51efa6a --- /dev/null +++ b/vrto3d/src/presenter/display_utils.h @@ -0,0 +1,110 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include +#include +#include + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +namespace vrto3d { +namespace display_utils { + +struct DisplayPositionSnapshot { + std::wstring deviceName; + POINTL position; +}; + +inline std::vector SnapshotDisplayPositions() +{ + std::vector snapshots; + DISPLAY_DEVICEW dd = {}; + dd.cb = sizeof(dd); + for (DWORD i = 0; EnumDisplayDevicesW(nullptr, i, &dd, 0); ++i) { + if (!(dd.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP)) { + dd = {}; dd.cb = sizeof(dd); continue; + } + DEVMODEW dm = {}; + dm.dmSize = sizeof(dm); + if (EnumDisplaySettingsExW(dd.DeviceName, ENUM_CURRENT_SETTINGS, &dm, 0)) { + DisplayPositionSnapshot snap; + snap.deviceName = dd.DeviceName; + snap.position = dm.dmPosition; + snapshots.push_back(snap); + } + dd = {}; dd.cb = sizeof(dd); + } + return snapshots; +} + +inline void RestoreDisplayPositions(const std::vector& snapshots) +{ + if (snapshots.empty()) return; + + UINT32 numPaths = 0, numModes = 0; + if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &numPaths, &numModes) != ERROR_SUCCESS) + return; + + std::vector paths(numPaths); + std::vector modes(numModes); + if (QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &numPaths, paths.data(), + &numModes, modes.data(), nullptr) != ERROR_SUCCESS) + return; + paths.resize(numPaths); + modes.resize(numModes); + + std::unordered_map posMap; + for (const auto& snap : snapshots) posMap[snap.deviceName] = snap.position; + + for (auto& mode : modes) { + if (mode.infoType != DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE) continue; + DISPLAYCONFIG_SOURCE_DEVICE_NAME srcName = {}; + srcName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; + srcName.header.size = sizeof(srcName); + srcName.header.adapterId = mode.adapterId; + srcName.header.id = mode.id; + if (DisplayConfigGetDeviceInfo(&srcName.header) != ERROR_SUCCESS) continue; + auto it = posMap.find(srcName.viewGdiDeviceName); + if (it == posMap.end()) continue; + mode.sourceMode.position.x = it->second.x; + mode.sourceMode.position.y = it->second.y; + } + + SetDisplayConfig(numPaths, paths.data(), numModes, modes.data(), + SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_NO_OPTIMIZATION); +} + +// Pump messages and sleep to let a modeset settle. +inline void WaitForModesetSettle(DWORD timeoutMs) +{ + DWORD start = GetTickCount(); + while (GetTickCount() - start < timeoutMs) { + MSG msg; + while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + Sleep(10); + } +} + +} // namespace display_utils +} // namespace vrto3d diff --git a/vrto3d/src/presenter/leiasr_presenter.cpp b/vrto3d/src/presenter/leiasr_presenter.cpp new file mode 100644 index 00000000..4f8362af --- /dev/null +++ b/vrto3d/src/presenter/leiasr_presenter.cpp @@ -0,0 +1,874 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ + + +#define NOMINMAX +#include // must precede windows.h +#include +#pragma comment(lib, "ws2_32.lib") + +#include "leiasr_presenter.h" + +#include +#include +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include "dx11_renderer.h" +#include "focus_policy.h" +#include "hmd_device_driver.h" +#include "vrto3dlib/debug_log.hpp" +#include "vrto3dlib/win32_helper.hpp" +#include "one_euro_filter.h" + +// LeiaSR SDK headers +#include "sr/management/srcontext.h" +#include "sr/utility/exception.h" +#include "sr/weaver/dx11weaver.h" +#include "sr/sense/display/switchablehint.h" +#include "sr/sense/headtracker/headposetracker.h" +#include "sr/sense/core/inputstream.h" + +using Microsoft::WRL::ComPtr; + +namespace { + +// Helper: invokes SRContext::create inside an SEH __try block so a missing +// LeiaSR / OpenCV DLL (delay-load resolution failure) returns nullptr instead +// of propagating a structured exception that would crash vrserver. We also +// catch SR's C++ ServerNotAvailableException at the C++ layer above. +// +// Two-tier protection: +// 1. SEH __except: catches VcppException(MOD_NOT_FOUND / PROC_NOT_FOUND) +// raised by the delay-load helper if the DLL or import isn't resolvable. +// 2. C++ try/catch (in caller): catches SR's runtime exceptions like +// ServerNotAvailableException when DLLs are present but service isn't. +SR::SRContext* TryCreateSRContextSEH(bool* dll_failure) +{ + *dll_failure = false; + __try { + return SR::SRContext::create(); + } __except ( + (GetExceptionCode() == VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND) || + GetExceptionCode() == VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND)) + ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH + ) { + *dll_failure = true; + return nullptr; + } +} + +} // namespace + +namespace vrto3d { + +// --------------------------------------------------------------------------- +// Head-pose listener: receives SR_headPose frames on an SDK-owned thread, +// stores the latest sample under a mutex for the tracking thread to consume. +// --------------------------------------------------------------------------- +class LeiaSrHeadPoseListener : public SR::HeadPoseListener { + std::mutex mutex_; + float pos_[3] = { 0.0f, 0.0f, 600.0f }; + float orient_[3] = { 0.0f, 0.0f, 0.0f }; + uint64_t frame_time_ = 0; + bool has_data_ = false; + +public: + SR::InputStream stream; + + void accept(const SR_headPose& frame) override { + std::lock_guard lock(mutex_); + pos_[0] = static_cast(frame.position.x); + pos_[1] = static_cast(frame.position.y); + pos_[2] = static_cast(frame.position.z); + orient_[0] = static_cast(frame.orientation.x); + orient_[1] = static_cast(frame.orientation.y); + orient_[2] = static_cast(frame.orientation.z); + frame_time_ = frame.time; + has_data_ = true; + } + + bool get(float pos[3], float orient[3], uint64_t& time_us) { + std::lock_guard lock(mutex_); + if (!has_data_) return false; + pos[0] = pos_[0]; pos[1] = pos_[1]; pos[2] = pos_[2]; + orient[0] = orient_[0]; orient[1] = orient_[1]; orient[2] = orient_[2]; + time_us = frame_time_; + return true; + } +}; + +// --------------------------------------------------------------------------- +// OpenTrack UDP sender: 48-byte (6 doubles) little-endian XYZ + YPR packet +// to localhost:open_track_port. Non-blocking, fire-and-forget. +// --------------------------------------------------------------------------- +class LeiaSrOpenTrackSender { +public: +#pragma pack(push, 1) + struct Packet { + double X; + double Y; + double Z; + double Yaw; + double Pitch; + double Roll; + }; +#pragma pack(pop) + + bool init(int port, const char* host = "127.0.0.1") { + WSADATA wsa; + if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false; + wsa_started_ = true; + + sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock_ == INVALID_SOCKET) return false; + + u_long nonblock = 1; + ioctlsocket(sock_, FIONBIO, &nonblock); + + dest_.sin_family = AF_INET; + dest_.sin_port = htons(static_cast(port)); + inet_pton(AF_INET, host, &dest_.sin_addr); + + initialized_ = true; + return true; + } + + bool send(double x, double y, double z, double yaw, double pitch, double roll) { + if (!initialized_) return false; + Packet pkt = { x, y, z, yaw, pitch, roll }; + int sent = sendto(sock_, reinterpret_cast(&pkt), sizeof(pkt), 0, + reinterpret_cast(&dest_), sizeof(dest_)); + return sent == sizeof(pkt); + } + + bool sendIdentity() { return send(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); } + + void shutdown() { + if (sock_ != INVALID_SOCKET) { + sendIdentity(); // park consumer at neutral pose so it doesn't freeze + closesocket(sock_); + sock_ = INVALID_SOCKET; + } + if (wsa_started_) { + WSACleanup(); + wsa_started_ = false; + } + initialized_ = false; + } + + ~LeiaSrOpenTrackSender() { shutdown(); } + +private: + SOCKET sock_ = INVALID_SOCKET; + sockaddr_in dest_ = {}; + bool initialized_ = false; + bool wsa_started_ = false; +}; + +// --------------------------------------------------------------------------- +// Track pipeline: SR head pose -> One-Euro filter -> sensitivity/clamp/deadzone +// -> mode-specific axis masking. Mirrors the external bridge's TrackPipeline, +// configured from StereoDisplayDriverConfiguration.sr_* fields. +// --------------------------------------------------------------------------- +class LeiaSrTrackPipeline { +public: + enum class Mode { XYZ_YawPitch = 1, XYZ = 2, YawPitch = 3, Full6DOF = 4, YawPitchRoll = 5 }; + + struct Result { + float yaw_deg = 0, pitch_deg = 0, roll_deg = 0; + float pos_x_cm = 0, pos_y_cm = 0, pos_z_cm = 0; + bool valid = false; + }; + + static Mode ParseMode(const std::string& s) { + if (s == "XYZ") return Mode::XYZ; + if (s == "YawPitch") return Mode::YawPitch; + if (s == "Full6DOF") return Mode::Full6DOF; + if (s == "YawPitchRoll") return Mode::YawPitchRoll; + return Mode::XYZ_YawPitch; // default + "XYZ_YawPitch" + } + + explicit LeiaSrTrackPipeline(const StereoDisplayDriverConfiguration& cfg) { + const float freq = 60.0f; + f_yaw_ = OneEuroFilter(freq, cfg.sr_filter_rot_mincutoff, cfg.sr_filter_rot_beta); + f_pitch_ = OneEuroFilter(freq, cfg.sr_filter_rot_mincutoff, cfg.sr_filter_rot_beta); + f_roll_ = OneEuroFilter(freq, cfg.sr_filter_rot_mincutoff, cfg.sr_filter_rot_beta); + f_x_ = OneEuroFilter(freq, cfg.sr_filter_pos_mincutoff, cfg.sr_filter_pos_beta); + f_y_ = OneEuroFilter(freq, cfg.sr_filter_pos_mincutoff, cfg.sr_filter_pos_beta); + f_z_ = OneEuroFilter(freq, cfg.sr_filter_pos_mincutoff, cfg.sr_filter_pos_beta); + Apply(cfg); + } + + // Push live cfg into the pipeline. Cheap — no allocation or reset of the + // One Euro filter state. Safe to call every iteration so OSD-tuned + // sr_filter_* / sr_sens_* / sr_max_* changes propagate without rebuild. + void Apply(const StereoDisplayDriverConfiguration& cfg) { + mode_ = ParseMode(cfg.sr_track_mode); + deadzone_deg_ = cfg.sr_angle_deadzone_deg; + sens_yaw_ = cfg.sr_sens_yaw; + sens_pitch_ = cfg.sr_sens_pitch; + sens_roll_ = cfg.sr_sens_roll; + max_yaw_ = cfg.sr_max_yaw; + max_pitch_ = cfg.sr_max_pitch; + max_roll_ = cfg.sr_max_roll; + + f_yaw_.setMinCutoff(cfg.sr_filter_rot_mincutoff); + f_yaw_.setBeta(cfg.sr_filter_rot_beta); + f_pitch_.setMinCutoff(cfg.sr_filter_rot_mincutoff); + f_pitch_.setBeta(cfg.sr_filter_rot_beta); + f_roll_.setMinCutoff(cfg.sr_filter_rot_mincutoff); + f_roll_.setBeta(cfg.sr_filter_rot_beta); + f_x_.setMinCutoff(cfg.sr_filter_pos_mincutoff); + f_x_.setBeta(cfg.sr_filter_pos_beta); + f_y_.setMinCutoff(cfg.sr_filter_pos_mincutoff); + f_y_.setBeta(cfg.sr_filter_pos_beta); + f_z_.setMinCutoff(cfg.sr_filter_pos_mincutoff); + f_z_.setBeta(cfg.sr_filter_pos_beta); + } + + // Drop accumulated filter state — the next process() call re-initializes + // both LP filters from the new sample. Use on a use_open_track edge so + // resumption doesn't see a giant dx/dt spike from the long idle gap. + void ResetFilters() { + f_yaw_.reset(); f_pitch_.reset(); f_roll_.reset(); + f_x_.reset(); f_y_.reset(); f_z_.reset(); + } + + // Set the current head orientation as the neutral zero. Stores per-axis + // degree offsets that process() then ADDS to the filtered values before + // sensitivity scaling — mirrors the Simulated-Reality-OpenTrack-Bridge + // Ctrl+X calibrate behavior. Position offsets are not adjusted. + void Calibrate(float orient_x_rad, float orient_y_rad, float orient_z_rad) { + const float to_deg = 180.0f / static_cast(M_PI); + // Apply the same axis sign conventions process() uses (only yaw is + // pre-inverted to match the OpenTrack receiver). + const float pitch = orient_x_rad * to_deg; + const float yaw = -(orient_y_rad * to_deg); + const float roll = orient_z_rad * to_deg; + yaw_offset_ = -yaw; + pitch_offset_ = -pitch; + roll_offset_ = -roll; + } + + Result process(float pos_x_mm, float pos_y_mm, float pos_z_mm, + float orient_x_rad, float orient_y_rad, float orient_z_rad, + float timestamp_sec) + { + Result r; + if (!std::isfinite(orient_x_rad) || !std::isfinite(orient_y_rad) || !std::isfinite(orient_z_rad)) { + return r; + } + + const float to_deg = 180.0f / static_cast(M_PI); + float pitch_raw = orient_x_rad * to_deg; + float yaw_raw = orient_y_rad * to_deg; + float roll_raw = orient_z_rad * to_deg; + + float yaw_f = f_yaw_.filter(yaw_raw, timestamp_sec); + float pitch_f = f_pitch_.filter(pitch_raw, timestamp_sec); + float roll_f = f_roll_.filter(roll_raw, timestamp_sec); + + // OpenTrack convention to match the consumer (MockControllerDeviceDriver + // ::OpenTrackThread negates X and Yaw on receive): pre-invert yaw here + // so the round-trip matches an external bridge running in OpenTrack + // mode. Roll is NOT negated — the receiver passes it straight into the + // quaternion, so negating here would mirror the roll direction. + yaw_f = -yaw_f; + + r.yaw_deg = std::clamp((yaw_f + yaw_offset_) * sens_yaw_, -max_yaw_, max_yaw_); + r.pitch_deg = std::clamp((pitch_f + pitch_offset_) * sens_pitch_, -max_pitch_, max_pitch_); + r.roll_deg = std::clamp((roll_f + roll_offset_) * sens_roll_, -max_roll_, max_roll_); + + if (std::fabs(r.yaw_deg) < deadzone_deg_) r.yaw_deg = 0.0f; + if (std::fabs(r.pitch_deg) < deadzone_deg_) r.pitch_deg = 0.0f; + if (std::fabs(r.roll_deg) < deadzone_deg_) r.roll_deg = 0.0f; + + // Position passthrough (mm -> cm), X inverted to match OpenTrack. + float x_cm = -pos_x_mm / 10.0f; + float y_cm = pos_y_mm / 10.0f; + float z_cm = pos_z_mm / 10.0f; + // Run position through filter for stability. + r.pos_x_cm = f_x_.filter(x_cm, timestamp_sec); + r.pos_y_cm = f_y_.filter(y_cm, timestamp_sec); + r.pos_z_cm = f_z_.filter(z_cm, timestamp_sec); + + switch (mode_) { + case Mode::XYZ_YawPitch: + r.roll_deg = 0.0f; + break; + case Mode::XYZ: + r.yaw_deg = r.pitch_deg = r.roll_deg = 0.0f; + break; + case Mode::YawPitch: + r.pos_x_cm = r.pos_y_cm = r.pos_z_cm = 0.0f; + r.roll_deg = 0.0f; + break; + case Mode::Full6DOF: + break; // everything passes through + case Mode::YawPitchRoll: + r.pos_x_cm = r.pos_y_cm = r.pos_z_cm = 0.0f; + break; + } + + r.valid = true; + return r; + } + +private: + Mode mode_ = Mode::XYZ_YawPitch; + float deadzone_deg_ = 0.2f; + float sens_yaw_ = 1, sens_pitch_ = 1, sens_roll_ = 1; + float max_yaw_ = 70, max_pitch_ = 70, max_roll_ = 70; + OneEuroFilter f_yaw_, f_pitch_, f_roll_; + OneEuroFilter f_x_, f_y_, f_z_; + float yaw_offset_ = 0.0f; + float pitch_offset_ = 0.0f; + float roll_offset_ = 0.0f; +}; + + +// Out-of-line ctor/dtor: the unique_ptr<> members above wrap forward-declared +// types in the header. Defining them here, where the full types are visible, +// keeps presenter_factory.cpp (and any other includer) free of the SR/winsock +// headers. +LeiaSrPresenter::LeiaSrPresenter() = default; +LeiaSrPresenter::~LeiaSrPresenter() { Shutdown(); } + + +bool LeiaSrPresenter::CreateSwapChain(Dx11Renderer& renderer) +{ + ID3D11Device* dev = renderer.Device(); + + ComPtr dxgi_dev; + if (FAILED(dev->QueryInterface(IID_PPV_ARGS(&dxgi_dev)))) return false; + ComPtr adapter; + if (FAILED(dxgi_dev->GetAdapter(&adapter))) return false; + ComPtr factory; + if (FAILED(adapter->GetParent(IID_PPV_ARGS(&factory)))) return false; + + // Flip-model + waitable swap chain. The waitable handle gates pacing on + // the window thread, so Present(0, 0) returns immediately — tear-free + // without blocking inside Present (which would stall the compositor via + // the shared context_mutex_). + DXGI_SWAP_CHAIN_DESC1 scd{}; + scd.Width = window_->Width(); + scd.Height = window_->Height(); + scd.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + scd.SampleDesc = { 1, 0 }; + scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + scd.BufferCount = 2; + scd.Scaling = DXGI_SCALING_STRETCH; + scd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + scd.AlphaMode = DXGI_ALPHA_MODE_IGNORE; + scd.Flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT; + + HWND hwnd = static_cast(window_->NativeHandle()); + HRESULT hr = factory->CreateSwapChainForHwnd(dev, hwnd, &scd, nullptr, nullptr, &swapchain_); + if (FAILED(hr)) { + LOG() << "LeiaSrPresenter: CreateSwapChainForHwnd failed hr=0x" << std::hex << hr; + return false; + } + factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_WINDOW_CHANGES); + + if (FAILED(swapchain_.As(&swapchain2_)) || !swapchain2_) { + LOG() << "LeiaSrPresenter: QueryInterface(IDXGISwapChain2) failed"; + return false; + } + swapchain2_->SetMaximumFrameLatency(1); + frame_latency_wait_ = swapchain2_->GetFrameLatencyWaitableObject(); + if (!frame_latency_wait_) { + LOG() << "LeiaSrPresenter: GetFrameLatencyWaitableObject returned null"; + } + + ComPtr bb; + if (FAILED(swapchain_->GetBuffer(0, IID_PPV_ARGS(&bb)))) return false; + if (FAILED(dev->CreateRenderTargetView(bb.Get(), nullptr, &swapchain_rtv_))) return false; + + swap_width_ = window_->Width(); + swap_height_ = window_->Height(); + LOG() << "LeiaSrPresenter: swapchain " << swap_width_ << "x" << swap_height_ + << " (FLIP_DISCARD, 2 buffers, waitable=" + << (frame_latency_wait_ ? "yes" : "no") << ")"; + return true; +} + + +bool LeiaSrPresenter::Init(Dx11Renderer& renderer, + const StereoDisplayDriverConfiguration& cfg, + const FocusContext& focus) +{ + renderer_ = &renderer; + eye_swap_ = cfg.eye_swap; + auto_focus_ = cfg.auto_focus; + focus_ = focus; + + // Cache tracking config so the head-tracking thread can read it without + // touching the caller's StereoDisplayDriverConfiguration after Init returns. + tracking_enabled_ = cfg.use_open_track; + tracking_port_ = cfg.open_track_port; + tracking_cfg_ = cfg; + + // LeiaSR weaver targets a single SR display — never spans two monitors. + platform::MonitorInfo primary{}, secondary{}; + if (!platform::ResolveTargetMonitors(cfg.display_index, false, primary, secondary)) { + LOG() << "LeiaSrPresenter::Init: ResolveTargetMonitors failed"; + return false; + } + + window_stop_.store(false); + window_ready_.store(false); + window_failed_.store(false); + window_thread_ = std::thread(&LeiaSrPresenter::WindowThreadLoop, this, &renderer, + primary, secondary); + + for (int i = 0; i < 500; ++i) { + if (window_ready_.load() || window_failed_.load()) break; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + if (window_failed_.load() || !window_ready_.load()) { + LOG() << "LeiaSrPresenter::Init: window thread failed to come up"; + Shutdown(); + return false; + } + + LOG() << "LeiaSrPresenter: ready, target_monitor=" << primary.device_name + << " " << (window_ ? window_->Width() : 0) << "x" << (window_ ? window_->Height() : 0); + + focus_stop_.store(false); + focus_thread_ = std::thread(&LeiaSrPresenter::FocusThreadLoop, this); + return true; +} + + +void LeiaSrPresenter::WindowThreadLoop(Dx11Renderer* renderer, + platform::MonitorInfo primary, + platform::MonitorInfo secondary) +{ + platform::EnablePerMonitorV2DpiAwareness(); + + window_ = platform::CreatePresentWindow( + primary, + (secondary.width > 0 ? &secondary : nullptr), + "VRto3D-LeiaSR"); + if (!window_) { + LOG() << "LeiaSrPresenter: CreatePresentWindow failed"; + window_failed_.store(true); + return; + } + + if (!CreateSwapChain(*renderer)) { + LOG() << "LeiaSrPresenter: swapchain init failed"; + window_failed_.store(true); + window_.reset(); + return; + } + + // Initial black so DWM has something to composite. + { + ID3D11DeviceContext* ctx = renderer->Context(); + const float clear[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; + ctx->ClearRenderTargetView(swapchain_rtv_.Get(), clear); + ID3D11RenderTargetView* null_rtv[] = { nullptr }; + ctx->OMSetRenderTargets(1, null_rtv, nullptr); + swapchain_rtv_.Reset(); // release ref to back buffer before flip + swapchain_->Present(0, 0); + } + + // Build SR context + weaver. Block briefly waiting for SRService. + HWND hwnd = static_cast(window_->NativeHandle()); + { + const ULONGLONG t0 = GetTickCount64(); + const ULONGLONG max_wait_ms = 5000; + bool dll_missing = false; + while (!sr_context_) { + try { + sr_context_ = TryCreateSRContextSEH(&dll_missing); + if (dll_missing) break; // delay-load failure — no point retrying + if (sr_context_) break; // success + // Shouldn't reach here without exception, but bail out if we do. + break; + } catch (const SR::ServerNotAvailableException&) { + if (GetTickCount64() - t0 > max_wait_ms) break; + Sleep(100); + } catch (...) { + break; + } + } + if (dll_missing) { + LOG() << "LeiaSrPresenter: LeiaSR runtime DLL(s) not resolvable. " + << "Install the LeiaSR / SR Platform runtime, or pick a different output_mode."; + window_failed_.store(true); + window_.reset(); + return; + } + if (!sr_context_) { + LOG() << "LeiaSrPresenter: SRContext::create failed (SRService not running?)"; + window_failed_.store(true); + window_.reset(); + return; + } + + WeaverErrorCode wec = SR::CreateDX11Weaver(sr_context_, renderer->Context(), hwnd, &sr_weaver_); + if (wec != WeaverErrorCode::WeaverSuccess || !sr_weaver_) { + LOG() << "LeiaSrPresenter: CreateDX11Weaver failed code=" << static_cast(wec); + window_failed_.store(true); + window_.reset(); + return; + } + // Configure weaver. Input is sRGB R8G8B8A8 from compositor; backbuffer + // is _UNORM (linear). Tell the weaver to convert sRGB→Linear on read + // and Linear→sRGB on write so colors come out right. + sr_weaver_->setShaderSRGBConversion(true, true); + sr_weaver_->setLatencyInFrames(1); + sr_weaver_->setContext(renderer->Context()); + + // Always register the SR head tracker as a Sense before initialize(). + // The use_open_track flag is checked at consumption time (see + // HeadTrackingThreadLoop) so the OSD can toggle it live without + // needing a presenter restart. + try { + sr_head_tracker_ = SR::HeadPoseTracker::create(*sr_context_); + head_listener_ = std::make_unique(); + head_listener_->stream.set(sr_head_tracker_->openHeadPoseStream(head_listener_.get())); + } catch (const std::exception& e) { + LOG() << "LeiaSrPresenter: HeadPoseTracker::create failed: " << e.what(); + sr_head_tracker_ = nullptr; + head_listener_.reset(); + } + + // Activate sense streams. + sr_context_->initialize(); + lens_hint_ = SR::SwitchableLensHint::create(*sr_context_); + sr_initialized_ = true; + LOG() << "LeiaSrPresenter: SRContext initialized; weaver ready"; + } + + window_ready_.store(true); + + // Always spawn the head-tracking sender thread once SR is up. The + // per-frame use_open_track flag is checked inside the loop so toggling + // it via the OSD takes effect immediately. + if (head_listener_) { + ot_sender_ = std::make_unique(); + if (!ot_sender_->init(tracking_port_)) { + LOG() << "LeiaSrPresenter: OpenTrack UDP sender init failed; head tracking disabled"; + ot_sender_.reset(); + } else { + track_pipeline_ = std::make_unique(tracking_cfg_); + tracking_stop_.store(false); + tracking_thread_ = std::thread(&LeiaSrPresenter::HeadTrackingThreadLoop, this); + LOG() << "LeiaSrPresenter: head tracking sender ready (UDP -> 127.0.0.1:" << tracking_port_ + << ", mode=" << tracking_cfg_.sr_track_mode + << ", initial use_open_track=" << (tracking_enabled_ ? "true" : "false") << ")"; + } + } + + while (!window_stop_.load(std::memory_order_relaxed)) { + // DWM signals frame_latency_wait_ once per display refresh; gates + // pacing without blocking inside Present. + if (frame_latency_wait_) { + WaitForSingleObjectEx(frame_latency_wait_, 100, TRUE); + } + renderer->WaitAndDrawPending(33); + if (window_) window_->PollEvents(); + } + + // Stop head tracking before tearing down SR. Stream must be closed before + // the tracker / context is destroyed. + tracking_stop_.store(true); + if (tracking_thread_.joinable()) tracking_thread_.join(); + head_listener_.reset(); // ~InputStream calls stopListening for us + sr_head_tracker_ = nullptr; // managed by SRContext; do not delete + if (ot_sender_) { ot_sender_->shutdown(); ot_sender_.reset(); } + track_pipeline_.reset(); + + // Tear down SR resources on this thread (they were created here). + if (sr_weaver_) { + sr_weaver_->destroy(); + sr_weaver_ = nullptr; + } + lens_hint_ = nullptr; // managed by SRContext; do not delete + if (sr_context_) { + delete sr_context_; + sr_context_ = nullptr; + } + sr_initialized_ = false; + + input_srv_.Reset(); + if (frame_latency_wait_) { + CloseHandle(frame_latency_wait_); + frame_latency_wait_ = nullptr; + } + swapchain_rtv_.Reset(); + swapchain2_.Reset(); + swapchain_.Reset(); + window_.reset(); + LOG() << "LeiaSrPresenter: window thread exited"; +} + + +void LeiaSrPresenter::RecordComposite(ID3D11Texture2D* sbs_input) +{ + if (!swapchain_ || !sbs_input || !sr_weaver_ || !renderer_) return; + + ID3D11Device* dev = renderer_->Device(); + ID3D11DeviceContext* ctx = renderer_->Context(); + + // FLIP_DISCARD rotates buffer 0 every Present, so refresh the RTV each + // frame against the current back buffer. Must happen BEFORE weave() so + // the weaver writes to the live buffer. + ComPtr bb; + if (FAILED(swapchain_->GetBuffer(0, IID_PPV_ARGS(&bb))) || !bb) return; + swapchain_rtv_.Reset(); + if (FAILED(dev->CreateRenderTargetView(bb.Get(), nullptr, &swapchain_rtv_))) return; + + D3D11_TEXTURE2D_DESC td{}; + sbs_input->GetDesc(&td); + + // Rebuild SRV + retell weaver only when input changes. + if (sbs_input != cached_input_ptr_ + || td.Width != cached_input_w_ + || td.Height != cached_input_h_ + || td.Format != cached_input_fmt_) { + D3D11_SHADER_RESOURCE_VIEW_DESC sv{}; + sv.Format = td.Format; + sv.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + sv.Texture2D.MipLevels = static_cast(-1); + sv.Texture2D.MostDetailedMip = 0; + ComPtr new_srv; + if (FAILED(dev->CreateShaderResourceView(sbs_input, &sv, &new_srv))) return; + input_srv_ = new_srv; + cached_input_ptr_ = sbs_input; + cached_input_w_ = td.Width; + cached_input_h_ = td.Height; + cached_input_fmt_ = td.Format; + + // Weaver expects per-eye dimensions (input is treated as side-by-side). + sr_weaver_->setInputViewTexture(input_srv_.Get(), + static_cast(td.Width / 2), + static_cast(td.Height), + td.Format); + LOG() << "LeiaSrPresenter: input view texture (re)bound " + << td.Width << "x" << td.Height << " fmt=" << td.Format; + } + + D3D11_VIEWPORT vp{}; + vp.Width = static_cast(swap_width_); + vp.Height = static_cast(swap_height_); + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + + ID3D11RenderTargetView* rtv = swapchain_rtv_.Get(); + const float clear[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; + ctx->ClearRenderTargetView(rtv, clear); + ctx->OMSetRenderTargets(1, &rtv, nullptr); + ctx->RSSetViewports(1, &vp); + + sr_weaver_->weave(); + + // Release RTV + back-buffer ref before Present (FLIP requirement). + ID3D11RenderTargetView* null_rtv[] = { nullptr }; + ctx->OMSetRenderTargets(1, null_rtv, nullptr); + swapchain_rtv_.Reset(); +} + + +void LeiaSrPresenter::Present() +{ + if (!swapchain_) return; + + // Flip-model + waitable: SyncInterval=0 queues the flip and returns + // immediately; DWM consumes it at the next vblank. Pacing comes from the + // waitable handle on the window thread. + HRESULT hr = swapchain_->Present(0, 0); + if (FAILED(hr)) { + static std::atomic logged{false}; + bool e = false; + if (logged.compare_exchange_strong(e, true)) { + LOG() << "LeiaSrPresenter: Present failed hr=0x" << std::hex << hr; + } + } +} + + +void LeiaSrPresenter::FocusThreadLoop() +{ + using namespace std::chrono_literals; + bool was_on_top = false; + int reassert_counter = 0; + FocusLatchState focus_latch; // auto-focus per-PID latch (focus_policy.h) + + while (!focus_stop_.load(std::memory_order_relaxed)) { + if (!window_) break; + + // LeiaSR runs on a single SR display — no multi-display nudge. + + // Shared decision (see focus_policy.h) — kept identical to + // WindowPresenter / the Linux VkRenderer focus block so they can't + // drift. + FocusInputs fi; + fi.is_on_top = focus_.is_on_top && focus_.is_on_top->load(); + fi.man_on_top = focus_.man_on_top && focus_.man_on_top->load(); + fi.app_pid = focus_.app_pid ? focus_.app_pid->load() : 0; + fi.auto_focus = focus_.auto_focus ? focus_.auto_focus->load() : auto_focus_; + fi.app_running = platform::IsProcessRunning(fi.app_pid); + fi.force_on_top = false; // topmost-model window: an open OSD is made + // visible/clickable via ApplyMenuVisibility, + // not by forcing topmost. + bool set_is = false, set_man = false; + const bool want_on_top = + ComputeWantOnTop(fi, focus_latch, &set_is, &set_man); + if (set_is && focus_.is_on_top) focus_.is_on_top->store(true); + if (set_man && focus_.man_on_top) focus_.man_on_top->store(true); + const uint32_t pid = fi.app_pid; + + if (want_on_top != was_on_top) { + HWND vr_hwnd = static_cast(window_->NativeHandle()); + if (want_on_top) { + window_->BringToTop(); + if (lens_hint_) lens_hint_->enable(); + if (vr_hwnd) { + // WS_EX_LAYERED stays on for the window's lifetime once + // set; only WS_EX_TRANSPARENT is toggled. Tearing down the + // layered surface on a DPI-scaled display can leave it + // recreated at virtualized dimensions. + LONG_PTR ex = GetWindowLongPtrW(vr_hwnd, GWL_EXSTYLE); + SetWindowLongPtrW(vr_hwnd, GWL_EXSTYLE, + ex | WS_EX_LAYERED | WS_EX_TRANSPARENT); + SetLayeredWindowAttributes(vr_hwnd, 0, 255, LWA_ALPHA); + } + // When SteamVR is launched by the app, the game window may + // not exist yet at the +8s mark, or SteamVR's bring-up + // (vrmonitor / status window) may grab foreground after + // our first ForceFocus. Run a watch loop that re-asserts + // focus whenever the foreground drifts off the game. + std::thread([pid, man_on_top = focus_.man_on_top]() { + for (int i = 0; i < 15; ++i) { + if (man_on_top && !man_on_top->load()) return; + HWND game_hwnd = GetHWNDFromPID(pid); + if (game_hwnd && GetForegroundWindow() != game_hwnd) { + ForceFocus(game_hwnd, + GetCurrentThreadId(), + GetWindowThreadProcessId(game_hwnd, nullptr)); + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + }).detach(); + } else { + window_->ReleaseTopmost(); + if (lens_hint_) lens_hint_->disable(); + if (vr_hwnd) { + LONG_PTR ex = GetWindowLongPtrW(vr_hwnd, GWL_EXSTYLE); + SetWindowLongPtrW(vr_hwnd, GWL_EXSTYLE, + ex & ~WS_EX_TRANSPARENT); + } + } + was_on_top = want_on_top; + reassert_counter = 0; + } else if (want_on_top && ++reassert_counter >= 20) { + reassert_counter = 0; + window_->BringToTop(); + } + + std::this_thread::sleep_for(50ms); + } +} + + +void LeiaSrPresenter::HeadTrackingThreadLoop() +{ + using namespace std::chrono; + if (!head_listener_ || !ot_sender_ || !track_pipeline_) return; + + const auto t0 = steady_clock::now(); + float pos[3], orient[3]; + uint64_t time_us = 0; + + bool prev_use_ot = false; + while (!tracking_stop_.load(std::memory_order_relaxed)) { + // Live-poll use_open_track + the SR filter cfg so the OSD's + // "Enable OpenTrack" checkbox and the sr_filter_*/sens_*/max_* + // sliders take effect without a presenter restart. We drain the + // SR head-pose stream regardless to keep its queue from backing + // up — but skip the UDP send and reset the One Euro filters on + // the disabled→enabled edge so resumption doesn't see a giant + // dx/dt spike from the idle gap. sr_tracking_enabled mutes this + // built-in sender while leaving the OpenTrack receiver running, so + // an external source (OpenTrack app, VertoXR) can feed the port. + bool use_ot = true; + StereoDisplayDriverConfiguration live_cfg{}; + if (renderer_ && renderer_->Component()) { + live_cfg = renderer_->Component()->GetConfig(); + use_ot = live_cfg.use_open_track && live_cfg.sr_tracking_enabled; + } + + if (use_ot && !prev_use_ot && track_pipeline_) { + track_pipeline_->ResetFilters(); + } + prev_use_ot = use_ot; + + // Push the latest cfg into the pipeline every tick (cheap). + if (track_pipeline_) track_pipeline_->Apply(live_cfg); + + if (head_listener_->get(pos, orient, time_us)) { + // Apply pending calibrate using the most recent raw orientation. + if (calibrate_request_.exchange(false) && track_pipeline_) { + track_pipeline_->Calibrate(orient[0], orient[1], orient[2]); + LOG() << "LeiaSrPresenter: head pose calibrated to neutral"; + } + if (use_ot) { + const float ts = duration(steady_clock::now() - t0).count(); + auto r = track_pipeline_->process(pos[0], pos[1], pos[2], + orient[0], orient[1], orient[2], ts); + if (r.valid) { + ot_sender_->send(r.pos_x_cm, r.pos_y_cm, r.pos_z_cm, + r.yaw_deg, r.pitch_deg, r.roll_deg); + } + } + } + // ~120Hz polling — SR head pose typically arrives at 60Hz; this keeps + // jitter low without burning a core. + std::this_thread::sleep_for(milliseconds(8)); + } +} + + +void LeiaSrPresenter::RequestCalibrate() +{ + calibrate_request_.store(true); +} + +void LeiaSrPresenter::Shutdown() +{ + if (!window_thread_.joinable() && !window_) return; + LOG() << "LeiaSrPresenter: Shutdown called"; + + focus_stop_.store(true); + if (focus_thread_.joinable()) focus_thread_.join(); + + window_stop_.store(true); + if (window_thread_.joinable()) window_thread_.join(); + + renderer_ = nullptr; +} + +} // namespace vrto3d + diff --git a/vrto3d/src/presenter/leiasr_presenter.h b/vrto3d/src/presenter/leiasr_presenter.h new file mode 100644 index 00000000..b53601d2 --- /dev/null +++ b/vrto3d/src/presenter/leiasr_presenter.h @@ -0,0 +1,123 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + + +#include +#include +#include + +#include +#include +#include +#include + +#include "platform.h" +#include "presenter/output_presenter.h" +#include "vrto3dlib/stereo_config.h" + +// Forward decls — full headers only included in the .cpp to keep the SDK +// dependency contained. +namespace SR { class SRContext; class IDX11Weaver1; class SwitchableLensHint; class HeadPoseTracker; } + +namespace vrto3d { + +// Internal head-pose listener + UDP sender (defined in .cpp). Forward-declared +// here so the impl can stash unique_ptrs without leaking SR/winsock headers. +class LeiaSrHeadPoseListener; +class LeiaSrOpenTrackSender; +class LeiaSrTrackPipeline; + +class LeiaSrPresenter : public IOutputPresenter { +public: + LeiaSrPresenter(); + ~LeiaSrPresenter() override; + + bool Init(Dx11Renderer& renderer, + const StereoDisplayDriverConfiguration& cfg, + const FocusContext& focus) override; + void RecordComposite(ID3D11Texture2D* sbs_input) override; + void Present() override; + void Shutdown() override; + void RequestCalibrate() override; + +private: + bool CreateSwapChain(Dx11Renderer& renderer); + void WindowThreadLoop(Dx11Renderer* renderer, + platform::MonitorInfo primary, + platform::MonitorInfo secondary); + void FocusThreadLoop(); + void HeadTrackingThreadLoop(); + + Dx11Renderer* renderer_ = nullptr; + bool eye_swap_ = false; + bool auto_focus_ = true; + + FocusContext focus_{}; + + std::unique_ptr window_; + Microsoft::WRL::ComPtr swapchain_; + Microsoft::WRL::ComPtr swapchain2_; + // FLIP_DISCARD rotates buffer 0 each present, so we recreate the RTV per + // frame instead of caching one at swap-chain creation. + Microsoft::WRL::ComPtr swapchain_rtv_; + // DWM-signaled handle that releases once per display refresh — gates the + // window thread's pacing without blocking inside Present. + HANDLE frame_latency_wait_ = nullptr; + uint32_t swap_width_ = 0; + uint32_t swap_height_ = 0; + + SR::SRContext* sr_context_ = nullptr; + SR::IDX11Weaver1* sr_weaver_ = nullptr; + SR::SwitchableLensHint* lens_hint_ = nullptr; + bool sr_initialized_ = false; + + // Cache last input texture dims so we don't call setInputViewTexture + // every frame when nothing changed. + Microsoft::WRL::ComPtr input_srv_; + ID3D11Texture2D* cached_input_ptr_ = nullptr; + uint32_t cached_input_w_ = 0; + uint32_t cached_input_h_ = 0; + DXGI_FORMAT cached_input_fmt_ = DXGI_FORMAT_UNKNOWN; + + std::thread window_thread_; + std::atomic window_stop_{false}; + std::atomic window_ready_{false}; + std::atomic window_failed_{false}; + + std::thread focus_thread_; + std::atomic focus_stop_{false}; + + // Head tracking — only spawned when use_open_track && output_mode==LeiaSR. + // Sends OpenTrack UDP packets to 127.0.0.1:open_track_port, received by + // MockControllerDeviceDriver::OpenTrackThread. Decouples SR types from the + // consumer side: this presenter is the only translation unit that touches + // SR head-tracking APIs. + bool tracking_enabled_ = false; + int32_t tracking_port_ = 4242; + StereoDisplayDriverConfiguration tracking_cfg_{}; + SR::HeadPoseTracker* sr_head_tracker_ = nullptr; + std::unique_ptr head_listener_; + std::unique_ptr ot_sender_; + std::unique_ptr track_pipeline_; + std::thread tracking_thread_; + std::atomic tracking_stop_{false}; + std::atomic calibrate_request_{false}; +}; + +} // namespace vrto3d + diff --git a/vrto3d/src/presenter/nvstereo_dx9_presenter.cpp b/vrto3d/src/presenter/nvstereo_dx9_presenter.cpp new file mode 100644 index 00000000..05eb280f --- /dev/null +++ b/vrto3d/src/presenter/nvstereo_dx9_presenter.cpp @@ -0,0 +1,493 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ + + +#include "nvstereo_dx9_presenter.h" + +#include +#include + +#define WIN32_LEAN_AND_MEAN +#include + +#include + +#include + +#include "dx11_renderer.h" +#include "focus_policy.h" +#include "hmd_device_driver.h" +#include "osd/osd_renderer.h" +#include "platform.h" +#include "vrto3dlib/debug_log.hpp" +#include "vrto3dlib/win32_helper.hpp" + +namespace vrto3d { + +namespace { + +// Set once CreateInterfaceDX11 succeeds. Read by MyDeviceProvider::Cleanup +// (via NvStereoWasActiveThisSession) to gate the TerminateProcess exit path. +std::atomic g_nv3d_was_active{false}; + +// Routes NV3D-Lib diagnostics into the driver log so [NV3D] lines interleave +// with our own messages — crash/TDR forensics depend on one unified timeline +// (the same pattern NV3D-Glass uses). +void Nv3dLogSink(NV3D::LogLevel level, const wchar_t* msg, void* /*user*/) +{ + if (!msg) return; + const int n = WideCharToMultiByte(CP_UTF8, 0, msg, -1, nullptr, 0, nullptr, nullptr); + std::string s(n > 0 ? static_cast(n - 1) : 0, '\0'); + if (n > 1) { + WideCharToMultiByte(CP_UTF8, 0, msg, -1, s.data(), n, nullptr, nullptr); + } + const char* lvl = ""; + switch (level) { + case NV3D::LogLevel::Warning: lvl = " WARN:"; break; + case NV3D::LogLevel::Error: lvl = " ERROR:"; break; + default: break; + } + LOG() << "[NV3D]" << lvl << " " << s; +} + +std::wstring Utf8ToWide(const char* s) +{ + if (!s || !*s) return {}; + const int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, nullptr, 0); + if (n <= 1) return {}; + std::wstring w(static_cast(n - 1), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, s, -1, w.data(), n); + return w; +} + +} // namespace + +bool NvStereoWasActiveThisSession() +{ + return g_nv3d_was_active.load(std::memory_order_acquire); +} + + +bool NvStereoDx9Presenter::Init(Dx11Renderer& renderer, + const StereoDisplayDriverConfiguration& cfg, + const FocusContext& focus) +{ + renderer_ = &renderer; + focus_ = focus; + auto_focus_cache_ = cfg.auto_focus; + last_eye_swap_ = cfg.eye_swap; + renderer.Device()->GetImmediateContext(&ctx_); + + // 3D Vision output is single-monitor. + platform::MonitorInfo primary{}, secondary{}; + if (!platform::ResolveTargetMonitors(cfg.display_index, false, primary, secondary)) { + LOG() << "NvStereoDx9Presenter::Init: ResolveTargetMonitors failed"; + return false; + } + RECT mr{ primary.x, primary.y, + primary.x + static_cast(primary.width), + primary.y + static_cast(primary.height) }; + HMONITOR monitor = MonitorFromRect(&mr, MONITOR_DEFAULTTONEAREST); + + // Wire the log sink before any other library call so bring-up diagnostics + // land in the driver log too. + NV3D::SetLogSink(&Nv3dLogSink, nullptr); + + // Keep shipping the user-updatable LightBoost DB as a SteamVR resource; + // the library falls back to its embedded copy when the path is null. + char path8[512] = {}; + vr::VRResources()->GetResourceFullPath( + "{vrto3d}/nvtimings.json", "", path8, sizeof(path8)); + nvtimings_path_w_ = Utf8ToWide(path8); + + NV3D::InitParams p{}; + p.target_monitor = monitor; + p.eye_swap = cfg.eye_swap; + p.on_top = true; + p.enable_lightboost = true; + p.nvtimings_json_path = nvtimings_path_w_.empty() ? nullptr + : nvtimings_path_w_.c_str(); + p.host_hwnd = nullptr; // library-owned FSE click-through popup + p.enable_suppressor = true; // nvd3dumx OSD/rating/hotkey detours + p.activation_retry_budget = 60; + // Own-PID trick (from NV3D-Glass): IsProcessRunning(self) is always true, + // so the library's tracked mode reduces to "visible while SetVisible(true)" + // and our FocusThreadLoop stays authoritative. At the pinned lib commit, + // tracked mode never force-foregrounds the tracked process (the NV3D.hpp + // doc comment claiming it does is stale — see present_window.cpp there). + // Do NOT pass 0: that selects minimize-on-host-focus-loss, and vrserver + // loses focus the moment the game takes foreground, i.e. always. + p.tracked_game_pid = GetCurrentProcessId(); + + LOG() << "NvStereoDx9Presenter::Init: creating NV3D interface on " + << primary.device_name << " " << primary.width << "x" << primary.height + << " lightboost_db=" << (p.nvtimings_json_path ? "resource" : "embedded"); + + // Blocks through window creation, FSE CreateDeviceEx, LightBoost modeset + // and stereo bring-up — can take seconds (parity with the old in-tree + // implementation, which blocked this same driver thread up to 30s). + const HRESULT hr = NV3D::CreateInterfaceDX11(renderer.Device(), &p, &iface_); + if (FAILED(hr) || !iface_) { + LOG() << "NvStereoDx9Presenter::Init: CreateInterfaceDX11 failed hr=0x" + << std::hex << static_cast(hr); + iface_ = nullptr; + return false; + } + g_nv3d_was_active.store(true, std::memory_order_release); + + render_stop_.store(false); + render_thread_ = std::thread(&NvStereoDx9Presenter::RenderLoop, this); + focus_stop_.store(false); + focus_thread_ = std::thread(&NvStereoDx9Presenter::FocusThreadLoop, this); + + LOG() << "NvStereoDx9Presenter: ready (NV3D-Lib), target_monitor=" + << primary.device_name; + return true; +} + + +void NvStereoDx9Presenter::RenderLoop() +{ + // WaitAndDrawPending paces off the compositor's submit signal (33ms + // timeout fall-through re-presents, keeping stereo alive when idle) and + // invokes RecordComposite under context_mutex_ / Present() outside it. + // It returns false before the renderer finishes initializing and after + // device death — sleep briefly then so the loop can't spin hot. + while (!render_stop_.load(std::memory_order_relaxed)) { + if (!renderer_->WaitAndDrawPending(33)) { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + // Service this thread's message queue every iteration. The OSD's + // WH_MOUSE_LL mouse hook is installed from THIS thread (OsdRenderer:: + // RenderFrame → OsdInput::SetMouseHookActive runs inside our + // RecordComposite), and low-level hook callbacks are only delivered + // while the installing thread pumps messages — without this, the hook + // never fires and OSD clicks are dead. The old in-tree presenter got + // this for free from its window thread's PollEvents(); this thread + // owns no windows, so the pump exists solely to service the hook. + MSG msg; + while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } +} + + +bool NvStereoDx9Presenter::EnsureRing(ID3D11Texture2D* sbs) +{ + D3D11_TEXTURE2D_DESC in{}; + sbs->GetDesc(&in); + if (ring_[0] && in.Width == ring_w_ && in.Height == ring_h_ && + in.Format == ring_fmt_) { + return true; + } + + // Dims/format changed (in practice: once, on the first frame — the + // renderer pins out_sbs_ for NvidiaDX9 mode). Old slots may still be + // referenced by the library's import cache; SetInputTexture with the new + // pointers below replaces them. + for (auto& t : ring_) t.Reset(); + last_tex_ = nullptr; + + D3D11_TEXTURE2D_DESC d{}; + d.Width = in.Width; + d.Height = in.Height; + d.MipLevels = 1; + d.ArraySize = 1; + d.Format = in.Format; + d.SampleDesc.Count = 1; + d.Usage = D3D11_USAGE_DEFAULT; + d.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; + // Legacy KMT share — the only flavor the 3D Vision driver's D3D9 side + // can open (NT handles are rejected). + d.MiscFlags = D3D11_RESOURCE_MISC_SHARED; + + for (UINT i = 0; i < kRing; ++i) { + const HRESULT hr = renderer_->Device()->CreateTexture2D(&d, nullptr, &ring_[i]); + if (FAILED(hr)) { + LOG() << "NvStereoDx9Presenter: staging ring slot " << i + << " create failed hr=0x" << std::hex + << static_cast(hr); + for (auto& t : ring_) t.Reset(); + return false; + } + } + ring_w_ = in.Width; + ring_h_ = in.Height; + ring_fmt_ = in.Format; + ring_idx_ = 0; + LOG() << "NvStereoDx9Presenter: staging ring " << kRing << "x " + << ring_w_ << "x" << ring_h_ << " fmt=" << ring_fmt_; + return true; +} + + +void NvStereoDx9Presenter::RecordComposite(ID3D11Texture2D* sbs_input) +{ + if (dead_.load(std::memory_order_relaxed) || !iface_ || !sbs_input || !ctx_) { + return; + } + + // Host-side device-removed detection. The library often can't observe a + // TDR itself (a hidden popup means no D3D9 call runs), so we watch our + // own D3D11 device and hand the dead-mark over via NotifyDeviceLost. + if (++frames_since_dev_check_ >= 60) { + frames_since_dev_check_ = 0; + if (FAILED(renderer_->Device()->GetDeviceRemovedReason())) { + MarkDead("D3D11 device removed"); + return; + } + } + + // OSD wiring. renderer_->Osd() is null until the OSD is lazily created on + // this (render) thread — which happens only after the driver's + // ConfigureOsd(nullptr) has run — so the first non-null observation here + // is guaranteed to be past that point. + if (auto* osd = renderer_->Osd()) { + // (1) Hand the library-owned popup HWND to the OSD so it can map + // screen cursor coords into the popup's client area. Done here + // (not in Init) because ConfigureOsd(nullptr) runs after Init and + // would clobber an earlier set. Without a valid HWND the OSD's + // cursor mapping never runs and the menu is unclickable. + if (!osd_hwnd_pushed_) { + renderer_->SetOsdHeadsetHwnd(iface_->GetWindowHandle()); + osd_hwnd_pushed_ = true; + } + // (2) Drive the popup's interactivity from OSD-menu visibility. We're + // on the render thread — the same thread that renders the OSD — so + // reading MenuVisible() is race-free. Menu open → make the FSE + // popup solid so clicks land on the OSD instead of passing through + // to the game; close → restore click-through. The library owns the + // actual toggle (WndProc + WS_EX_TRANSPARENT), which is why VRto3D's + // OSD no longer manipulates the window styles itself for this mode. + const bool menu = osd->MenuVisible(); + if (menu != last_interactive_) { + last_interactive_ = menu; + iface_->SetInteractive(menu); + } + } + + // Occluded-present protection: PresentEx against a minimized FSE popup + // wedges some drivers. want_visible_ flips before the focus thread calls + // SetVisible, so submissions stop before/while the library drains. + if (!want_visible_.load(std::memory_order_relaxed)) { + return; + } + + if (!EnsureRing(sbs_input)) return; + ring_idx_ = (ring_idx_ + 1) % kRing; + ID3D11Texture2D* slot = ring_[ring_idx_].Get(); + // The previous slots may still be mid-StretchRect on the library's worker + // (KMT sharing has no implicit sync) — that's exactly why this copy goes + // into a rotating slot rather than handing out_sbs_ over directly. + ctx_->CopyResource(slot, sbs_input); + + // Live eye-swap: hotkey/OSD toggles land in the config; the library + // applies the change on the next frame without re-init. + if (renderer_->Component()) { + const bool swap = renderer_->Component()->GetConfig().eye_swap; + if (swap != last_eye_swap_) { + last_eye_swap_ = swap; + iface_->SetEyeSwap(swap); + } + } + + if (slot != last_tex_) { + const HRESULT hr = iface_->SetInputTexture(slot); + if (FAILED(hr)) { + LOG() << "NvStereoDx9Presenter: SetInputTexture failed hr=0x" + << std::hex << static_cast(hr); + return; + } + last_tex_ = slot; + } + + // Signals the lib's fence / EVENT query on our immediate context (hence + // under context_mutex_) and returns in microseconds; the D3D9 StretchRect + // + PresentEx run on the library's async worker. The HRESULT reflects the + // PREVIOUS frame, so judge failure by streak, not a single result. + if (FAILED(iface_->Present())) { + if (++present_fail_streak_ >= 40) { + MarkDead("40-frame present failure streak"); + } + } else { + present_fail_streak_ = 0; + } +} + + +void NvStereoDx9Presenter::FocusThreadLoop() +{ + using namespace std::chrono_literals; + + // The library creates its popup VISIBLE + on-top (FSE engaged). Our + // policy, by contrast, starts lowered (is_on_top_/man_on_top_ init false + // and no app is connected yet → ComputeWantOnTop == false). Those two + // disagree, so we must drive SetVisible on the FIRST iteration to reconcile + // them — otherwise want(false) == was_on_top(false), no transition fires, + // the popup is never told to minimize, and it stays stuck maximized until + // some later want=true→false round-trip (the "starts maximized / can't + // minimize" bug). `first` forces that initial sync; thereafter we act on + // transitions only. + bool was_on_top = true; // matches the library's initial visible state + bool first = true; + FocusLatchState latch; // auto-focus per-PID latch (focus_policy.h) + + while (!focus_stop_.load(std::memory_order_relaxed)) { + FocusInputs fi; + fi.is_on_top = focus_.is_on_top && focus_.is_on_top->load(); + fi.man_on_top = focus_.man_on_top && focus_.man_on_top->load(); + fi.app_pid = focus_.app_pid ? focus_.app_pid->load() : 0; + // Live mirror — falls back to the init-time cache if the driver + // didn't plumb the pointer. + fi.auto_focus = focus_.auto_focus ? focus_.auto_focus->load() + : auto_focus_cache_; + fi.app_running = platform::IsProcessRunning(fi.app_pid); + // An open OSD must be visible to use — force the popup up while the + // menu is open (mirrors the Linux VkRenderer focus block). Without + // this, opening the menu while the popup is hidden softlocks: the + // open menu suppresses the driver's hotkey poll (incl. Ctrl+F8), so + // the user can't restore the window to see what they're doing. On + // menu close this falls back to the normal policy, re-minimizing if + // the popup was lowered. Cross-thread MenuVisible() reads are + // established practice (PollHotkeysThread / CursorControlThread). + { + auto* osd = renderer_ ? renderer_->Osd() : nullptr; + fi.force_on_top = osd && osd->MenuVisible(); + } + + bool set_is = false, set_man = false; + const bool want = ComputeWantOnTop(fi, latch, &set_is, &set_man); + if (set_is && focus_.is_on_top) focus_.is_on_top->store(true); + if (set_man && focus_.man_on_top) focus_.man_on_top->store(true); + + if (first || want != was_on_top) { + first = false; + // Stop submissions first (see RecordComposite), then let the + // library's window thread do the SW_MINIMIZE/SW_RESTORE + topmost + // + suppression sequencing. Never touch the popup HWND from here: + // cross-thread ShowWindow on a FSE D3D9Ex device window wedges + // DWM. SetVisible(false) may block ~1s (worker drain + GPU idle) + // — acceptable on this thread, never call it under context_mutex_. + want_visible_.store(want, std::memory_order_relaxed); + iface_->SetVisible(want); + if (want) { + StartForceFocusWatcher(fi.app_pid); + } else { + StopForceFocusWatcher(); + } + was_on_top = want; + } + + std::this_thread::sleep_for(50ms); + } +} + + +void NvStereoDx9Presenter::StartForceFocusWatcher(uint32_t pid) +{ + StopForceFocusWatcher(); + if (pid == 0 || pid == GetCurrentProcessId()) return; + + // When SteamVR is launched by the app, the game window may not exist yet, + // or SteamVR's bring-up (vrmonitor / status window) may grab foreground — + // re-assert focus onto the game whenever it drifts, for up to 15s. + // + // JOINABLE by design: a detached watcher mid-ForceFocus at process exit + // leaves the OS-wide input chain attached to a dying thread ("display + // frozen, cursor stuck, reboot required"). Joining caps the wedge window + // at one iteration; the stop flag is polled in 100ms slices so joins are + // prompt. + focus_watcher_stop_ = std::make_shared>(false); + auto stop = focus_watcher_stop_; + auto man_on_top = focus_.man_on_top; + focus_watcher_thread_ = std::thread([pid, stop, man_on_top]() { + for (int i = 0; i < 15; ++i) { + if (stop->load(std::memory_order_relaxed)) return; + if (man_on_top && !man_on_top->load()) return; + HWND game_hwnd = GetHWNDFromPID(pid); + if (game_hwnd && GetForegroundWindow() != game_hwnd) { + ForceFocus(game_hwnd, + GetCurrentThreadId(), + GetWindowThreadProcessId(game_hwnd, nullptr)); + } + for (int j = 0; j < 10; ++j) { + if (stop->load(std::memory_order_relaxed)) return; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } + }); +} + + +void NvStereoDx9Presenter::StopForceFocusWatcher() +{ + if (focus_watcher_stop_) { + focus_watcher_stop_->store(true, std::memory_order_relaxed); + } + if (focus_watcher_thread_.joinable()) { + focus_watcher_thread_.join(); + } + focus_watcher_stop_.reset(); +} + + +void NvStereoDx9Presenter::MarkDead(const char* why) +{ + if (dead_.exchange(true, std::memory_order_acq_rel)) return; + LOG() << "NvStereoDx9Presenter: marking dead (" << why + << ") — output disabled until SteamVR restart"; + // Hand the dead-mark to the library BEFORE Delete() so teardown takes the + // non-blocking path (no Stereo_DestroyHandle, Detach instead of Release — + // both can block indefinitely against a wedged kernel driver). + if (iface_) iface_->NotifyDeviceLost(); +} + + +void NvStereoDx9Presenter::Shutdown() +{ + // Join order is load-bearing: focus thread (calls SetVisible) and watcher + // first, then the render thread (calls Present via RecordComposite), then + // the library teardown — Delete() drains its own worker, unhooks the + // suppressor, releases D3D9/NvAPI (Detach path when dead) and destroys + // its window last. + focus_stop_.store(true); + if (focus_thread_.joinable()) focus_thread_.join(); + StopForceFocusWatcher(); + + render_stop_.store(true); + if (render_thread_.joinable()) render_thread_.join(); + + if (iface_) { + if (dead_.load(std::memory_order_acquire)) { + iface_->NotifyDeviceLost(); // idempotent — belt & braces + } + LOG() << "NvStereoDx9Presenter: Shutdown — deleting NV3D interface"; + iface_->Delete(); + iface_ = nullptr; + } + + for (auto& t : ring_) t.Reset(); + last_tex_ = nullptr; + ring_w_ = ring_h_ = 0; + ring_fmt_ = DXGI_FORMAT_UNKNOWN; + ctx_.Reset(); + renderer_ = nullptr; +} + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/nvstereo_dx9_presenter.h b/vrto3d/src/presenter/nvstereo_dx9_presenter.h new file mode 100644 index 00000000..d59bad1f --- /dev/null +++ b/vrto3d/src/presenter/nvstereo_dx9_presenter.h @@ -0,0 +1,154 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + + +#include +#include +#include +#include + +#include +#include + +#include "presenter/output_presenter.h" +#include "vrto3dlib/stereo_config.h" + +namespace NV3D { class InterfaceDX11; } + +namespace vrto3d { + +// True once an NV3D-Lib interface was successfully created this session. +// Read by MyDeviceProvider::Cleanup to decide whether to TerminateProcess at +// driver unload instead of letting nvd3dumx/nvapi64/d3d9 run their +// DLL_PROCESS_DETACH (see device_provider.cpp for the NV3D-Glass rationale). +bool NvStereoWasActiveThisSession(); + +// NVIDIA 3D Vision output via NV3D-Lib (external/NV3D-Lib, nv3d-glass branch). +// +// The library owns everything that historically lived in this file: the FSE +// click-through popup and its message-pumping window thread, the D3D9Ex +// device, NvAPI stereo bring-up + activation retries, LightBoost custom +// timings (nvtimings DB), the in-process 3D Vision suppressor (nvd3dumx OSD / +// rating-overlay / Ctrl+F-hotkey detours — MinHook is compiled into +// NV3DLib.lib), the DX11→D3D9 shared-surface import, and an async present +// worker that runs StretchRect + PresentEx off our threads. +// +// This wrapper owns: +// - the render-loop thread that drives Dx11Renderer::WaitAndDrawPending, +// - the focus-policy thread: ComputeWantOnTop (focus_policy.h) over the +// FocusContext atoms → iface_->SetVisible on transitions. The library is +// inited with tracked_game_pid = our own PID (NV3D-Glass's trick), which +// makes popup visibility purely SetVisible-driven — the library never +// minimizes on its own and never steals game focus, so VRto3D's focus +// policy stays authoritative, +// - a JOINABLE game-focus watcher (the old detached watcher could wedge the +// OS input chain if vrserver exited mid-ForceFocus — "display frozen, +// reboot required"), +// - a 3-deep MISC_SHARED staging ring. The library reads our texture through +// a legacy KMT shared handle, which has ZERO implicit cross-API sync; its +// async worker may still be StretchRect-reading slot N while the +// compositor overwrites out_sbs_, so each frame is copied into a rotating +// slot instead of handing out_sbs_ over directly (the library identity- +// caches 4 import slots, documented >= ring + 1 transient). +// +// Interface contract notes: +// - RecordComposite runs under Dx11Renderer::context_mutex_ and does ALL +// the library work: the lib's Present() signals its fence / issues its +// EVENT query on the host immediate context (caller's thread), so it must +// be serialized against the compositor — and it returns in microseconds +// because the actual D3D9 present happens on the lib's worker. +// - Present() is therefore a no-op: pacing comes from WaitAndDrawPending's +// composite_cv_ wait, and the lib worker owns PresentEx/vsync. +// - The lib's Present() HRESULT reflects the PREVIOUS frame (async worker), +// so failure is judged by a 40-frame streak, not a single result. +// - On host-observed device removal we call NotifyDeviceLost() BEFORE +// Delete() so the lib takes its non-blocking teardown path (no +// Stereo_DestroyHandle / COM Release into a wedged kernel driver). +class NvStereoDx9Presenter : public IOutputPresenter { +public: + NvStereoDx9Presenter() = default; + ~NvStereoDx9Presenter() override { Shutdown(); } + + bool Init(Dx11Renderer& renderer, + const StereoDisplayDriverConfiguration& cfg, + const FocusContext& focus) override; + void RecordComposite(ID3D11Texture2D* sbs_input) override; + // Everything context-bound already ran in RecordComposite; the library's + // async worker owns PresentEx pacing. + void Present() override {} + void Shutdown() override; + bool IsAlive() const override { + return !dead_.load(std::memory_order_acquire); + } + +private: + void RenderLoop(); + void FocusThreadLoop(); + // Joinable replacement for the old detached game-focus watcher. + void StartForceFocusWatcher(uint32_t pid); + void StopForceFocusWatcher(); + bool EnsureRing(ID3D11Texture2D* sbs); + // Latch dead_ + NotifyDeviceLost so Shutdown's Delete() takes the + // non-blocking teardown path. Idempotent. + void MarkDead(const char* why); + + Dx11Renderer* renderer_ = nullptr; + NV3D::InterfaceDX11* iface_ = nullptr; + Microsoft::WRL::ComPtr ctx_; + FocusContext focus_{}; + bool auto_focus_cache_ = true; + bool last_eye_swap_ = false; + // InitParams copies the raw nvtimings_json_path pointer — this string + // must outlive iface_. + std::wstring nvtimings_path_w_; + + // Staging ring (see class comment). Lazily (re)created from the incoming + // texture desc so it always matches out_sbs_'s pinned dims/format exactly + // (CopyResource requires an exact match, including _SRGB-ness). + static constexpr UINT kRing = 3; + Microsoft::WRL::ComPtr ring_[kRing]; + UINT ring_idx_ = 0; + UINT ring_w_ = 0; + UINT ring_h_ = 0; + DXGI_FORMAT ring_fmt_ = DXGI_FORMAT_UNKNOWN; + ID3D11Texture2D* last_tex_ = nullptr; // identity cache for SetInputTexture + + int frames_since_dev_check_ = 0; + int present_fail_streak_ = 0; + // Tracks the last OSD-menu-driven interactivity state pushed to the + // library (true = popup solid/clickable for the OSD, false = click-through). + bool last_interactive_ = false; + // One-shot: the library popup HWND has been handed to the OSD for cursor + // mapping (done on the render thread, after the driver's ConfigureOsd). + bool osd_hwnd_pushed_ = false; + + std::atomic dead_{false}; + // Mirrors the last SetVisible state. Stored BEFORE calling SetVisible so + // RecordComposite stops submitting before/while the lib drains its worker + // (PresentEx against a minimized FSE window wedges some drivers). + std::atomic want_visible_{true}; + + std::thread render_thread_; + std::atomic render_stop_{false}; + std::thread focus_thread_; + std::atomic focus_stop_{false}; + std::thread focus_watcher_thread_; + std::shared_ptr> focus_watcher_stop_; +}; + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/output_presenter.h b/vrto3d/src/presenter/output_presenter.h new file mode 100644 index 00000000..b4b9b170 --- /dev/null +++ b/vrto3d/src/presenter/output_presenter.h @@ -0,0 +1,71 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include + +#include + +#include "focus_context.h" +#include "vrto3dlib/stereo_config.h" + +class Dx11Renderer; + +namespace vrto3d { + +class IOutputPresenter { +public: + virtual ~IOutputPresenter() = default; + + // Called once after Dx11Renderer has finished constructing the D3D11 device. + virtual bool Init(Dx11Renderer& renderer, + const StereoDisplayDriverConfiguration& cfg, + const FocusContext& focus) = 0; + + // Hand the freshly-copied 2W x H SbS input texture to the presenter, record + // all GPU work for this frame (compose-shader draw, weaver, packed-surface + // pipeline, etc.) onto the immediate context, and Flush so the commands are + // in the driver queue. Caller MUST hold Dx11Renderer::context_mutex_ across + // this call. + virtual void RecordComposite(ID3D11Texture2D* sbs_input) = 0; + + // Issue the swap-chain Present for the frame recorded by RecordComposite. + // Caller MUST NOT hold Dx11Renderer::context_mutex_ — Present blocks on + // display pacing (waitable handle / vsync interval), and holding the mutex + // would stall the compositor thread's next OnDirectModeFrame. + virtual void Present() = 0; + + virtual void Shutdown() = 0; + + // Optional: snap the current head pose as the neutral zero. Default + // no-op — only the LeiaSR presenter implements head-pose calibration. + // Called from the menu-callback thread; implementations MUST be + // thread-safe (typically just an atomic flag the render/track thread + // reads on the next iteration). + virtual void RequestCalibrate() {} + + // Returns false once the presenter has hit a terminal device state and + // can no longer produce output. The renderer uses this to short-circuit + // OSD render + RecordComposite — submitting GPU work to a wedged GPU + // can prevent NVIDIA's TDR from recovering, and we've seen full-PC + // freezes when this isn't stopped. Default true (presenter is alive). + virtual bool IsAlive() const { return true; } +}; + +std::unique_ptr MakePresenter(OutputMode mode); + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/presenter_factory.cpp b/vrto3d/src/presenter/presenter_factory.cpp new file mode 100644 index 00000000..35c773f6 --- /dev/null +++ b/vrto3d/src/presenter/presenter_factory.cpp @@ -0,0 +1,66 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ + +#include "output_presenter.h" +#include "window_presenter.h" +#include "leiasr_presenter.h" +#include "nvstereo_dx9_presenter.h" +#include "wibblewobble_presenter.h" + +#include "vrto3dlib/debug_log.hpp" + +namespace vrto3d { + +std::unique_ptr MakePresenter(OutputMode mode) +{ + switch (mode) { + case OutputMode::SbS: + case OutputMode::TaB: + case OutputMode::RowInterlaced: + case OutputMode::ColInterlaced: + case OutputMode::Checkerboard: + case OutputMode::VirtualDesktop: + case OutputMode::FramePacked720p60: + case OutputMode::FramePacked1080p24: + case OutputMode::FramePacked1080p60: + case OutputMode::FramePacked1080p60CVT: + case OutputMode::DualDisplay: + case OutputMode::DualDisplayFlip: + case OutputMode::AnaglyphRedCyan: + case OutputMode::AnaglyphRedCyanDubois: + case OutputMode::AnaglyphRedCyanDeghosted: + case OutputMode::AnaglyphRedCyanCompromise: + case OutputMode::AnaglyphGreenMagenta: + case OutputMode::AnaglyphGreenMagentaDubois: + case OutputMode::AnaglyphGreenMagentaDeghosted: + case OutputMode::AnaglyphBlueAmber: + case OutputMode::Mono: + return std::make_unique(); + + case OutputMode::LeiaSR: + return std::make_unique(); + + case OutputMode::NvidiaDX9: + return std::make_unique(); + + case OutputMode::WibbleWobble: + return std::make_unique(); + } + return std::make_unique(); +} + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/swapchain_presenter_base.h b/vrto3d/src/presenter/swapchain_presenter_base.h new file mode 100644 index 00000000..b019c0f3 --- /dev/null +++ b/vrto3d/src/presenter/swapchain_presenter_base.h @@ -0,0 +1,47 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include "presenter/vk_presenter.h" +#include "presenter/vk_swapchain_util.h" + +namespace vrto3d { + +// Shared IVkPresenter plumbing for presenters backed by a SwapchainBundle +// (WaylandPresenter, X11Presenter). These delegations were identical in both; +// subclasses now implement only the native-windowing surface (Init, Shutdown, +// PumpEvents, SetAlwaysOnTop, SetInputCapture, Name) and populate ctx_ + +// swapchain_ during Init. Presenters that don't own a swapchain (e.g. +// WibbleWobblePresenter, which streams dmabufs) implement IVkPresenter directly. +class SwapchainPresenterBase : public IVkPresenter { +public: + bool AcquireNext(FrameTarget* out, VkSemaphore signal_sem) override { + return swapchain_.AcquireNext(out, signal_sem); + } + bool Present(uint32_t image_index, VkSemaphore wait_sem) override { + return swapchain_.Present(image_index, wait_sem); + } + VkRenderPass RenderPass() const override { return swapchain_.render_pass; } + VkExtent2D Extent() const override { return swapchain_.extent; } + VkFormat Format() const override { return swapchain_.format; } + +protected: + vk::DeviceCtx* ctx_ = nullptr; + SwapchainBundle swapchain_; +}; + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/vk_presenter.h b/vrto3d/src/presenter/vk_presenter.h new file mode 100644 index 00000000..554f66f3 --- /dev/null +++ b/vrto3d/src/presenter/vk_presenter.h @@ -0,0 +1,100 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include + +#include + +#include "vrto3dlib/stereo_config.h" + +namespace vrto3d::vk { +struct DeviceCtx; +} + +namespace vrto3d { + +// Linux presenter seam: owns the native window/surface + Vulkan swapchain on +// the chosen output. Two implementations: WaylandPresenter (layer-shell +// overlay with xdg_toplevel fullscreen fallback) and X11Presenter (positioned +// borderless topmost window, XRandR modelines). +// +// Thread model: ALL methods are called from the renderer's present thread +// (which is also the thread that created the object via MakeVkPresenter). +class IVkPresenter { +public: + virtual ~IVkPresenter() = default; + + struct FrameTarget { + VkImage image = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + uint32_t index = 0; + }; + + // Creates the native window fullscreen-covering the output selected by + // cfg.display_index (1-based; 0 = primary), the VkSurfaceKHR, a FIFO + // swapchain, and a render pass targeting the swapchain format + // (loadOp=DONT_CARE, storeOp=STORE, finalLayout=PRESENT_SRC). + virtual bool Init(vrto3d::vk::DeviceCtx* ctx, const StereoDisplayDriverConfiguration& cfg) = 0; + virtual void Shutdown() = 0; + + // Drain native display-server events. Returns false when the surface was + // closed/destroyed and presenting must stop. + virtual bool PumpEvents() = 0; + + // Acquire the next swapchain image; `signal_sem` is signaled when the + // image is ready to be rendered to. Handles OUT_OF_DATE by recreating + // the swapchain internally (returns false for "skip this frame"). + virtual bool AcquireNext(FrameTarget* out, VkSemaphore signal_sem) = 0; + + // Queue the present for the acquired image, waiting on `wait_sem`. + // FIFO mode: this is the frame-pacing block. + virtual bool Present(uint32_t image_index, VkSemaphore wait_sem) = 0; + + virtual VkRenderPass RenderPass() const = 0; + virtual VkExtent2D Extent() const = 0; + virtual VkFormat Format() const = 0; + virtual const char* Name() const = 0; + + // Best-effort topmost control (X11: _NET_WM_STATE_ABOVE; Wayland + // layer-shell: inherently on the overlay layer — no-ops). + virtual void BringToTop() {} + virtual void ReleaseTopmost() {} + + // Always-on-top toggle. true = overlay above everything (default); + // false = drop the overlay behind normal windows so the flat game shows + // through (X11: XLowerWindow + drop _NET_WM_STATE_ABOVE; Wayland: + // zwlr_layer_surface set_layer BACKGROUND). The surface stays mapped and + // presentable either way — no swapchain teardown. + virtual void SetAlwaysOnTop(bool on_top) {} + + // Input capture. false (default) = the overlay is click-through so mouse + // and keyboard reach the game beneath (X11: empty XShape input region + + // input=False WM hint; Wayland: empty wl_surface input region + + // keyboard_interactivity NONE). true = capture pointer + keyboard so the + // OSD can be clicked/typed without leaking to the game. The OSD itself + // reads input via evdev regardless; capture only shields the game. + virtual void SetInputCapture(bool capture) {} +}; + +// Session-based selection: Wayland when WAYLAND_DISPLAY is set and connectable +// (unless cfg overrides via linux_presenter = "x11"/"wayland"), else X11 via +// DISPLAY. Returns nullptr when neither display server is reachable. +std::unique_ptr MakeVkPresenter(const StereoDisplayDriverConfiguration& cfg); + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/vk_presenter_factory.cpp b/vrto3d/src/presenter/vk_presenter_factory.cpp new file mode 100644 index 00000000..5c10f7d4 --- /dev/null +++ b/vrto3d/src/presenter/vk_presenter_factory.cpp @@ -0,0 +1,76 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#include "presenter/vk_presenter.h" + +#include +#include + +#include "presenter/vk_swapchain_util.h" +#include "presenter/wayland_presenter.h" +#include "presenter/wibblewobble_presenter_linux.h" +#include "presenter/x11_presenter.h" + +namespace vrto3d { + +namespace { + +bool EnvSet(const char* name) +{ + const char* v = std::getenv(name); + return v != nullptr && v[0] != '\0'; +} + +} // namespace + +// Session-based selection. VRTO3D_PRESENTER=x11|wayland|wibblewobble forces a +// backend; otherwise output_mode==WibbleWobble routes to the WibbleWobbleLinux +// handoff, and the rest pick by session env (WAYLAND_DISPLAY -> Wayland, else +// DISPLAY -> X11). +std::unique_ptr MakeVkPresenter(const StereoDisplayDriverConfiguration& cfg) +{ + const char* forced = std::getenv("VRTO3D_PRESENTER"); + if ((forced && std::strcmp(forced, "wibblewobble") == 0) || + cfg.output_mode == OutputMode::WibbleWobble) { + PresenterLog("MakeVkPresenter: WibbleWobble output — streaming to wwserver"); + return std::make_unique(); + } + if (forced && std::strcmp(forced, "x11") == 0) { + PresenterLog("MakeVkPresenter: VRTO3D_PRESENTER=x11"); + return std::make_unique(); + } + if (forced && std::strcmp(forced, "wayland") == 0) { + PresenterLog("MakeVkPresenter: VRTO3D_PRESENTER=wayland"); + return std::make_unique(); + } + if (forced && forced[0] != '\0') { + PresenterLog("MakeVkPresenter: unknown VRTO3D_PRESENTER='%s' — using auto-detect", forced); + } + + if (EnvSet("WAYLAND_DISPLAY")) { + PresenterLog("MakeVkPresenter: WAYLAND_DISPLAY set — Wayland presenter"); + return std::make_unique(); + } + if (EnvSet("DISPLAY")) { + PresenterLog("MakeVkPresenter: DISPLAY set — X11 presenter"); + return std::make_unique(); + } + + PresenterLog("MakeVkPresenter: neither WAYLAND_DISPLAY nor DISPLAY set — no presenter"); + return nullptr; +} + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/vk_swapchain_util.cpp b/vrto3d/src/presenter/vk_swapchain_util.cpp new file mode 100644 index 00000000..9b22bf66 --- /dev/null +++ b/vrto3d/src/presenter/vk_swapchain_util.cpp @@ -0,0 +1,388 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#include "presenter/vk_swapchain_util.h" + +#include +#include +#include +#include + +namespace vrto3d { + +void PresenterLog(const char* fmt, ...) +{ + char body[1024]; + va_list args; + va_start(args, fmt); + vsnprintf(body, sizeof(body), fmt, args); + va_end(args); + std::fprintf(stderr, "[vrto3d] %s\n", body); + std::fflush(stderr); +} + +namespace { + +VkSurfaceFormatKHR PickSurfaceFormat(VkPhysicalDevice phys, VkSurfaceKHR surface) +{ + uint32_t count = 0; + vkGetPhysicalDeviceSurfaceFormatsKHR(phys, surface, &count, nullptr); + std::vector formats(count); + if (count) { + vkGetPhysicalDeviceSurfaceFormatsKHR(phys, surface, &count, formats.data()); + } + if (formats.empty()) { + return { VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR }; + } + // The SteamVR Linux compositor hands us LINEAR eye pixels (direct mode). + // We sample them raw (UNORM imports / out_sbs), so the final present must + // sRGB-ENCODE once — an _SRGB swapchain does that on store, handing the + // compositor correctly-encoded bytes. A UNORM swapchain skips the encode, + // so linear values are shown as if sRGB and the whole image is too dark. + // Prefer _SRGB; UNORM only as a last resort. + for (VkFormat want : {VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_R8G8B8A8_SRGB}) { + for (const auto& f : formats) { + if (f.format == want && f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { + return f; + } + } + } + return formats.front(); +} + +} // namespace + + +bool SwapchainBundle::Create(vk::DeviceCtx* device_ctx, VkSurfaceKHR surf, + uint32_t want_w, uint32_t want_h) +{ + ctx = device_ctx; + surface = surf; + desired_w = want_w; + desired_h = want_h; + + VkBool32 supported = VK_FALSE; + VkResult r = vkGetPhysicalDeviceSurfaceSupportKHR(ctx->phys, ctx->queue_family, + surface, &supported); + if (r != VK_SUCCESS || supported != VK_TRUE) { + PresenterLog("SwapchainBundle: surface not presentable on queue family %u (r=%d)", + ctx->queue_family, static_cast(r)); + return false; + } + + VkSurfaceFormatKHR sf = PickSurfaceFormat(ctx->phys, surface); + format = sf.format; + color_space = sf.colorSpace; + + // Render pass: single color attachment, loadOp DONT_CARE (the repack + // shader overwrites every pixel), storeOp STORE, final PRESENT_SRC. + VkAttachmentDescription att{}; + att.format = format; + att.samples = VK_SAMPLE_COUNT_1_BIT; + att.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + att.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + att.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + att.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + att.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + att.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + + VkAttachmentReference color_ref{}; + color_ref.attachment = 0; + color_ref.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkSubpassDescription sub{}; + sub.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + sub.colorAttachmentCount = 1; + sub.pColorAttachments = &color_ref; + + // Order the layout transition after the acquire semaphore wait. + VkSubpassDependency dep{}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.srcAccessMask = 0; + dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + + VkRenderPassCreateInfo rp{ VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO }; + rp.attachmentCount = 1; + rp.pAttachments = &att; + rp.subpassCount = 1; + rp.pSubpasses = ⊂ + rp.dependencyCount = 1; + rp.pDependencies = &dep; + + r = vkCreateRenderPass(ctx->device, &rp, nullptr, &render_pass); + if (r != VK_SUCCESS) { + PresenterLog("SwapchainBundle: vkCreateRenderPass failed (r=%d)", static_cast(r)); + return false; + } + + if (!CreateSwapchainObjects(VK_NULL_HANDLE)) { + Destroy(); + return false; + } + return true; +} + + +bool SwapchainBundle::CreateSwapchainObjects(VkSwapchainKHR old_swapchain) +{ + VkSurfaceCapabilitiesKHR caps{}; + VkResult r = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(ctx->phys, surface, &caps); + if (r != VK_SUCCESS) { + PresenterLog("SwapchainBundle: surface caps query failed (r=%d)", static_cast(r)); + return false; + } + + if (caps.currentExtent.width != 0xFFFFFFFFu) { + extent = caps.currentExtent; + } else { + extent.width = std::clamp(desired_w, caps.minImageExtent.width, caps.maxImageExtent.width); + extent.height = std::clamp(desired_h, caps.minImageExtent.height, caps.maxImageExtent.height); + } + if (extent.width == 0 || extent.height == 0) { + // Surface is (temporarily) zero-sized — nothing to present to. + PresenterLog("SwapchainBundle: zero-sized surface extent, deferring swapchain"); + return false; + } + + uint32_t min_images = caps.minImageCount + 1; + if (caps.maxImageCount > 0) { + min_images = std::min(min_images, caps.maxImageCount); + } + + VkCompositeAlphaFlagBitsKHR alpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + if (!(caps.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)) { + const VkCompositeAlphaFlagBitsKHR candidates[] = { + VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR, + VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR, + VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR, + }; + for (auto c : candidates) { + if (caps.supportedCompositeAlpha & c) { alpha = c; break; } + } + } + + VkSwapchainCreateInfoKHR sci{ VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR }; + sci.surface = surface; + sci.minImageCount = min_images; + sci.imageFormat = format; + sci.imageColorSpace = color_space; + sci.imageExtent = extent; + sci.imageArrayLayers = 1; + sci.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + sci.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + sci.preTransform = caps.currentTransform; + sci.compositeAlpha = alpha; + sci.presentMode = VK_PRESENT_MODE_FIFO_KHR; // vsync frame pacing + sci.clipped = VK_TRUE; + sci.oldSwapchain = old_swapchain; + + VkSwapchainKHR new_swapchain = VK_NULL_HANDLE; + r = vkCreateSwapchainKHR(ctx->device, &sci, nullptr, &new_swapchain); + if (old_swapchain != VK_NULL_HANDLE) { + vkDestroySwapchainKHR(ctx->device, old_swapchain, nullptr); + } + if (r != VK_SUCCESS) { + PresenterLog("SwapchainBundle: vkCreateSwapchainKHR failed (r=%d)", static_cast(r)); + return false; + } + swapchain = new_swapchain; + + uint32_t image_count = 0; + vkGetSwapchainImagesKHR(ctx->device, swapchain, &image_count, nullptr); + images.resize(image_count); + vkGetSwapchainImagesKHR(ctx->device, swapchain, &image_count, images.data()); + + views.resize(image_count, VK_NULL_HANDLE); + framebuffers.resize(image_count, VK_NULL_HANDLE); + for (uint32_t i = 0; i < image_count; ++i) { + VkImageViewCreateInfo iv{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO }; + iv.image = images[i]; + iv.viewType = VK_IMAGE_VIEW_TYPE_2D; + iv.format = format; + iv.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + iv.subresourceRange.levelCount = 1; + iv.subresourceRange.layerCount = 1; + r = vkCreateImageView(ctx->device, &iv, nullptr, &views[i]); + if (r != VK_SUCCESS) { + PresenterLog("SwapchainBundle: vkCreateImageView failed (r=%d)", static_cast(r)); + return false; + } + + VkFramebufferCreateInfo fb{ VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO }; + fb.renderPass = render_pass; + fb.attachmentCount = 1; + fb.pAttachments = &views[i]; + fb.width = extent.width; + fb.height = extent.height; + fb.layers = 1; + r = vkCreateFramebuffer(ctx->device, &fb, nullptr, &framebuffers[i]); + if (r != VK_SUCCESS) { + PresenterLog("SwapchainBundle: vkCreateFramebuffer failed (r=%d)", static_cast(r)); + return false; + } + } + + needs_recreate = false; + PresenterLog("SwapchainBundle: swapchain %ux%u, %u images, format %d (FIFO)", + extent.width, extent.height, image_count, static_cast(format)); + return true; +} + + +void SwapchainBundle::DestroySwapchainObjects(bool keep_render_pass) +{ + if (!ctx || ctx->device == VK_NULL_HANDLE) return; + + for (VkFramebuffer fb : framebuffers) { + if (fb) vkDestroyFramebuffer(ctx->device, fb, nullptr); + } + framebuffers.clear(); + for (VkImageView v : views) { + if (v) vkDestroyImageView(ctx->device, v, nullptr); + } + views.clear(); + images.clear(); + + if (swapchain) { + vkDestroySwapchainKHR(ctx->device, swapchain, nullptr); + swapchain = VK_NULL_HANDLE; + } + if (!keep_render_pass && render_pass) { + vkDestroyRenderPass(ctx->device, render_pass, nullptr); + render_pass = VK_NULL_HANDLE; + } +} + + +bool SwapchainBundle::Recreate() +{ + if (!ctx) return false; + + { + // Drain in-flight work touching the old swapchain images. queue_mutex + // keeps the compositor thread's submits out while we idle the device. + std::lock_guard lock(ctx->queue_mutex); + vkDeviceWaitIdle(ctx->device); + } + + for (VkFramebuffer fb : framebuffers) { + if (fb) vkDestroyFramebuffer(ctx->device, fb, nullptr); + } + framebuffers.clear(); + for (VkImageView v : views) { + if (v) vkDestroyImageView(ctx->device, v, nullptr); + } + views.clear(); + images.clear(); + + VkSwapchainKHR old = swapchain; + swapchain = VK_NULL_HANDLE; + return CreateSwapchainObjects(old); +} + + +void SwapchainBundle::Destroy() +{ + if (!ctx) return; + { + std::lock_guard lock(ctx->queue_mutex); + vkDeviceWaitIdle(ctx->device); + } + DestroySwapchainObjects(false); + surface = VK_NULL_HANDLE; + ctx = nullptr; +} + + +void SwapchainBundle::SetDesiredExtent(uint32_t w, uint32_t h) +{ + if (w != 0 && h != 0 && (w != desired_w || h != desired_h)) { + desired_w = w; + desired_h = h; + needs_recreate = true; + } +} + + +bool SwapchainBundle::AcquireNext(IVkPresenter::FrameTarget* out, VkSemaphore signal_sem) +{ + if (!ctx) return false; + + if (needs_recreate || swapchain == VK_NULL_HANDLE) { + if (!Recreate()) return false; + // Skip this frame — caller retries next frame with a clean semaphore. + return false; + } + + uint32_t index = 0; + VkResult r = vkAcquireNextImageKHR(ctx->device, swapchain, UINT64_MAX, + signal_sem, VK_NULL_HANDLE, &index); + if (r == VK_ERROR_OUT_OF_DATE_KHR) { + // Semaphore was NOT signaled — safe to recreate and skip the frame. + needs_recreate = true; + return false; + } + if (r == VK_SUBOPTIMAL_KHR) { + // Image acquired and semaphore pending — must proceed with the frame; + // recreate before the next acquire. + needs_recreate = true; + } else if (r != VK_SUCCESS) { + PresenterLog("SwapchainBundle: vkAcquireNextImageKHR failed (r=%d)", static_cast(r)); + return false; + } + + out->image = images[index]; + out->view = views[index]; + out->framebuffer = framebuffers[index]; + out->index = index; + return true; +} + + +bool SwapchainBundle::Present(uint32_t image_index, VkSemaphore wait_sem) +{ + if (!ctx || swapchain == VK_NULL_HANDLE) return false; + + VkPresentInfoKHR pi{ VK_STRUCTURE_TYPE_PRESENT_INFO_KHR }; + pi.waitSemaphoreCount = 1; + pi.pWaitSemaphores = &wait_sem; + pi.swapchainCount = 1; + pi.pSwapchains = &swapchain; + pi.pImageIndices = &image_index; + + VkResult r; + { + std::lock_guard lock(ctx->queue_mutex); + r = vkQueuePresentKHR(ctx->queue, &pi); + } + + if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR) { + // Wait semaphore is still consumed by a rejected present (spec + // guarantees the wait executes), so just flag the recreate. + needs_recreate = true; + return true; + } + if (r != VK_SUCCESS) { + PresenterLog("SwapchainBundle: vkQueuePresentKHR failed (r=%d)", static_cast(r)); + return false; + } + return true; +} + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/vk_swapchain_util.h b/vrto3d/src/presenter/vk_swapchain_util.h new file mode 100644 index 00000000..684b6ce8 --- /dev/null +++ b/vrto3d/src/presenter/vk_swapchain_util.h @@ -0,0 +1,90 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include +#include + +#include + +#include "presenter/vk_presenter.h" +#include "vk/vk_context.h" + +namespace vrto3d { + +// Minimal stderr logger for the Linux presenter files (printf-style). The +// driver's DebugLog is Windows-only; vrserver captures driver stderr. +void PresenterLog(const char* fmt, ...); + +// Shared FIFO swapchain + render pass + framebuffers for the Linux +// presenters (Wayland/X11). The presenter owns the VkSurfaceKHR; this bundle +// owns everything derived from it. +// +// Recreate policy (matches IVkPresenter's contract): +// - Callers set `needs_recreate` on native resize events. +// - AcquireNext() recreates BEFORE acquiring when flagged or on +// VK_ERROR_OUT_OF_DATE_KHR (returns false — skip this frame; the +// signal semaphore is never left pending). +// - VK_SUBOPTIMAL_KHR at acquire proceeds with the frame (the semaphore +// is already signaled) and flags a recreate for the next frame. +// - Present() flags a recreate on VK_SUBOPTIMAL_KHR / OUT_OF_DATE. +// +// Thread model: all methods on the present thread. Present() takes +// ctx->queue_mutex around vkQueuePresentKHR. +struct SwapchainBundle { + vk::DeviceCtx* ctx = nullptr; + VkSurfaceKHR surface = VK_NULL_HANDLE; // not owned + VkSwapchainKHR swapchain = VK_NULL_HANDLE; + VkRenderPass render_pass = VK_NULL_HANDLE; + VkFormat format = VK_FORMAT_UNDEFINED; + VkColorSpaceKHR color_space = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; + VkExtent2D extent{}; + std::vector images; + std::vector views; + std::vector framebuffers; + bool needs_recreate = false; + + // `desired_w/h` are the fallback extent when the surface reports + // currentExtent = 0xFFFFFFFF (Wayland lets the client pick). + uint32_t desired_w = 0; + uint32_t desired_h = 0; + + // Verifies present support on ctx->queue_family, picks + // VK_FORMAT_B8G8R8A8_SRGB when available (else the first reported + // format), creates the FIFO swapchain, render pass (loadOp=DONT_CARE, + // storeOp=STORE, finalLayout=PRESENT_SRC), image views and framebuffers. + bool Create(vk::DeviceCtx* ctx, VkSurfaceKHR surface, uint32_t desired_w, uint32_t desired_h); + + // Waits for the device to go idle (under queue_mutex) and rebuilds the + // swapchain with oldSwapchain retirement. Render pass is kept (the + // surface format does not change across resizes). + bool Recreate(); + + void Destroy(); + + bool AcquireNext(IVkPresenter::FrameTarget* out, VkSemaphore signal_sem); + bool Present(uint32_t image_index, VkSemaphore wait_sem); + + // Update the fallback extent (called from native configure events). + void SetDesiredExtent(uint32_t w, uint32_t h); + +private: + bool CreateSwapchainObjects(VkSwapchainKHR old_swapchain); + void DestroySwapchainObjects(bool keep_render_pass); +}; + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/wayland_presenter.cpp b/vrto3d/src/presenter/wayland_presenter.cpp new file mode 100644 index 00000000..852ab438 --- /dev/null +++ b/vrto3d/src/presenter/wayland_presenter.cpp @@ -0,0 +1,598 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#include "presenter/wayland_presenter.h" + +#include + +#include + +#include "xdg-shell-client-protocol.h" + +// The canonical wlr-layer-shell XML names get_layer_surface's last argument +// "namespace", so wayland-scanner emits a C prototype with a parameter named +// after a C++ keyword. All headers the protocol header pulls in are already +// included above (their guards make re-inclusion a no-op), so the macro only +// rewrites the offending parameter name. +#include +#include +#define namespace namespace_ // C++ keyword used as a C parameter name +#include "wlr-layer-shell-client-protocol.h" +#undef namespace + +#define VK_USE_PLATFORM_WAYLAND_KHR +#include +#include + +namespace vrto3d { + +namespace { + +// --------------------------------------------------------------------------- +// C listener trampolines. `data` is the owning WaylandPresenter; per-output +// state is looked up by proxy pointer (the outputs_ vector may reallocate +// while globals are still arriving, so no direct OutputInfo* is stored as +// listener data). +// --------------------------------------------------------------------------- + +WaylandPresenter::OutputInfo* FindOutput(WaylandPresenter* self, wl_output* output) +{ + for (auto& o : self->Outputs()) { + if (o.output == output) return &o; + } + return nullptr; +} + +void RegistryGlobal(void* data, wl_registry* registry, uint32_t name, + const char* interface, uint32_t version) +{ + static_cast(data)->OnGlobal(registry, name, interface, version); +} + +void RegistryGlobalRemove(void* data, wl_registry*, uint32_t name) +{ + static_cast(data)->OnGlobalRemove(name); +} + +const wl_registry_listener kRegistryListener = { + RegistryGlobal, + RegistryGlobalRemove, +}; + +void OutputGeometry(void* data, wl_output* output, int32_t x, int32_t y, + int32_t /*phys_w*/, int32_t /*phys_h*/, int32_t /*subpixel*/, + const char* make, const char* model, int32_t /*transform*/) +{ + auto* o = FindOutput(static_cast(data), output); + if (!o) return; + o->x = x; + o->y = y; + o->make = make ? make : ""; + o->model = model ? model : ""; +} + +void OutputMode(void* data, wl_output* output, uint32_t flags, + int32_t width, int32_t height, int32_t refresh) +{ + auto* o = FindOutput(static_cast(data), output); + if (!o) return; + if (flags & WL_OUTPUT_MODE_CURRENT) { + o->width = width; + o->height = height; + o->refresh_mhz = refresh; + } +} + +void OutputDone(void* data, wl_output* output) +{ + auto* o = FindOutput(static_cast(data), output); + if (o) o->done = true; +} + +void OutputScale(void* /*data*/, wl_output* /*output*/, int32_t /*factor*/) +{ +} + +void OutputName(void* data, wl_output* output, const char* name) +{ + auto* o = FindOutput(static_cast(data), output); + if (o && name) o->name = name; +} + +void OutputDescription(void* /*data*/, wl_output* /*output*/, const char* /*description*/) +{ +} + +const wl_output_listener kOutputListener = { + OutputGeometry, + OutputMode, + OutputDone, + OutputScale, + OutputName, + OutputDescription, +}; + +void WmBasePing(void* /*data*/, xdg_wm_base* wm_base, uint32_t serial) +{ + xdg_wm_base_pong(wm_base, serial); +} + +const xdg_wm_base_listener kWmBaseListener = { + WmBasePing, +}; + +void LayerSurfaceConfigure(void* data, zwlr_layer_surface_v1* /*surface*/, + uint32_t serial, uint32_t width, uint32_t height) +{ + static_cast(data)->OnLayerConfigure(serial, width, height); +} + +void LayerSurfaceClosed(void* data, zwlr_layer_surface_v1* /*surface*/) +{ + static_cast(data)->OnLayerClosed(); +} + +const zwlr_layer_surface_v1_listener kLayerSurfaceListener = { + LayerSurfaceConfigure, + LayerSurfaceClosed, +}; + +void XdgSurfaceConfigure(void* data, xdg_surface* /*surface*/, uint32_t serial) +{ + static_cast(data)->OnXdgSurfaceConfigure(serial); +} + +const xdg_surface_listener kXdgSurfaceListener = { + XdgSurfaceConfigure, +}; + +void XdgToplevelConfigure(void* data, xdg_toplevel* /*toplevel*/, + int32_t width, int32_t height, wl_array* /*states*/) +{ + static_cast(data)->OnXdgToplevelConfigure(width, height); +} + +void XdgToplevelClose(void* data, xdg_toplevel* /*toplevel*/) +{ + static_cast(data)->OnXdgToplevelClose(); +} + +void XdgToplevelConfigureBounds(void* /*data*/, xdg_toplevel* /*toplevel*/, + int32_t /*width*/, int32_t /*height*/) +{ +} + +void XdgToplevelWmCapabilities(void* /*data*/, xdg_toplevel* /*toplevel*/, + wl_array* /*capabilities*/) +{ +} + +const xdg_toplevel_listener kXdgToplevelListener = { + XdgToplevelConfigure, + XdgToplevelClose, + XdgToplevelConfigureBounds, + XdgToplevelWmCapabilities, +}; + +} // namespace + + +WaylandPresenter::~WaylandPresenter() +{ + Shutdown(); +} + + +void WaylandPresenter::OnGlobal(wl_registry* registry, uint32_t name, + const char* interface, uint32_t version) +{ + if (std::strcmp(interface, wl_compositor_interface.name) == 0) { + uint32_t v = version < 4 ? version : 4; + compositor_ = static_cast( + wl_registry_bind(registry, name, &wl_compositor_interface, v)); + } else if (std::strcmp(interface, xdg_wm_base_interface.name) == 0) { + uint32_t v = version < 2 ? version : 2; + wm_base_ = static_cast( + wl_registry_bind(registry, name, &xdg_wm_base_interface, v)); + xdg_wm_base_add_listener(wm_base_, &kWmBaseListener, this); + } else if (std::strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { + uint32_t v = version < 4 ? version : 4; + layer_shell_ = static_cast( + wl_registry_bind(registry, name, &zwlr_layer_shell_v1_interface, v)); + } else if (std::strcmp(interface, wl_output_interface.name) == 0) { + uint32_t v = version < 4 ? version : 4; + OutputInfo info; + info.global_name = name; + info.output = static_cast( + wl_registry_bind(registry, name, &wl_output_interface, v)); + outputs_.push_back(info); + wl_output_add_listener(outputs_.back().output, &kOutputListener, this); + } +} + + +void WaylandPresenter::OnGlobalRemove(uint32_t name) +{ + for (auto it = outputs_.begin(); it != outputs_.end(); ++it) { + if (it->global_name == name) { + wl_output_destroy(it->output); + outputs_.erase(it); + return; + } + } +} + + +void WaylandPresenter::OnLayerConfigure(uint32_t serial, uint32_t width, uint32_t height) +{ + zwlr_layer_surface_v1_ack_configure(layer_surface_, serial); + if (width != 0 && height != 0) { + if (configured_ && (width != surf_width_ || height != surf_height_)) { + swapchain_.SetDesiredExtent(width, height); + } + surf_width_ = width; + surf_height_ = height; + } + configured_ = true; +} + + +void WaylandPresenter::OnLayerClosed() +{ + closed_ = true; +} + + +void WaylandPresenter::OnXdgToplevelConfigure(int32_t width, int32_t height) +{ + // Buffered until the matching xdg_surface.configure arrives. + pending_xdg_w_ = width; + pending_xdg_h_ = height; +} + + +void WaylandPresenter::OnXdgSurfaceConfigure(uint32_t serial) +{ + xdg_surface_ack_configure(xdg_surface_, serial); + if (pending_xdg_w_ > 0 && pending_xdg_h_ > 0) { + const uint32_t w = static_cast(pending_xdg_w_); + const uint32_t h = static_cast(pending_xdg_h_); + if (configured_ && (w != surf_width_ || h != surf_height_)) { + swapchain_.SetDesiredExtent(w, h); + } + surf_width_ = w; + surf_height_ = h; + } + configured_ = true; +} + + +void WaylandPresenter::OnXdgToplevelClose() +{ + closed_ = true; +} + + +bool WaylandPresenter::ConnectAndBind() +{ + display_ = wl_display_connect(nullptr); + if (!display_) { + PresenterLog("WaylandPresenter: wl_display_connect failed (WAYLAND_DISPLAY=%s)", + getenv("WAYLAND_DISPLAY") ? getenv("WAYLAND_DISPLAY") : ""); + return false; + } + + registry_ = wl_display_get_registry(display_); + wl_registry_add_listener(registry_, &kRegistryListener, this); + + // First roundtrip: registry globals arrive and outputs get bound. + // Second roundtrip: the bound outputs' geometry/mode/name/done events. + wl_display_roundtrip(display_); + wl_display_roundtrip(display_); + + if (!compositor_) { + PresenterLog("WaylandPresenter: compositor lacks wl_compositor (?)"); + return false; + } + if (!wm_base_ && !layer_shell_) { + PresenterLog("WaylandPresenter: neither zwlr_layer_shell_v1 nor xdg_wm_base available"); + return false; + } + if (outputs_.empty()) { + PresenterLog("WaylandPresenter: no wl_output globals"); + return false; + } + return true; +} + + +bool WaylandPresenter::CreateLayerSurface(wl_output* output) +{ + layer_surface_ = zwlr_layer_shell_v1_get_layer_surface( + layer_shell_, surface_, output, ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, "vrto3d"); + if (!layer_surface_) return false; + zwlr_layer_surface_v1_add_listener(layer_surface_, &kLayerSurfaceListener, this); + + // Anchor to all four edges with no explicit size — the compositor sizes + // the surface to the full output. exclusive_zone -1 = extend over panels. + zwlr_layer_surface_v1_set_anchor(layer_surface_, + ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP + | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM + | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT + | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT); + zwlr_layer_surface_v1_set_exclusive_zone(layer_surface_, -1); + zwlr_layer_surface_v1_set_keyboard_interactivity( + layer_surface_, ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE); + SetSurfaceInputRegion(false); // click-through by default (menu closed) + wl_surface_commit(surface_); + return WaitForConfigure(); +} + +void WaylandPresenter::SetSurfaceInputRegion(bool capture) +{ + if (!surface_) return; + if (capture) { + // null region = whole surface receives pointer input (game shielded). + wl_surface_set_input_region(surface_, nullptr); + } else { + // empty region = input-transparent; pointer falls through to the game. + wl_region* region = wl_compositor_create_region(compositor_); + wl_surface_set_input_region(surface_, region); + wl_region_destroy(region); + } +} + +void WaylandPresenter::SetAlwaysOnTop(bool on_top) +{ + // Layer-shell (v2+): drop to the BACKGROUND layer to reveal the flat game + // (a normal xdg_toplevel sits above background), OVERLAY to restore. Surface + // stays mapped/presentable — we keep rendering behind it. + if (layer_surface_) { + zwlr_layer_surface_v1_set_layer( + layer_surface_, + on_top ? ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY : ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND); + wl_surface_commit(surface_); + } + // xdg_toplevel fallback (GNOME): z-order is compositor policy; the peek + // toggle is a no-op there (documented). +} + +void WaylandPresenter::SetInputCapture(bool capture) +{ + SetSurfaceInputRegion(capture); + if (layer_surface_) { + // Take keyboard while the OSD is open so text entry doesn't leak into + // the game; release (NONE) otherwise so the game keeps keys. + zwlr_layer_surface_v1_set_keyboard_interactivity( + layer_surface_, + capture ? ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE + : ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE); + } + if (surface_) + wl_surface_commit(surface_); +} + + +bool WaylandPresenter::CreateXdgFullscreenSurface(wl_output* output) +{ + xdg_surface_ = xdg_wm_base_get_xdg_surface(wm_base_, surface_); + if (!xdg_surface_) return false; + xdg_surface_add_listener(xdg_surface_, &kXdgSurfaceListener, this); + + xdg_toplevel_ = xdg_surface_get_toplevel(xdg_surface_); + if (!xdg_toplevel_) return false; + xdg_toplevel_add_listener(xdg_toplevel_, &kXdgToplevelListener, this); + xdg_toplevel_set_title(xdg_toplevel_, "VRto3D"); + xdg_toplevel_set_app_id(xdg_toplevel_, "vrto3d"); + xdg_toplevel_set_fullscreen(xdg_toplevel_, output); + + wl_surface_commit(surface_); + return WaitForConfigure(); +} + + +bool WaylandPresenter::WaitForConfigure() +{ + // Blocking dispatch is allowed here — Init() runs before the Vulkan WSI + // has its own event queue on this display. + for (int i = 0; i < 100 && !configured_ && !closed_; ++i) { + if (wl_display_roundtrip(display_) < 0) { + PresenterLog("WaylandPresenter: display error while waiting for configure"); + return false; + } + } + if (!configured_ || closed_) { + PresenterLog("WaylandPresenter: surface never configured (closed=%d)", closed_ ? 1 : 0); + return false; + } + return true; +} + + +bool WaylandPresenter::Init(vrto3d::vk::DeviceCtx* ctx, const StereoDisplayDriverConfiguration& cfg) +{ + ctx_ = ctx; + + if (!ConnectAndBind()) { + Shutdown(); + return false; + } + + // Pick output by display_index: 1-based into the bound order; 0 = first. + size_t pick = 0; + if (cfg.display_index > 0 && static_cast(cfg.display_index) <= outputs_.size()) { + pick = static_cast(cfg.display_index - 1); + } + const OutputInfo& out = outputs_[pick]; + PresenterLog("WaylandPresenter: output %zu/%zu '%s' %dx%d@%.2fHz (display_index=%d)", + pick + 1, outputs_.size(), + !out.name.empty() ? out.name.c_str() : out.model.c_str(), + out.width, out.height, out.refresh_mhz / 1000.0, + cfg.display_index); + + if (cfg.output_mode == OutputMode::DualDisplay + || cfg.output_mode == OutputMode::DualDisplayFlip) { + PresenterLog("WaylandPresenter: DualDisplay spanning is not possible on Wayland " + "(one surface per output) — rendering SbS on the chosen output only"); + } + + surface_ = wl_compositor_create_surface(compositor_); + if (!surface_) { + PresenterLog("WaylandPresenter: wl_compositor_create_surface failed"); + Shutdown(); + return false; + } + + // Preferred: layer-shell overlay on the chosen output. Fallback: xdg + // toplevel fullscreened onto it. + bool surface_ok = false; + if (layer_shell_) { + surface_ok = CreateLayerSurface(out.output); + if (!surface_ok) { + PresenterLog("WaylandPresenter: layer-shell surface failed, trying xdg fallback"); + } + } + if (!surface_ok && wm_base_) { + // A wl_surface whose layer-surface role failed cannot be reused. + if (layer_surface_) { + zwlr_layer_surface_v1_destroy(layer_surface_); + layer_surface_ = nullptr; + wl_surface_destroy(surface_); + surface_ = wl_compositor_create_surface(compositor_); + configured_ = false; + } + surface_ok = surface_ && CreateXdgFullscreenSurface(out.output); + } + if (!surface_ok) { + Shutdown(); + return false; + } + + // 0 from configure means "client picks" — use the output's mode size. + if (surf_width_ == 0 || surf_height_ == 0) { + surf_width_ = out.width > 0 ? static_cast(out.width) : 1920u; + surf_height_ = out.height > 0 ? static_cast(out.height) : 1080u; + } + + VkWaylandSurfaceCreateInfoKHR sci{ VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR }; + sci.display = display_; + sci.surface = surface_; + VkResult r = vkCreateWaylandSurfaceKHR(ctx_->instance, &sci, nullptr, &vk_surface_); + if (r != VK_SUCCESS) { + PresenterLog("WaylandPresenter: vkCreateWaylandSurfaceKHR failed (r=%d)", + static_cast(r)); + Shutdown(); + return false; + } + + if (!swapchain_.Create(ctx_, vk_surface_, surf_width_, surf_height_)) { + Shutdown(); + return false; + } + + wl_display_flush(display_); + PresenterLog("WaylandPresenter: up via %s, %ux%u", + layer_surface_ ? "zwlr_layer_shell_v1 (overlay)" : "xdg_toplevel fullscreen", + swapchain_.extent.width, swapchain_.extent.height); + return true; +} + + +void WaylandPresenter::DestroyNative() +{ + if (layer_surface_) { + zwlr_layer_surface_v1_destroy(layer_surface_); + layer_surface_ = nullptr; + } + if (xdg_toplevel_) { + xdg_toplevel_destroy(xdg_toplevel_); + xdg_toplevel_ = nullptr; + } + if (xdg_surface_) { + xdg_surface_destroy(xdg_surface_); + xdg_surface_ = nullptr; + } + if (surface_) { + wl_surface_destroy(surface_); + surface_ = nullptr; + } + for (auto& o : outputs_) { + if (o.output) wl_output_destroy(o.output); + } + outputs_.clear(); + if (layer_shell_) { + zwlr_layer_shell_v1_destroy(layer_shell_); + layer_shell_ = nullptr; + } + if (wm_base_) { + xdg_wm_base_destroy(wm_base_); + wm_base_ = nullptr; + } + if (compositor_) { + wl_compositor_destroy(compositor_); + compositor_ = nullptr; + } + if (registry_) { + wl_registry_destroy(registry_); + registry_ = nullptr; + } + if (display_) { + wl_display_flush(display_); + wl_display_disconnect(display_); + display_ = nullptr; + } +} + + +void WaylandPresenter::Shutdown() +{ + if (swapchain_.ctx) { + swapchain_.Destroy(); + } + if (vk_surface_ != VK_NULL_HANDLE && ctx_ && ctx_->instance != VK_NULL_HANDLE) { + vkDestroySurfaceKHR(ctx_->instance, vk_surface_, nullptr); + } + vk_surface_ = VK_NULL_HANDLE; + DestroyNative(); + configured_ = false; + closed_ = false; + ctx_ = nullptr; +} + + +bool WaylandPresenter::PumpEvents() +{ + if (!display_ || closed_) return false; + + // Non-blocking only: the Vulkan WSI owns its own wl_event_queue and reads + // the socket during FIFO presents, which also queues our default-queue + // events — dispatch whatever is pending and flush our outgoing requests. + wl_display_dispatch_pending(display_); + wl_display_flush(display_); + + if (wl_display_get_error(display_) != 0) { + PresenterLog("WaylandPresenter: display error %d — stopping", + wl_display_get_error(display_)); + return false; + } + return !closed_; +} + + +// AcquireNext / Present / RenderPass / Extent / Format inherited from +// SwapchainPresenterBase (delegate to swapchain_). + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/wayland_presenter.h b/vrto3d/src/presenter/wayland_presenter.h new file mode 100644 index 00000000..6e09151e --- /dev/null +++ b/vrto3d/src/presenter/wayland_presenter.h @@ -0,0 +1,136 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include +#include +#include + +#include "presenter/swapchain_presenter_base.h" +#include "presenter/vk_swapchain_util.h" + +// Raw libwayland-client types (from wayland-client-protocol.h and the +// generated protocol headers) — forward-declared here so the header stays +// light; the .cpp includes the real headers. +struct wl_display; +struct wl_registry; +struct wl_compositor; +struct wl_surface; +struct wl_output; +struct xdg_wm_base; +struct xdg_surface; +struct xdg_toplevel; +struct zwlr_layer_shell_v1; +struct zwlr_layer_surface_v1; + +namespace vrto3d { + +// Wayland implementation of IVkPresenter. +// +// Preferred surface role: zwlr_layer_surface_v1 on the OVERLAY layer of the +// chosen output (anchored to all four edges, exclusive_zone -1, no keyboard +// interactivity) — inherently fullscreen and topmost, no WM fights. When the +// compositor lacks wlr-layer-shell (e.g. GNOME Mutter), falls back to an +// xdg_toplevel with set_fullscreen(output). +// +// Event handling: PumpEvents() only does wl_display_dispatch_pending + +// wl_display_flush — never a blocking dispatch. The Vulkan WSI runs its own +// wl_event_queue and reads the socket during FIFO presents, which also +// queues our default-queue events for the next dispatch_pending. Blocking +// roundtrips are confined to Init(). +class WaylandPresenter final : public SwapchainPresenterBase { +public: + WaylandPresenter() = default; + ~WaylandPresenter() override; + + bool Init(vrto3d::vk::DeviceCtx* ctx, const StereoDisplayDriverConfiguration& cfg) override; + void Shutdown() override; + + bool PumpEvents() override; + // AcquireNext/Present/RenderPass/Extent/Format inherited from + // SwapchainPresenterBase (delegate to swapchain_). + const char* Name() const override { return "WaylandPresenter"; } + + // Layer-shell overlay surfaces are inherently topmost; xdg fullscreen + // z-order is compositor policy. Both are no-ops (SetAlwaysOnTop replaces + // them for the peek-at-game toggle). + void BringToTop() override {} + void ReleaseTopmost() override {} + void SetAlwaysOnTop(bool on_top) override; + void SetInputCapture(bool capture) override; + + // --- listener plumbing (public for the C callback trampolines) --- + struct OutputInfo { + wl_output* output = nullptr; + uint32_t global_name = 0; // registry name (for remove tracking) + int32_t x = 0; + int32_t y = 0; + int32_t width = 0; // current mode + int32_t height = 0; + int32_t refresh_mhz = 0; + std::string name; // wl_output.name (v4) or make/model + std::string make; + std::string model; + bool done = false; + }; + + void OnGlobal(wl_registry* registry, uint32_t name, const char* interface, uint32_t version); + void OnGlobalRemove(uint32_t name); + void OnLayerConfigure(uint32_t serial, uint32_t width, uint32_t height); + void OnLayerClosed(); + void OnXdgSurfaceConfigure(uint32_t serial); + void OnXdgToplevelConfigure(int32_t width, int32_t height); + void OnXdgToplevelClose(); + + std::vector& Outputs() { return outputs_; } + +private: + bool ConnectAndBind(); + bool CreateLayerSurface(wl_output* output); + bool CreateXdgFullscreenSurface(wl_output* output); + bool WaitForConfigure(); + void DestroyNative(); + // Empty input region on surface_ = click-through; null = capture whole + // surface. Shared by surface init and SetInputCapture. + void SetSurfaceInputRegion(bool capture); + + // ctx_ + swapchain_ live in SwapchainPresenterBase. + + wl_display* display_ = nullptr; + wl_registry* registry_ = nullptr; + wl_compositor* compositor_ = nullptr; + xdg_wm_base* wm_base_ = nullptr; + zwlr_layer_shell_v1* layer_shell_ = nullptr; + std::vector outputs_; + + wl_surface* surface_ = nullptr; + zwlr_layer_surface_v1* layer_surface_ = nullptr; + xdg_surface* xdg_surface_ = nullptr; + xdg_toplevel* xdg_toplevel_ = nullptr; + + VkSurfaceKHR vk_surface_ = VK_NULL_HANDLE; + + // Configure-driven state. + bool configured_ = false; + bool closed_ = false; + uint32_t surf_width_ = 0; // latest configure size (0 = pick our own) + uint32_t surf_height_ = 0; + int32_t pending_xdg_w_ = 0; // xdg_toplevel configure is buffered until + int32_t pending_xdg_h_ = 0; // the matching xdg_surface configure +}; + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/wayland_protocols/wlr-layer-shell-client-protocol.c b/vrto3d/src/presenter/wayland_protocols/wlr-layer-shell-client-protocol.c new file mode 100644 index 00000000..0d5fa313 --- /dev/null +++ b/vrto3d/src/presenter/wayland_protocols/wlr-layer-shell-client-protocol.c @@ -0,0 +1,94 @@ +/* Generated by wayland-scanner 1.24.0 */ + +/* + * Copyright © 2017 Drew DeVault + * + * Permission to use, copy, modify, distribute, and sell this + * software and its documentation for any purpose is hereby granted + * without fee, provided that the above copyright notice appear in + * all copies and that both that copyright notice and this permission + * notice appear in supporting documentation, and that the name of + * the copyright holders not be used in advertising or publicity + * pertaining to distribution of the software without specific, + * written prior permission. The copyright holders make no + * representations about the suitability of this software for any + * purpose. It is provided "as is" without express or implied + * warranty. + * + * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + * THIS SOFTWARE. + */ + +#include +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface wl_output_interface; +extern const struct wl_interface wl_surface_interface; +extern const struct wl_interface xdg_popup_interface; +extern const struct wl_interface zwlr_layer_surface_v1_interface; + +static const struct wl_interface *wlr_layer_shell_unstable_v1_types[] = { + NULL, + NULL, + NULL, + NULL, + &zwlr_layer_surface_v1_interface, + &wl_surface_interface, + &wl_output_interface, + NULL, + NULL, + &xdg_popup_interface, +}; + +static const struct wl_message zwlr_layer_shell_v1_requests[] = { + { "get_layer_surface", "no?ous", wlr_layer_shell_unstable_v1_types + 4 }, + { "destroy", "3", wlr_layer_shell_unstable_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwlr_layer_shell_v1_interface = { + "zwlr_layer_shell_v1", 4, + 2, zwlr_layer_shell_v1_requests, + 0, NULL, +}; + +static const struct wl_message zwlr_layer_surface_v1_requests[] = { + { "set_size", "uu", wlr_layer_shell_unstable_v1_types + 0 }, + { "set_anchor", "u", wlr_layer_shell_unstable_v1_types + 0 }, + { "set_exclusive_zone", "i", wlr_layer_shell_unstable_v1_types + 0 }, + { "set_margin", "iiii", wlr_layer_shell_unstable_v1_types + 0 }, + { "set_keyboard_interactivity", "u", wlr_layer_shell_unstable_v1_types + 0 }, + { "get_popup", "o", wlr_layer_shell_unstable_v1_types + 9 }, + { "ack_configure", "u", wlr_layer_shell_unstable_v1_types + 0 }, + { "destroy", "", wlr_layer_shell_unstable_v1_types + 0 }, + { "set_layer", "2u", wlr_layer_shell_unstable_v1_types + 0 }, +}; + +static const struct wl_message zwlr_layer_surface_v1_events[] = { + { "configure", "uuu", wlr_layer_shell_unstable_v1_types + 0 }, + { "closed", "", wlr_layer_shell_unstable_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwlr_layer_surface_v1_interface = { + "zwlr_layer_surface_v1", 4, + 9, zwlr_layer_surface_v1_requests, + 2, zwlr_layer_surface_v1_events, +}; + diff --git a/vrto3d/src/presenter/wayland_protocols/wlr-layer-shell-client-protocol.h b/vrto3d/src/presenter/wayland_protocols/wlr-layer-shell-client-protocol.h new file mode 100644 index 00000000..243cc43f --- /dev/null +++ b/vrto3d/src/presenter/wayland_protocols/wlr-layer-shell-client-protocol.h @@ -0,0 +1,704 @@ +/* Generated by wayland-scanner 1.24.0 */ + +#ifndef WLR_LAYER_SHELL_UNSTABLE_V1_CLIENT_PROTOCOL_H +#define WLR_LAYER_SHELL_UNSTABLE_V1_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_wlr_layer_shell_unstable_v1 The wlr_layer_shell_unstable_v1 protocol + * @section page_ifaces_wlr_layer_shell_unstable_v1 Interfaces + * - @subpage page_iface_zwlr_layer_shell_v1 - create surfaces that are layers of the desktop + * - @subpage page_iface_zwlr_layer_surface_v1 - layer metadata interface + * @section page_copyright_wlr_layer_shell_unstable_v1 Copyright + *
+ *
+ * Copyright © 2017 Drew DeVault
+ *
+ * Permission to use, copy, modify, distribute, and sell this
+ * software and its documentation for any purpose is hereby granted
+ * without fee, provided that the above copyright notice appear in
+ * all copies and that both that copyright notice and this permission
+ * notice appear in supporting documentation, and that the name of
+ * the copyright holders not be used in advertising or publicity
+ * pertaining to distribution of the software without specific,
+ * written prior permission.  The copyright holders make no
+ * representations about the suitability of this software for any
+ * purpose.  It is provided "as is" without express or implied
+ * warranty.
+ *
+ * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+ * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+ * THIS SOFTWARE.
+ * 
+ */ +struct wl_output; +struct wl_surface; +struct xdg_popup; +struct zwlr_layer_shell_v1; +struct zwlr_layer_surface_v1; + +#ifndef ZWLR_LAYER_SHELL_V1_INTERFACE +#define ZWLR_LAYER_SHELL_V1_INTERFACE +/** + * @page page_iface_zwlr_layer_shell_v1 zwlr_layer_shell_v1 + * @section page_iface_zwlr_layer_shell_v1_desc Description + * + * Clients can use this interface to assign the surface_layer role to + * wl_surfaces. Such surfaces are assigned to a "layer" of the output and + * rendered with a defined z-depth respective to each other. They may also be + * anchored to the edges and corners of a screen and have margins, or be + * positioned in a specific part of the screen. They are intended for + * surfaces that are drawn as part of the desktop, like panels and docks. + * @section page_iface_zwlr_layer_shell_v1_api API + * See @ref iface_zwlr_layer_shell_v1. + */ +/** + * @defgroup iface_zwlr_layer_shell_v1 The zwlr_layer_shell_v1 interface + * + * Clients can use this interface to assign the surface_layer role to + * wl_surfaces. Such surfaces are assigned to a "layer" of the output and + * rendered with a defined z-depth respective to each other. They may also be + * anchored to the edges and corners of a screen and have margins, or be + * positioned in a specific part of the screen. They are intended for + * surfaces that are drawn as part of the desktop, like panels and docks. + */ +extern const struct wl_interface zwlr_layer_shell_v1_interface; +#endif +#ifndef ZWLR_LAYER_SURFACE_V1_INTERFACE +#define ZWLR_LAYER_SURFACE_V1_INTERFACE +/** + * @page page_iface_zwlr_layer_surface_v1 zwlr_layer_surface_v1 + * @section page_iface_zwlr_layer_surface_v1_desc Description + * + * An interface that may be implemented by a wl_surface, for surfaces that + * are designed to be rendered as a layer of a stacked desktop-like + * environment. + * + * Layer surface state (layer, size, anchor, exclusive zone, + * margin, interactivity) is double-buffered, and will be applied at the + * time wl_surface.commit of the corresponding wl_surface is called. + * + * Attaching a null buffer to a layer surface unmaps it. + * + * Unmapping a layer_surface means that the surface cannot be shown by the + * compositor until it is explicitly mapped again. The layer_surface + * returns to the state it had right after layer_shell.get_layer_surface. + * The client can re-map the surface by performing a commit without any + * buffer attached, waiting for a configure event and handling it as usual. + * @section page_iface_zwlr_layer_surface_v1_api API + * See @ref iface_zwlr_layer_surface_v1. + */ +/** + * @defgroup iface_zwlr_layer_surface_v1 The zwlr_layer_surface_v1 interface + * + * An interface that may be implemented by a wl_surface, for surfaces that + * are designed to be rendered as a layer of a stacked desktop-like + * environment. + * + * Layer surface state (layer, size, anchor, exclusive zone, + * margin, interactivity) is double-buffered, and will be applied at the + * time wl_surface.commit of the corresponding wl_surface is called. + * + * Attaching a null buffer to a layer surface unmaps it. + * + * Unmapping a layer_surface means that the surface cannot be shown by the + * compositor until it is explicitly mapped again. The layer_surface + * returns to the state it had right after layer_shell.get_layer_surface. + * The client can re-map the surface by performing a commit without any + * buffer attached, waiting for a configure event and handling it as usual. + */ +extern const struct wl_interface zwlr_layer_surface_v1_interface; +#endif + +#ifndef ZWLR_LAYER_SHELL_V1_ERROR_ENUM +#define ZWLR_LAYER_SHELL_V1_ERROR_ENUM +enum zwlr_layer_shell_v1_error { + /** + * wl_surface has another role + */ + ZWLR_LAYER_SHELL_V1_ERROR_ROLE = 0, + /** + * layer value is invalid + */ + ZWLR_LAYER_SHELL_V1_ERROR_INVALID_LAYER = 1, + /** + * wl_surface has a buffer attached or committed + */ + ZWLR_LAYER_SHELL_V1_ERROR_ALREADY_CONSTRUCTED = 2, +}; +#endif /* ZWLR_LAYER_SHELL_V1_ERROR_ENUM */ + +#ifndef ZWLR_LAYER_SHELL_V1_LAYER_ENUM +#define ZWLR_LAYER_SHELL_V1_LAYER_ENUM +/** + * @ingroup iface_zwlr_layer_shell_v1 + * available layers for surfaces + * + * These values indicate which layers a surface can be rendered in. They + * are ordered by z depth, bottom-most first. Traditional shell surfaces + * will typically be rendered between the bottom and top layers. + * Fullscreen shell surfaces are typically rendered at the top layer. + * Multiple surfaces can share a single layer, and ordering within a + * single layer is undefined. + */ +enum zwlr_layer_shell_v1_layer { + ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND = 0, + ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM = 1, + ZWLR_LAYER_SHELL_V1_LAYER_TOP = 2, + ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY = 3, +}; +#endif /* ZWLR_LAYER_SHELL_V1_LAYER_ENUM */ + +#define ZWLR_LAYER_SHELL_V1_GET_LAYER_SURFACE 0 +#define ZWLR_LAYER_SHELL_V1_DESTROY 1 + + +/** + * @ingroup iface_zwlr_layer_shell_v1 + */ +#define ZWLR_LAYER_SHELL_V1_GET_LAYER_SURFACE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_shell_v1 + */ +#define ZWLR_LAYER_SHELL_V1_DESTROY_SINCE_VERSION 3 + +/** @ingroup iface_zwlr_layer_shell_v1 */ +static inline void +zwlr_layer_shell_v1_set_user_data(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwlr_layer_shell_v1, user_data); +} + +/** @ingroup iface_zwlr_layer_shell_v1 */ +static inline void * +zwlr_layer_shell_v1_get_user_data(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwlr_layer_shell_v1); +} + +static inline uint32_t +zwlr_layer_shell_v1_get_version(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zwlr_layer_shell_v1); +} + +/** + * @ingroup iface_zwlr_layer_shell_v1 + * + * Create a layer surface for an existing surface. This assigns the role of + * layer_surface, or raises a protocol error if another role is already + * assigned. + * + * Creating a layer surface from a wl_surface which has a buffer attached + * or committed is a client error, and any attempts by a client to attach + * or manipulate a buffer prior to the first layer_surface.configure call + * must also be treated as errors. + * + * After creating a layer_surface object and setting it up, the client + * must perform an initial commit without any buffer attached. + * The compositor will reply with a layer_surface.configure event. + * The client must acknowledge it and is then allowed to attach a buffer + * to map the surface. + * + * You may pass NULL for output to allow the compositor to decide which + * output to use. Generally this will be the one that the user most + * recently interacted with. + * + * Clients can specify a namespace that defines the purpose of the layer + * surface. + */ +static inline struct zwlr_layer_surface_v1 * +zwlr_layer_shell_v1_get_layer_surface(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1, struct wl_surface *surface, struct wl_output *output, uint32_t layer, const char *namespace) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_shell_v1, + ZWLR_LAYER_SHELL_V1_GET_LAYER_SURFACE, &zwlr_layer_surface_v1_interface, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_shell_v1), 0, NULL, surface, output, layer, namespace); + + return (struct zwlr_layer_surface_v1 *) id; +} + +/** + * @ingroup iface_zwlr_layer_shell_v1 + * + * This request indicates that the client will not use the layer_shell + * object any more. Objects that have been created through this instance + * are not affected. + */ +static inline void +zwlr_layer_shell_v1_destroy(struct zwlr_layer_shell_v1 *zwlr_layer_shell_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_shell_v1, + ZWLR_LAYER_SHELL_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_shell_v1), WL_MARSHAL_FLAG_DESTROY); +} + +#ifndef ZWLR_LAYER_SURFACE_V1_ERROR_ENUM +#define ZWLR_LAYER_SURFACE_V1_ERROR_ENUM +enum zwlr_layer_surface_v1_error { + /** + * provided surface state is invalid + */ + ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_SURFACE_STATE = 0, + /** + * size is invalid + */ + ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_SIZE = 1, + /** + * anchor bitfield is invalid + */ + ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_ANCHOR = 2, + /** + * keyboard interactivity is invalid + */ + ZWLR_LAYER_SURFACE_V1_ERROR_INVALID_KEYBOARD_INTERACTIVITY = 3, +}; +#endif /* ZWLR_LAYER_SURFACE_V1_ERROR_ENUM */ + +#ifndef ZWLR_LAYER_SURFACE_V1_ANCHOR_ENUM +#define ZWLR_LAYER_SURFACE_V1_ANCHOR_ENUM +enum zwlr_layer_surface_v1_anchor { + /** + * the top edge of the anchor rectangle + */ + ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP = 1, + /** + * the bottom edge of the anchor rectangle + */ + ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM = 2, + /** + * the left edge of the anchor rectangle + */ + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT = 4, + /** + * the right edge of the anchor rectangle + */ + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT = 8, +}; +#endif /* ZWLR_LAYER_SURFACE_V1_ANCHOR_ENUM */ + +#ifndef ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ENUM +#define ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ENUM +/** + * @ingroup iface_zwlr_layer_surface_v1 + * types of keyboard interaction possible for a layer shell surface + * + * Types of keyboard interaction possible for layer shell surfaces. The + * rationale for this is twofold: (1) some applications are not interested + * in keyboard events and not allowing them to be focused can improve the + * desktop experience; (2) some applications will want to take exclusive + * keyboard focus. + */ +enum zwlr_layer_surface_v1_keyboard_interactivity { + /** + * no keyboard focus is possible + * + * This value indicates that this surface is not interested in + * keyboard events and the compositor should never assign it the + * keyboard focus. + * + * This is the default value, set for newly created layer shell + * surfaces. + * + * This is useful for e.g. desktop widgets that display information + * or only have interaction with non-keyboard input devices. + */ + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE = 0, + /** + * request exclusive keyboard focus + * + * Request exclusive keyboard focus if this surface is above the + * shell surface layer. + * + * For the top and overlay layers, the seat will always give + * exclusive keyboard focus to the top-most layer which has + * keyboard interactivity set to exclusive. If this layer contains + * multiple surfaces with keyboard interactivity set to exclusive, + * the compositor determines the one receiving keyboard events in + * an implementation- defined manner. In this case, no guarantee is + * made when this surface will receive keyboard focus (if ever). + * + * For the bottom and background layers, the compositor is allowed + * to use normal focus semantics. + * + * This setting is mainly intended for applications that need to + * ensure they receive all keyboard events, such as as lock screens + * or on-screen keyboards. + */ + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE = 1, + /** + * request regular keyboard focus semantics + * + * This requests the compositor to allow this surface to be + * focused and unfocused by the user in an implementation-defined + * manner. The user should be able to unfocus this surface even + * regardless of the layer it is on. + * + * Typically, the compositor will want to use its normal mechanism + * to manage keyboard focus between layer shell surfaces with this + * setting and regular toplevels on the desktop layer (e.g. click + * to focus). Nevertheless, it is possible for a compositor to + * require a special interaction to focus or unfocus layer shell + * surfaces (e.g. requiring a click even if focus follows the mouse + * normally, or providing a keybinding to switch focus between + * layers). + * + * This setting is mainly intended for desktop shell components + * (e.g. panels) that allow keyboard interaction. Using this option + * can allow implementing a desktop shell that can be fully usable + * without the mouse. + * @since 4 + */ + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND = 2, +}; +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND_SINCE_VERSION 4 +#endif /* ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ENUM */ + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * @struct zwlr_layer_surface_v1_listener + */ +struct zwlr_layer_surface_v1_listener { + /** + * suggest a surface change + * + * The configure event asks the client to resize its surface. + * + * Clients should arrange their surface for the new states, and + * then send an ack_configure request with the serial sent in this + * configure event at some point before committing the new surface. + * + * The client is free to dismiss all but the last configure event + * it received. + * + * The width and height arguments specify the size of the window in + * surface-local coordinates. + * + * The size is a hint, in the sense that the client is free to + * ignore it if it doesn't resize, pick a smaller size (to satisfy + * aspect ratio or resize in steps of NxM pixels). If the client + * picks a smaller size and is anchored to two opposite anchors + * (e.g. 'top' and 'bottom'), the surface will be centered on this + * axis. + * + * If the width or height arguments are zero, it means the client + * should decide its own window dimension. + */ + void (*configure)(void *data, + struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, + uint32_t serial, + uint32_t width, + uint32_t height); + /** + * surface should be closed + * + * The closed event is sent by the compositor when the surface + * will no longer be shown. The output may have been destroyed or + * the user may have asked for it to be removed. Further changes to + * the surface will be ignored. The client should destroy the + * resource after receiving this event, and create a new surface if + * they so choose. + */ + void (*closed)(void *data, + struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1); +}; + +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +static inline int +zwlr_layer_surface_v1_add_listener(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, + const struct zwlr_layer_surface_v1_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) zwlr_layer_surface_v1, + (void (**)(void)) listener, data); +} + +#define ZWLR_LAYER_SURFACE_V1_SET_SIZE 0 +#define ZWLR_LAYER_SURFACE_V1_SET_ANCHOR 1 +#define ZWLR_LAYER_SURFACE_V1_SET_EXCLUSIVE_ZONE 2 +#define ZWLR_LAYER_SURFACE_V1_SET_MARGIN 3 +#define ZWLR_LAYER_SURFACE_V1_SET_KEYBOARD_INTERACTIVITY 4 +#define ZWLR_LAYER_SURFACE_V1_GET_POPUP 5 +#define ZWLR_LAYER_SURFACE_V1_ACK_CONFIGURE 6 +#define ZWLR_LAYER_SURFACE_V1_DESTROY 7 +#define ZWLR_LAYER_SURFACE_V1_SET_LAYER 8 + +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_CONFIGURE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_CLOSED_SINCE_VERSION 1 + +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_SET_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_SET_ANCHOR_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_SET_EXCLUSIVE_ZONE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_SET_MARGIN_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_SET_KEYBOARD_INTERACTIVITY_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_GET_POPUP_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_ACK_CONFIGURE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_layer_surface_v1 + */ +#define ZWLR_LAYER_SURFACE_V1_SET_LAYER_SINCE_VERSION 2 + +/** @ingroup iface_zwlr_layer_surface_v1 */ +static inline void +zwlr_layer_surface_v1_set_user_data(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwlr_layer_surface_v1, user_data); +} + +/** @ingroup iface_zwlr_layer_surface_v1 */ +static inline void * +zwlr_layer_surface_v1_get_user_data(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwlr_layer_surface_v1); +} + +static inline uint32_t +zwlr_layer_surface_v1_get_version(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * Sets the size of the surface in surface-local coordinates. The + * compositor will display the surface centered with respect to its + * anchors. + * + * If you pass 0 for either value, the compositor will assign it and + * inform you of the assignment in the configure event. You must set your + * anchor to opposite edges in the dimensions you omit; not doing so is a + * protocol error. Both values are 0 by default. + * + * Size is double-buffered, see wl_surface.commit. + */ +static inline void +zwlr_layer_surface_v1_set_size(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t width, uint32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_SET_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, width, height); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * Requests that the compositor anchor the surface to the specified edges + * and corners. If two orthogonal edges are specified (e.g. 'top' and + * 'left'), then the anchor point will be the intersection of the edges + * (e.g. the top left corner of the output); otherwise the anchor point + * will be centered on that edge, or in the center if none is specified. + * + * Anchor is double-buffered, see wl_surface.commit. + */ +static inline void +zwlr_layer_surface_v1_set_anchor(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t anchor) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_SET_ANCHOR, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, anchor); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * Requests that the compositor avoids occluding an area with other + * surfaces. The compositor's use of this information is + * implementation-dependent - do not assume that this region will not + * actually be occluded. + * + * A positive value is only meaningful if the surface is anchored to one + * edge or an edge and both perpendicular edges. If the surface is not + * anchored, anchored to only two perpendicular edges (a corner), anchored + * to only two parallel edges or anchored to all edges, a positive value + * will be treated the same as zero. + * + * A positive zone is the distance from the edge in surface-local + * coordinates to consider exclusive. + * + * Surfaces that do not wish to have an exclusive zone may instead specify + * how they should interact with surfaces that do. If set to zero, the + * surface indicates that it would like to be moved to avoid occluding + * surfaces with a positive exclusive zone. If set to -1, the surface + * indicates that it would not like to be moved to accommodate for other + * surfaces, and the compositor should extend it all the way to the edges + * it is anchored to. + * + * For example, a panel might set its exclusive zone to 10, so that + * maximized shell surfaces are not shown on top of it. A notification + * might set its exclusive zone to 0, so that it is moved to avoid + * occluding the panel, but shell surfaces are shown underneath it. A + * wallpaper or lock screen might set their exclusive zone to -1, so that + * they stretch below or over the panel. + * + * The default value is 0. + * + * Exclusive zone is double-buffered, see wl_surface.commit. + */ +static inline void +zwlr_layer_surface_v1_set_exclusive_zone(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, int32_t zone) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_SET_EXCLUSIVE_ZONE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, zone); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * Requests that the surface be placed some distance away from the anchor + * point on the output, in surface-local coordinates. Setting this value + * for edges you are not anchored to has no effect. + * + * The exclusive zone includes the margin. + * + * Margin is double-buffered, see wl_surface.commit. + */ +static inline void +zwlr_layer_surface_v1_set_margin(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, int32_t top, int32_t right, int32_t bottom, int32_t left) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_SET_MARGIN, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, top, right, bottom, left); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * Set how keyboard events are delivered to this surface. By default, + * layer shell surfaces do not receive keyboard events; this request can + * be used to change this. + * + * This setting is inherited by child surfaces set by the get_popup + * request. + * + * Layer surfaces receive pointer, touch, and tablet events normally. If + * you do not want to receive them, set the input region on your surface + * to an empty region. + * + * Keyboard interactivity is double-buffered, see wl_surface.commit. + */ +static inline void +zwlr_layer_surface_v1_set_keyboard_interactivity(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t keyboard_interactivity) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_SET_KEYBOARD_INTERACTIVITY, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, keyboard_interactivity); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * This assigns an xdg_popup's parent to this layer_surface. This popup + * should have been created via xdg_surface::get_popup with the parent set + * to NULL, and this request must be invoked before committing the popup's + * initial state. + * + * See the documentation of xdg_popup for more details about what an + * xdg_popup is and how it is used. + */ +static inline void +zwlr_layer_surface_v1_get_popup(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, struct xdg_popup *popup) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_GET_POPUP, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, popup); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * When a configure event is received, if a client commits the + * surface in response to the configure event, then the client + * must make an ack_configure request sometime before the commit + * request, passing along the serial of the configure event. + * + * If the client receives multiple configure events before it + * can respond to one, it only has to ack the last configure event. + * + * A client is not required to commit immediately after sending + * an ack_configure request - it may even ack_configure several times + * before its next surface commit. + * + * A client may send multiple ack_configure requests before committing, but + * only the last request sent before a commit indicates which configure + * event the client really is responding to. + */ +static inline void +zwlr_layer_surface_v1_ack_configure(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_ACK_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, serial); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * This request destroys the layer surface. + */ +static inline void +zwlr_layer_surface_v1_destroy(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_zwlr_layer_surface_v1 + * + * Change the layer that the surface is rendered on. + * + * Layer is double-buffered, see wl_surface.commit. + */ +static inline void +zwlr_layer_surface_v1_set_layer(struct zwlr_layer_surface_v1 *zwlr_layer_surface_v1, uint32_t layer) +{ + wl_proxy_marshal_flags((struct wl_proxy *) zwlr_layer_surface_v1, + ZWLR_LAYER_SURFACE_V1_SET_LAYER, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_layer_surface_v1), 0, layer); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/vrto3d/src/presenter/wayland_protocols/xdg-shell-client-protocol.c b/vrto3d/src/presenter/wayland_protocols/xdg-shell-client-protocol.c new file mode 100644 index 00000000..5a50433b --- /dev/null +++ b/vrto3d/src/presenter/wayland_protocols/xdg-shell-client-protocol.c @@ -0,0 +1,184 @@ +/* Generated by wayland-scanner 1.24.0 */ + +/* + * Copyright © 2008-2013 Kristian Høgsberg + * Copyright © 2013 Rafael Antognolli + * Copyright © 2013 Jasper St. Pierre + * Copyright © 2010-2013 Intel Corporation + * Copyright © 2015-2017 Samsung Electronics Co., Ltd + * Copyright © 2015-2017 Red Hat Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface wl_output_interface; +extern const struct wl_interface wl_seat_interface; +extern const struct wl_interface wl_surface_interface; +extern const struct wl_interface xdg_popup_interface; +extern const struct wl_interface xdg_positioner_interface; +extern const struct wl_interface xdg_surface_interface; +extern const struct wl_interface xdg_toplevel_interface; + +static const struct wl_interface *xdg_shell_types[] = { + NULL, + NULL, + NULL, + NULL, + &xdg_positioner_interface, + &xdg_surface_interface, + &wl_surface_interface, + &xdg_toplevel_interface, + &xdg_popup_interface, + &xdg_surface_interface, + &xdg_positioner_interface, + &xdg_toplevel_interface, + &wl_seat_interface, + NULL, + NULL, + NULL, + &wl_seat_interface, + NULL, + &wl_seat_interface, + NULL, + NULL, + &wl_output_interface, + &wl_seat_interface, + NULL, + &xdg_positioner_interface, + NULL, +}; + +static const struct wl_message xdg_wm_base_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "create_positioner", "n", xdg_shell_types + 4 }, + { "get_xdg_surface", "no", xdg_shell_types + 5 }, + { "pong", "u", xdg_shell_types + 0 }, +}; + +static const struct wl_message xdg_wm_base_events[] = { + { "ping", "u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_wm_base_interface = { + "xdg_wm_base", 7, + 4, xdg_wm_base_requests, + 1, xdg_wm_base_events, +}; + +static const struct wl_message xdg_positioner_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "set_size", "ii", xdg_shell_types + 0 }, + { "set_anchor_rect", "iiii", xdg_shell_types + 0 }, + { "set_anchor", "u", xdg_shell_types + 0 }, + { "set_gravity", "u", xdg_shell_types + 0 }, + { "set_constraint_adjustment", "u", xdg_shell_types + 0 }, + { "set_offset", "ii", xdg_shell_types + 0 }, + { "set_reactive", "3", xdg_shell_types + 0 }, + { "set_parent_size", "3ii", xdg_shell_types + 0 }, + { "set_parent_configure", "3u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_positioner_interface = { + "xdg_positioner", 7, + 10, xdg_positioner_requests, + 0, NULL, +}; + +static const struct wl_message xdg_surface_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "get_toplevel", "n", xdg_shell_types + 7 }, + { "get_popup", "n?oo", xdg_shell_types + 8 }, + { "set_window_geometry", "iiii", xdg_shell_types + 0 }, + { "ack_configure", "u", xdg_shell_types + 0 }, +}; + +static const struct wl_message xdg_surface_events[] = { + { "configure", "u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_surface_interface = { + "xdg_surface", 7, + 5, xdg_surface_requests, + 1, xdg_surface_events, +}; + +static const struct wl_message xdg_toplevel_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "set_parent", "?o", xdg_shell_types + 11 }, + { "set_title", "s", xdg_shell_types + 0 }, + { "set_app_id", "s", xdg_shell_types + 0 }, + { "show_window_menu", "ouii", xdg_shell_types + 12 }, + { "move", "ou", xdg_shell_types + 16 }, + { "resize", "ouu", xdg_shell_types + 18 }, + { "set_max_size", "ii", xdg_shell_types + 0 }, + { "set_min_size", "ii", xdg_shell_types + 0 }, + { "set_maximized", "", xdg_shell_types + 0 }, + { "unset_maximized", "", xdg_shell_types + 0 }, + { "set_fullscreen", "?o", xdg_shell_types + 21 }, + { "unset_fullscreen", "", xdg_shell_types + 0 }, + { "set_minimized", "", xdg_shell_types + 0 }, +}; + +static const struct wl_message xdg_toplevel_events[] = { + { "configure", "iia", xdg_shell_types + 0 }, + { "close", "", xdg_shell_types + 0 }, + { "configure_bounds", "4ii", xdg_shell_types + 0 }, + { "wm_capabilities", "5a", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_toplevel_interface = { + "xdg_toplevel", 7, + 14, xdg_toplevel_requests, + 4, xdg_toplevel_events, +}; + +static const struct wl_message xdg_popup_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "grab", "ou", xdg_shell_types + 22 }, + { "reposition", "3ou", xdg_shell_types + 24 }, +}; + +static const struct wl_message xdg_popup_events[] = { + { "configure", "iiii", xdg_shell_types + 0 }, + { "popup_done", "", xdg_shell_types + 0 }, + { "repositioned", "3u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_popup_interface = { + "xdg_popup", 7, + 3, xdg_popup_requests, + 3, xdg_popup_events, +}; + diff --git a/vrto3d/src/presenter/wayland_protocols/xdg-shell-client-protocol.h b/vrto3d/src/presenter/wayland_protocols/xdg-shell-client-protocol.h new file mode 100644 index 00000000..5a2ed89f --- /dev/null +++ b/vrto3d/src/presenter/wayland_protocols/xdg-shell-client-protocol.h @@ -0,0 +1,2384 @@ +/* Generated by wayland-scanner 1.24.0 */ + +#ifndef XDG_SHELL_CLIENT_PROTOCOL_H +#define XDG_SHELL_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_xdg_shell The xdg_shell protocol + * @section page_ifaces_xdg_shell Interfaces + * - @subpage page_iface_xdg_wm_base - create desktop-style surfaces + * - @subpage page_iface_xdg_positioner - child surface positioner + * - @subpage page_iface_xdg_surface - desktop user interface surface base interface + * - @subpage page_iface_xdg_toplevel - toplevel surface + * - @subpage page_iface_xdg_popup - short-lived, popup surfaces for menus + * @section page_copyright_xdg_shell Copyright + *
+ *
+ * Copyright © 2008-2013 Kristian Høgsberg
+ * Copyright © 2013      Rafael Antognolli
+ * Copyright © 2013      Jasper St. Pierre
+ * Copyright © 2010-2013 Intel Corporation
+ * Copyright © 2015-2017 Samsung Electronics Co., Ltd
+ * Copyright © 2015-2017 Red Hat Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * 
+ */ +struct wl_output; +struct wl_seat; +struct wl_surface; +struct xdg_popup; +struct xdg_positioner; +struct xdg_surface; +struct xdg_toplevel; +struct xdg_wm_base; + +#ifndef XDG_WM_BASE_INTERFACE +#define XDG_WM_BASE_INTERFACE +/** + * @page page_iface_xdg_wm_base xdg_wm_base + * @section page_iface_xdg_wm_base_desc Description + * + * The xdg_wm_base interface is exposed as a global object enabling clients + * to turn their wl_surfaces into windows in a desktop environment. It + * defines the basic functionality needed for clients and the compositor to + * create windows that can be dragged, resized, maximized, etc, as well as + * creating transient windows such as popup menus. + * @section page_iface_xdg_wm_base_api API + * See @ref iface_xdg_wm_base. + */ +/** + * @defgroup iface_xdg_wm_base The xdg_wm_base interface + * + * The xdg_wm_base interface is exposed as a global object enabling clients + * to turn their wl_surfaces into windows in a desktop environment. It + * defines the basic functionality needed for clients and the compositor to + * create windows that can be dragged, resized, maximized, etc, as well as + * creating transient windows such as popup menus. + */ +extern const struct wl_interface xdg_wm_base_interface; +#endif +#ifndef XDG_POSITIONER_INTERFACE +#define XDG_POSITIONER_INTERFACE +/** + * @page page_iface_xdg_positioner xdg_positioner + * @section page_iface_xdg_positioner_desc Description + * + * The xdg_positioner provides a collection of rules for the placement of a + * child surface relative to a parent surface. Rules can be defined to ensure + * the child surface remains within the visible area's borders, and to + * specify how the child surface changes its position, such as sliding along + * an axis, or flipping around a rectangle. These positioner-created rules are + * constrained by the requirement that a child surface must intersect with or + * be at least partially adjacent to its parent surface. + * + * See the various requests for details about possible rules. + * + * At the time of the request, the compositor makes a copy of the rules + * specified by the xdg_positioner. Thus, after the request is complete the + * xdg_positioner object can be destroyed or reused; further changes to the + * object will have no effect on previous usages. + * + * For an xdg_positioner object to be considered complete, it must have a + * non-zero size set by set_size, and a non-zero anchor rectangle set by + * set_anchor_rect. Passing an incomplete xdg_positioner object when + * positioning a surface raises an invalid_positioner error. + * @section page_iface_xdg_positioner_api API + * See @ref iface_xdg_positioner. + */ +/** + * @defgroup iface_xdg_positioner The xdg_positioner interface + * + * The xdg_positioner provides a collection of rules for the placement of a + * child surface relative to a parent surface. Rules can be defined to ensure + * the child surface remains within the visible area's borders, and to + * specify how the child surface changes its position, such as sliding along + * an axis, or flipping around a rectangle. These positioner-created rules are + * constrained by the requirement that a child surface must intersect with or + * be at least partially adjacent to its parent surface. + * + * See the various requests for details about possible rules. + * + * At the time of the request, the compositor makes a copy of the rules + * specified by the xdg_positioner. Thus, after the request is complete the + * xdg_positioner object can be destroyed or reused; further changes to the + * object will have no effect on previous usages. + * + * For an xdg_positioner object to be considered complete, it must have a + * non-zero size set by set_size, and a non-zero anchor rectangle set by + * set_anchor_rect. Passing an incomplete xdg_positioner object when + * positioning a surface raises an invalid_positioner error. + */ +extern const struct wl_interface xdg_positioner_interface; +#endif +#ifndef XDG_SURFACE_INTERFACE +#define XDG_SURFACE_INTERFACE +/** + * @page page_iface_xdg_surface xdg_surface + * @section page_iface_xdg_surface_desc Description + * + * An interface that may be implemented by a wl_surface, for + * implementations that provide a desktop-style user interface. + * + * It provides a base set of functionality required to construct user + * interface elements requiring management by the compositor, such as + * toplevel windows, menus, etc. The types of functionality are split into + * xdg_surface roles. + * + * Creating an xdg_surface does not set the role for a wl_surface. In order + * to map an xdg_surface, the client must create a role-specific object + * using, e.g., get_toplevel, get_popup. The wl_surface for any given + * xdg_surface can have at most one role, and may not be assigned any role + * not based on xdg_surface. + * + * A role must be assigned before any other requests are made to the + * xdg_surface object. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_surface state to take effect. + * + * Creating an xdg_surface from a wl_surface which has a buffer attached or + * committed is a client error, and any attempts by a client to attach or + * manipulate a buffer prior to the first xdg_surface.configure call must + * also be treated as errors. + * + * After creating a role-specific object and setting it up (e.g. by sending + * the title, app ID, size constraints, parent, etc), the client must + * perform an initial commit without any buffer attached. The compositor + * will reply with initial wl_surface state such as + * wl_surface.preferred_buffer_scale followed by an xdg_surface.configure + * event. The client must acknowledge it and is then allowed to attach a + * buffer to map the surface. + * + * Mapping an xdg_surface-based role surface is defined as making it + * possible for the surface to be shown by the compositor. Note that + * a mapped surface is not guaranteed to be visible once it is mapped. + * + * For an xdg_surface to be mapped by the compositor, the following + * conditions must be met: + * (1) the client has assigned an xdg_surface-based role to the surface + * (2) the client has set and committed the xdg_surface state and the + * role-dependent state to the surface + * (3) the client has committed a buffer to the surface + * + * A newly-unmapped surface is considered to have met condition (1) out + * of the 3 required conditions for mapping a surface if its role surface + * has not been destroyed, i.e. the client must perform the initial commit + * again before attaching a buffer. + * @section page_iface_xdg_surface_api API + * See @ref iface_xdg_surface. + */ +/** + * @defgroup iface_xdg_surface The xdg_surface interface + * + * An interface that may be implemented by a wl_surface, for + * implementations that provide a desktop-style user interface. + * + * It provides a base set of functionality required to construct user + * interface elements requiring management by the compositor, such as + * toplevel windows, menus, etc. The types of functionality are split into + * xdg_surface roles. + * + * Creating an xdg_surface does not set the role for a wl_surface. In order + * to map an xdg_surface, the client must create a role-specific object + * using, e.g., get_toplevel, get_popup. The wl_surface for any given + * xdg_surface can have at most one role, and may not be assigned any role + * not based on xdg_surface. + * + * A role must be assigned before any other requests are made to the + * xdg_surface object. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_surface state to take effect. + * + * Creating an xdg_surface from a wl_surface which has a buffer attached or + * committed is a client error, and any attempts by a client to attach or + * manipulate a buffer prior to the first xdg_surface.configure call must + * also be treated as errors. + * + * After creating a role-specific object and setting it up (e.g. by sending + * the title, app ID, size constraints, parent, etc), the client must + * perform an initial commit without any buffer attached. The compositor + * will reply with initial wl_surface state such as + * wl_surface.preferred_buffer_scale followed by an xdg_surface.configure + * event. The client must acknowledge it and is then allowed to attach a + * buffer to map the surface. + * + * Mapping an xdg_surface-based role surface is defined as making it + * possible for the surface to be shown by the compositor. Note that + * a mapped surface is not guaranteed to be visible once it is mapped. + * + * For an xdg_surface to be mapped by the compositor, the following + * conditions must be met: + * (1) the client has assigned an xdg_surface-based role to the surface + * (2) the client has set and committed the xdg_surface state and the + * role-dependent state to the surface + * (3) the client has committed a buffer to the surface + * + * A newly-unmapped surface is considered to have met condition (1) out + * of the 3 required conditions for mapping a surface if its role surface + * has not been destroyed, i.e. the client must perform the initial commit + * again before attaching a buffer. + */ +extern const struct wl_interface xdg_surface_interface; +#endif +#ifndef XDG_TOPLEVEL_INTERFACE +#define XDG_TOPLEVEL_INTERFACE +/** + * @page page_iface_xdg_toplevel xdg_toplevel + * @section page_iface_xdg_toplevel_desc Description + * + * This interface defines an xdg_surface role which allows a surface to, + * among other things, set window-like properties such as maximize, + * fullscreen, and minimize, set application-specific metadata like title and + * id, and well as trigger user interactive operations such as interactive + * resize and move. + * + * A xdg_toplevel by default is responsible for providing the full intended + * visual representation of the toplevel, which depending on the window + * state, may mean things like a title bar, window controls and drop shadow. + * + * Unmapping an xdg_toplevel means that the surface cannot be shown + * by the compositor until it is explicitly mapped again. + * All active operations (e.g., move, resize) are canceled and all + * attributes (e.g. title, state, stacking, ...) are discarded for + * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + * the state it had right after xdg_surface.get_toplevel. The client + * can re-map the toplevel by performing a commit without any buffer + * attached, waiting for a configure event and handling it as usual (see + * xdg_surface description). + * + * Attaching a null buffer to a toplevel unmaps the surface. + * @section page_iface_xdg_toplevel_api API + * See @ref iface_xdg_toplevel. + */ +/** + * @defgroup iface_xdg_toplevel The xdg_toplevel interface + * + * This interface defines an xdg_surface role which allows a surface to, + * among other things, set window-like properties such as maximize, + * fullscreen, and minimize, set application-specific metadata like title and + * id, and well as trigger user interactive operations such as interactive + * resize and move. + * + * A xdg_toplevel by default is responsible for providing the full intended + * visual representation of the toplevel, which depending on the window + * state, may mean things like a title bar, window controls and drop shadow. + * + * Unmapping an xdg_toplevel means that the surface cannot be shown + * by the compositor until it is explicitly mapped again. + * All active operations (e.g., move, resize) are canceled and all + * attributes (e.g. title, state, stacking, ...) are discarded for + * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + * the state it had right after xdg_surface.get_toplevel. The client + * can re-map the toplevel by performing a commit without any buffer + * attached, waiting for a configure event and handling it as usual (see + * xdg_surface description). + * + * Attaching a null buffer to a toplevel unmaps the surface. + */ +extern const struct wl_interface xdg_toplevel_interface; +#endif +#ifndef XDG_POPUP_INTERFACE +#define XDG_POPUP_INTERFACE +/** + * @page page_iface_xdg_popup xdg_popup + * @section page_iface_xdg_popup_desc Description + * + * A popup surface is a short-lived, temporary surface. It can be used to + * implement for example menus, popovers, tooltips and other similar user + * interface concepts. + * + * A popup can be made to take an explicit grab. See xdg_popup.grab for + * details. + * + * When the popup is dismissed, a popup_done event will be sent out, and at + * the same time the surface will be unmapped. See the xdg_popup.popup_done + * event for details. + * + * Explicitly destroying the xdg_popup object will also dismiss the popup and + * unmap the surface. Clients that want to dismiss the popup when another + * surface of their own is clicked should dismiss the popup using the destroy + * request. + * + * A newly created xdg_popup will be stacked on top of all previously created + * xdg_popup surfaces associated with the same xdg_toplevel. + * + * The parent of an xdg_popup must be mapped (see the xdg_surface + * description) before the xdg_popup itself. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_popup state to take effect. + * @section page_iface_xdg_popup_api API + * See @ref iface_xdg_popup. + */ +/** + * @defgroup iface_xdg_popup The xdg_popup interface + * + * A popup surface is a short-lived, temporary surface. It can be used to + * implement for example menus, popovers, tooltips and other similar user + * interface concepts. + * + * A popup can be made to take an explicit grab. See xdg_popup.grab for + * details. + * + * When the popup is dismissed, a popup_done event will be sent out, and at + * the same time the surface will be unmapped. See the xdg_popup.popup_done + * event for details. + * + * Explicitly destroying the xdg_popup object will also dismiss the popup and + * unmap the surface. Clients that want to dismiss the popup when another + * surface of their own is clicked should dismiss the popup using the destroy + * request. + * + * A newly created xdg_popup will be stacked on top of all previously created + * xdg_popup surfaces associated with the same xdg_toplevel. + * + * The parent of an xdg_popup must be mapped (see the xdg_surface + * description) before the xdg_popup itself. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_popup state to take effect. + */ +extern const struct wl_interface xdg_popup_interface; +#endif + +#ifndef XDG_WM_BASE_ERROR_ENUM +#define XDG_WM_BASE_ERROR_ENUM +enum xdg_wm_base_error { + /** + * given wl_surface has another role + */ + XDG_WM_BASE_ERROR_ROLE = 0, + /** + * xdg_wm_base was destroyed before children + */ + XDG_WM_BASE_ERROR_DEFUNCT_SURFACES = 1, + /** + * the client tried to map or destroy a non-topmost popup + */ + XDG_WM_BASE_ERROR_NOT_THE_TOPMOST_POPUP = 2, + /** + * the client specified an invalid popup parent surface + */ + XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT = 3, + /** + * the client provided an invalid surface state + */ + XDG_WM_BASE_ERROR_INVALID_SURFACE_STATE = 4, + /** + * the client provided an invalid positioner + */ + XDG_WM_BASE_ERROR_INVALID_POSITIONER = 5, + /** + * the client didn’t respond to a ping event in time + */ + XDG_WM_BASE_ERROR_UNRESPONSIVE = 6, +}; +#endif /* XDG_WM_BASE_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_wm_base + * @struct xdg_wm_base_listener + */ +struct xdg_wm_base_listener { + /** + * check if the client is alive + * + * The ping event asks the client if it's still alive. Pass the + * serial specified in the event back to the compositor by sending + * a "pong" request back with the specified serial. See + * xdg_wm_base.pong. + * + * Compositors can use this to determine if the client is still + * alive. It's unspecified what will happen if the client doesn't + * respond to the ping request, or in what timeframe. Clients + * should try to respond in a reasonable amount of time. The + * “unresponsive” error is provided for compositors that wish + * to disconnect unresponsive clients. + * + * A compositor is free to ping in any way it wants, but a client + * must always respond to any xdg_wm_base object it created. + * @param serial pass this to the pong request + */ + void (*ping)(void *data, + struct xdg_wm_base *xdg_wm_base, + uint32_t serial); +}; + +/** + * @ingroup iface_xdg_wm_base + */ +static inline int +xdg_wm_base_add_listener(struct xdg_wm_base *xdg_wm_base, + const struct xdg_wm_base_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_wm_base, + (void (**)(void)) listener, data); +} + +#define XDG_WM_BASE_DESTROY 0 +#define XDG_WM_BASE_CREATE_POSITIONER 1 +#define XDG_WM_BASE_GET_XDG_SURFACE 2 +#define XDG_WM_BASE_PONG 3 + +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_PING_SINCE_VERSION 1 + +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_CREATE_POSITIONER_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_GET_XDG_SURFACE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_PONG_SINCE_VERSION 1 + +/** @ingroup iface_xdg_wm_base */ +static inline void +xdg_wm_base_set_user_data(struct xdg_wm_base *xdg_wm_base, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_wm_base, user_data); +} + +/** @ingroup iface_xdg_wm_base */ +static inline void * +xdg_wm_base_get_user_data(struct xdg_wm_base *xdg_wm_base) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_wm_base); +} + +static inline uint32_t +xdg_wm_base_get_version(struct xdg_wm_base *xdg_wm_base) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_wm_base); +} + +/** + * @ingroup iface_xdg_wm_base + * + * Destroy this xdg_wm_base object. + * + * Destroying a bound xdg_wm_base object while there are surfaces + * still alive created by this xdg_wm_base object instance is illegal + * and will result in a defunct_surfaces error. + */ +static inline void +xdg_wm_base_destroy(struct xdg_wm_base *xdg_wm_base) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_wm_base + * + * Create a positioner object. A positioner object is used to position + * surfaces relative to some parent surface. See the interface description + * and xdg_surface.get_popup for details. + */ +static inline struct xdg_positioner * +xdg_wm_base_create_positioner(struct xdg_wm_base *xdg_wm_base) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_CREATE_POSITIONER, &xdg_positioner_interface, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, NULL); + + return (struct xdg_positioner *) id; +} + +/** + * @ingroup iface_xdg_wm_base + * + * This creates an xdg_surface for the given surface. While xdg_surface + * itself is not a role, the corresponding surface may only be assigned + * a role extending xdg_surface, such as xdg_toplevel or xdg_popup. It is + * illegal to create an xdg_surface for a wl_surface which already has an + * assigned role and this will result in a role error. + * + * This creates an xdg_surface for the given surface. An xdg_surface is + * used as basis to define a role to a given surface, such as xdg_toplevel + * or xdg_popup. It also manages functionality shared between xdg_surface + * based surface roles. + * + * See the documentation of xdg_surface for more details about what an + * xdg_surface is and how it is used. + */ +static inline struct xdg_surface * +xdg_wm_base_get_xdg_surface(struct xdg_wm_base *xdg_wm_base, struct wl_surface *surface) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_GET_XDG_SURFACE, &xdg_surface_interface, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, NULL, surface); + + return (struct xdg_surface *) id; +} + +/** + * @ingroup iface_xdg_wm_base + * + * A client must respond to a ping event with a pong request or + * the client may be deemed unresponsive. See xdg_wm_base.ping + * and xdg_wm_base.error.unresponsive. + */ +static inline void +xdg_wm_base_pong(struct xdg_wm_base *xdg_wm_base, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_PONG, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, serial); +} + +#ifndef XDG_POSITIONER_ERROR_ENUM +#define XDG_POSITIONER_ERROR_ENUM +enum xdg_positioner_error { + /** + * invalid input provided + */ + XDG_POSITIONER_ERROR_INVALID_INPUT = 0, +}; +#endif /* XDG_POSITIONER_ERROR_ENUM */ + +#ifndef XDG_POSITIONER_ANCHOR_ENUM +#define XDG_POSITIONER_ANCHOR_ENUM +enum xdg_positioner_anchor { + XDG_POSITIONER_ANCHOR_NONE = 0, + XDG_POSITIONER_ANCHOR_TOP = 1, + XDG_POSITIONER_ANCHOR_BOTTOM = 2, + XDG_POSITIONER_ANCHOR_LEFT = 3, + XDG_POSITIONER_ANCHOR_RIGHT = 4, + XDG_POSITIONER_ANCHOR_TOP_LEFT = 5, + XDG_POSITIONER_ANCHOR_BOTTOM_LEFT = 6, + XDG_POSITIONER_ANCHOR_TOP_RIGHT = 7, + XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT = 8, +}; +#endif /* XDG_POSITIONER_ANCHOR_ENUM */ + +#ifndef XDG_POSITIONER_GRAVITY_ENUM +#define XDG_POSITIONER_GRAVITY_ENUM +enum xdg_positioner_gravity { + XDG_POSITIONER_GRAVITY_NONE = 0, + XDG_POSITIONER_GRAVITY_TOP = 1, + XDG_POSITIONER_GRAVITY_BOTTOM = 2, + XDG_POSITIONER_GRAVITY_LEFT = 3, + XDG_POSITIONER_GRAVITY_RIGHT = 4, + XDG_POSITIONER_GRAVITY_TOP_LEFT = 5, + XDG_POSITIONER_GRAVITY_BOTTOM_LEFT = 6, + XDG_POSITIONER_GRAVITY_TOP_RIGHT = 7, + XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT = 8, +}; +#endif /* XDG_POSITIONER_GRAVITY_ENUM */ + +#ifndef XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM +#define XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM +/** + * @ingroup iface_xdg_positioner + * constraint adjustments + * + * The constraint adjustment value define ways the compositor will adjust + * the position of the surface, if the unadjusted position would result + * in the surface being partly constrained. + * + * Whether a surface is considered 'constrained' is left to the compositor + * to determine. For example, the surface may be partly outside the + * compositor's defined 'work area', thus necessitating the child surface's + * position be adjusted until it is entirely inside the work area. + * + * The adjustments can be combined, according to a defined precedence: 1) + * Flip, 2) Slide, 3) Resize. + */ +enum xdg_positioner_constraint_adjustment { + /** + * don't move the child surface when constrained + * + * Don't alter the surface position even if it is constrained on + * some axis, for example partially outside the edge of an output. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE = 0, + /** + * move along the x axis until unconstrained + * + * Slide the surface along the x axis until it is no longer + * constrained. + * + * First try to slide towards the direction of the gravity on the x + * axis until either the edge in the opposite direction of the + * gravity is unconstrained or the edge in the direction of the + * gravity is constrained. + * + * Then try to slide towards the opposite direction of the gravity + * on the x axis until either the edge in the direction of the + * gravity is unconstrained or the edge in the opposite direction + * of the gravity is constrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X = 1, + /** + * move along the y axis until unconstrained + * + * Slide the surface along the y axis until it is no longer + * constrained. + * + * First try to slide towards the direction of the gravity on the y + * axis until either the edge in the opposite direction of the + * gravity is unconstrained or the edge in the direction of the + * gravity is constrained. + * + * Then try to slide towards the opposite direction of the gravity + * on the y axis until either the edge in the direction of the + * gravity is unconstrained or the edge in the opposite direction + * of the gravity is constrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y = 2, + /** + * invert the anchor and gravity on the x axis + * + * Invert the anchor and gravity on the x axis if the surface is + * constrained on the x axis. For example, if the left edge of the + * surface is constrained, the gravity is 'left' and the anchor is + * 'left', change the gravity to 'right' and the anchor to 'right'. + * + * If the adjusted position also ends up being constrained, the + * resulting position of the flip_x adjustment will be the one + * before the adjustment. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X = 4, + /** + * invert the anchor and gravity on the y axis + * + * Invert the anchor and gravity on the y axis if the surface is + * constrained on the y axis. For example, if the bottom edge of + * the surface is constrained, the gravity is 'bottom' and the + * anchor is 'bottom', change the gravity to 'top' and the anchor + * to 'top'. + * + * The adjusted position is calculated given the original anchor + * rectangle and offset, but with the new flipped anchor and + * gravity values. + * + * If the adjusted position also ends up being constrained, the + * resulting position of the flip_y adjustment will be the one + * before the adjustment. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y = 8, + /** + * horizontally resize the surface + * + * Resize the surface horizontally so that it is completely + * unconstrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X = 16, + /** + * vertically resize the surface + * + * Resize the surface vertically so that it is completely + * unconstrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y = 32, +}; +#endif /* XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM */ + +#define XDG_POSITIONER_DESTROY 0 +#define XDG_POSITIONER_SET_SIZE 1 +#define XDG_POSITIONER_SET_ANCHOR_RECT 2 +#define XDG_POSITIONER_SET_ANCHOR 3 +#define XDG_POSITIONER_SET_GRAVITY 4 +#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT 5 +#define XDG_POSITIONER_SET_OFFSET 6 +#define XDG_POSITIONER_SET_REACTIVE 7 +#define XDG_POSITIONER_SET_PARENT_SIZE 8 +#define XDG_POSITIONER_SET_PARENT_CONFIGURE 9 + + +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_ANCHOR_RECT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_ANCHOR_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_GRAVITY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_OFFSET_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION 3 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_PARENT_SIZE_SINCE_VERSION 3 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_PARENT_CONFIGURE_SINCE_VERSION 3 + +/** @ingroup iface_xdg_positioner */ +static inline void +xdg_positioner_set_user_data(struct xdg_positioner *xdg_positioner, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_positioner, user_data); +} + +/** @ingroup iface_xdg_positioner */ +static inline void * +xdg_positioner_get_user_data(struct xdg_positioner *xdg_positioner) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_positioner); +} + +static inline uint32_t +xdg_positioner_get_version(struct xdg_positioner *xdg_positioner) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_positioner); +} + +/** + * @ingroup iface_xdg_positioner + * + * Notify the compositor that the xdg_positioner will no longer be used. + */ +static inline void +xdg_positioner_destroy(struct xdg_positioner *xdg_positioner) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_positioner + * + * Set the size of the surface that is to be positioned with the positioner + * object. The size is in surface-local coordinates and corresponds to the + * window geometry. See xdg_surface.set_window_geometry. + * + * If a zero or negative size is set the invalid_input error is raised. + */ +static inline void +xdg_positioner_set_size(struct xdg_positioner *xdg_positioner, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, width, height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify the anchor rectangle within the parent surface that the child + * surface will be placed relative to. The rectangle is relative to the + * window geometry as defined by xdg_surface.set_window_geometry of the + * parent surface. + * + * When the xdg_positioner object is used to position a child surface, the + * anchor rectangle may not extend outside the window geometry of the + * positioned child's parent surface. + * + * If a negative size is set the invalid_input error is raised. + */ +static inline void +xdg_positioner_set_anchor_rect(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_ANCHOR_RECT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y, width, height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Defines the anchor point for the anchor rectangle. The specified anchor + * is used derive an anchor point that the child surface will be + * positioned relative to. If a corner anchor is set (e.g. 'top_left' or + * 'bottom_right'), the anchor point will be at the specified corner; + * otherwise, the derived anchor point will be centered on the specified + * edge, or in the center of the anchor rectangle if no edge is specified. + */ +static inline void +xdg_positioner_set_anchor(struct xdg_positioner *xdg_positioner, uint32_t anchor) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_ANCHOR, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, anchor); +} + +/** + * @ingroup iface_xdg_positioner + * + * Defines in what direction a surface should be positioned, relative to + * the anchor point of the parent surface. If a corner gravity is + * specified (e.g. 'bottom_right' or 'top_left'), then the child surface + * will be placed towards the specified gravity; otherwise, the child + * surface will be centered over the anchor point on any axis that had no + * gravity specified. If the gravity is not in the ‘gravity’ enum, an + * invalid_input error is raised. + */ +static inline void +xdg_positioner_set_gravity(struct xdg_positioner *xdg_positioner, uint32_t gravity) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_GRAVITY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, gravity); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify how the window should be positioned if the originally intended + * position caused the surface to be constrained, meaning at least + * partially outside positioning boundaries set by the compositor. The + * adjustment is set by constructing a bitmask describing the adjustment to + * be made when the surface is constrained on that axis. + * + * If no bit for one axis is set, the compositor will assume that the child + * surface should not change its position on that axis when constrained. + * + * If more than one bit for one axis is set, the order of how adjustments + * are applied is specified in the corresponding adjustment descriptions. + * + * The default adjustment is none. + */ +static inline void +xdg_positioner_set_constraint_adjustment(struct xdg_positioner *xdg_positioner, uint32_t constraint_adjustment) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, constraint_adjustment); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify the surface position offset relative to the position of the + * anchor on the anchor rectangle and the anchor on the surface. For + * example if the anchor of the anchor rectangle is at (x, y), the surface + * has the gravity bottom|right, and the offset is (ox, oy), the calculated + * surface position will be (x + ox, y + oy). The offset position of the + * surface is the one used for constraint testing. See + * set_constraint_adjustment. + * + * An example use case is placing a popup menu on top of a user interface + * element, while aligning the user interface element of the parent surface + * with some user interface element placed somewhere in the popup surface. + */ +static inline void +xdg_positioner_set_offset(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_OFFSET, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y); +} + +/** + * @ingroup iface_xdg_positioner + * + * When set reactive, the surface is reconstrained if the conditions used + * for constraining changed, e.g. the parent window moved. + * + * If the conditions changed and the popup was reconstrained, an + * xdg_popup.configure event is sent with updated geometry, followed by an + * xdg_surface.configure event. + */ +static inline void +xdg_positioner_set_reactive(struct xdg_positioner *xdg_positioner) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_REACTIVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0); +} + +/** + * @ingroup iface_xdg_positioner + * + * Set the parent window geometry the compositor should use when + * positioning the popup. The compositor may use this information to + * determine the future state the popup should be constrained using. If + * this doesn't match the dimension of the parent the popup is eventually + * positioned against, the behavior is undefined. + * + * The arguments are given in the surface-local coordinate space. + */ +static inline void +xdg_positioner_set_parent_size(struct xdg_positioner *xdg_positioner, int32_t parent_width, int32_t parent_height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_PARENT_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, parent_width, parent_height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Set the serial of an xdg_surface.configure event this positioner will be + * used in response to. The compositor may use this information together + * with set_parent_size to determine what future state the popup should be + * constrained using. + */ +static inline void +xdg_positioner_set_parent_configure(struct xdg_positioner *xdg_positioner, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_PARENT_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, serial); +} + +#ifndef XDG_SURFACE_ERROR_ENUM +#define XDG_SURFACE_ERROR_ENUM +enum xdg_surface_error { + /** + * Surface was not fully constructed + */ + XDG_SURFACE_ERROR_NOT_CONSTRUCTED = 1, + /** + * Surface was already constructed + */ + XDG_SURFACE_ERROR_ALREADY_CONSTRUCTED = 2, + /** + * Attaching a buffer to an unconfigured surface + */ + XDG_SURFACE_ERROR_UNCONFIGURED_BUFFER = 3, + /** + * Invalid serial number when acking a configure event + */ + XDG_SURFACE_ERROR_INVALID_SERIAL = 4, + /** + * Width or height was zero or negative + */ + XDG_SURFACE_ERROR_INVALID_SIZE = 5, + /** + * Surface was destroyed before its role object + */ + XDG_SURFACE_ERROR_DEFUNCT_ROLE_OBJECT = 6, +}; +#endif /* XDG_SURFACE_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_surface + * @struct xdg_surface_listener + */ +struct xdg_surface_listener { + /** + * suggest a surface change + * + * The configure event marks the end of a configure sequence. A + * configure sequence is a set of one or more events configuring + * the state of the xdg_surface, including the final + * xdg_surface.configure event. + * + * Where applicable, xdg_surface surface roles will during a + * configure sequence extend this event as a latched state sent as + * events before the xdg_surface.configure event. Such events + * should be considered to make up a set of atomically applied + * configuration states, where the xdg_surface.configure commits + * the accumulated state. + * + * Clients should arrange their surface for the new states, and + * then send an ack_configure request with the serial sent in this + * configure event at some point before committing the new surface. + * + * If the client receives multiple configure events before it can + * respond to one, it is free to discard all but the last event it + * received. + * @param serial serial of the configure event + */ + void (*configure)(void *data, + struct xdg_surface *xdg_surface, + uint32_t serial); +}; + +/** + * @ingroup iface_xdg_surface + */ +static inline int +xdg_surface_add_listener(struct xdg_surface *xdg_surface, + const struct xdg_surface_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_surface, + (void (**)(void)) listener, data); +} + +#define XDG_SURFACE_DESTROY 0 +#define XDG_SURFACE_GET_TOPLEVEL 1 +#define XDG_SURFACE_GET_POPUP 2 +#define XDG_SURFACE_SET_WINDOW_GEOMETRY 3 +#define XDG_SURFACE_ACK_CONFIGURE 4 + +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_CONFIGURE_SINCE_VERSION 1 + +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_GET_TOPLEVEL_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_GET_POPUP_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_SET_WINDOW_GEOMETRY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_ACK_CONFIGURE_SINCE_VERSION 1 + +/** @ingroup iface_xdg_surface */ +static inline void +xdg_surface_set_user_data(struct xdg_surface *xdg_surface, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_surface, user_data); +} + +/** @ingroup iface_xdg_surface */ +static inline void * +xdg_surface_get_user_data(struct xdg_surface *xdg_surface) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_surface); +} + +static inline uint32_t +xdg_surface_get_version(struct xdg_surface *xdg_surface) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_surface); +} + +/** + * @ingroup iface_xdg_surface + * + * Destroy the xdg_surface object. An xdg_surface must only be destroyed + * after its role object has been destroyed, otherwise + * a defunct_role_object error is raised. + */ +static inline void +xdg_surface_destroy(struct xdg_surface *xdg_surface) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_surface + * + * This creates an xdg_toplevel object for the given xdg_surface and gives + * the associated wl_surface the xdg_toplevel role. + * + * See the documentation of xdg_toplevel for more details about what an + * xdg_toplevel is and how it is used. + */ +static inline struct xdg_toplevel * +xdg_surface_get_toplevel(struct xdg_surface *xdg_surface) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_GET_TOPLEVEL, &xdg_toplevel_interface, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, NULL); + + return (struct xdg_toplevel *) id; +} + +/** + * @ingroup iface_xdg_surface + * + * This creates an xdg_popup object for the given xdg_surface and gives + * the associated wl_surface the xdg_popup role. + * + * If null is passed as a parent, a parent surface must be specified using + * some other protocol, before committing the initial state. + * + * See the documentation of xdg_popup for more details about what an + * xdg_popup is and how it is used. + */ +static inline struct xdg_popup * +xdg_surface_get_popup(struct xdg_surface *xdg_surface, struct xdg_surface *parent, struct xdg_positioner *positioner) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_GET_POPUP, &xdg_popup_interface, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, NULL, parent, positioner); + + return (struct xdg_popup *) id; +} + +/** + * @ingroup iface_xdg_surface + * + * The window geometry of a surface is its "visible bounds" from the + * user's perspective. Client-side decorations often have invisible + * portions like drop-shadows which should be ignored for the + * purposes of aligning, placing and constraining windows. Note that + * in some situations, compositors may clip rendering to the window + * geometry, so the client should avoid putting functional elements + * outside of it. + * + * The window geometry is double-buffered state, see wl_surface.commit. + * + * When maintaining a position, the compositor should treat the (x, y) + * coordinate of the window geometry as the top left corner of the window. + * A client changing the (x, y) window geometry coordinate should in + * general not alter the position of the window. + * + * Once the window geometry of the surface is set, it is not possible to + * unset it, and it will remain the same until set_window_geometry is + * called again, even if a new subsurface or buffer is attached. + * + * If never set, the value is the full bounds of the surface, + * including any subsurfaces. This updates dynamically on every + * commit. This unset is meant for extremely simple clients. + * + * The arguments are given in the surface-local coordinate space of + * the wl_surface associated with this xdg_surface, and may extend outside + * of the wl_surface itself to mark parts of the subsurface tree as part of + * the window geometry. + * + * When applied, the effective window geometry will be the set window + * geometry clamped to the bounding rectangle of the combined + * geometry of the surface of the xdg_surface and the associated + * subsurfaces. + * + * The effective geometry will not be recalculated unless a new call to + * set_window_geometry is done and the new pending surface state is + * subsequently applied. + * + * The width and height of the effective window geometry must be + * greater than zero. Setting an invalid size will raise an + * invalid_size error. + */ +static inline void +xdg_surface_set_window_geometry(struct xdg_surface *xdg_surface, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_SET_WINDOW_GEOMETRY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, x, y, width, height); +} + +/** + * @ingroup iface_xdg_surface + * + * When a configure event is received, if a client commits the + * surface in response to the configure event, then the client + * must make an ack_configure request sometime before the commit + * request, passing along the serial of the configure event. + * + * For instance, for toplevel surfaces the compositor might use this + * information to move a surface to the top left only when the client has + * drawn itself for the maximized or fullscreen state. + * + * If the client receives multiple configure events before it + * can respond to one, it only has to ack the last configure event. + * Acking a configure event that was never sent raises an invalid_serial + * error. + * + * A client is not required to commit immediately after sending + * an ack_configure request - it may even ack_configure several times + * before its next surface commit. + * + * A client may send multiple ack_configure requests before committing, but + * only the last request sent before a commit indicates which configure + * event the client really is responding to. + * + * Sending an ack_configure request consumes the serial number sent with + * the request, as well as serial numbers sent by all configure events + * sent on this xdg_surface prior to the configure event referenced by + * the committed serial. + * + * It is an error to issue multiple ack_configure requests referencing a + * serial from the same configure event, or to issue an ack_configure + * request referencing a serial from a configure event issued before the + * event identified by the last ack_configure request for the same + * xdg_surface. Doing so will raise an invalid_serial error. + */ +static inline void +xdg_surface_ack_configure(struct xdg_surface *xdg_surface, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_ACK_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, serial); +} + +#ifndef XDG_TOPLEVEL_ERROR_ENUM +#define XDG_TOPLEVEL_ERROR_ENUM +enum xdg_toplevel_error { + /** + * provided value is not a valid variant of the resize_edge enum + */ + XDG_TOPLEVEL_ERROR_INVALID_RESIZE_EDGE = 0, + /** + * invalid parent toplevel + */ + XDG_TOPLEVEL_ERROR_INVALID_PARENT = 1, + /** + * client provided an invalid min or max size + */ + XDG_TOPLEVEL_ERROR_INVALID_SIZE = 2, +}; +#endif /* XDG_TOPLEVEL_ERROR_ENUM */ + +#ifndef XDG_TOPLEVEL_RESIZE_EDGE_ENUM +#define XDG_TOPLEVEL_RESIZE_EDGE_ENUM +/** + * @ingroup iface_xdg_toplevel + * edge values for resizing + * + * These values are used to indicate which edge of a surface + * is being dragged in a resize operation. + */ +enum xdg_toplevel_resize_edge { + XDG_TOPLEVEL_RESIZE_EDGE_NONE = 0, + XDG_TOPLEVEL_RESIZE_EDGE_TOP = 1, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM = 2, + XDG_TOPLEVEL_RESIZE_EDGE_LEFT = 4, + XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT = 5, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT = 6, + XDG_TOPLEVEL_RESIZE_EDGE_RIGHT = 8, + XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT = 9, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT = 10, +}; +#endif /* XDG_TOPLEVEL_RESIZE_EDGE_ENUM */ + +#ifndef XDG_TOPLEVEL_STATE_ENUM +#define XDG_TOPLEVEL_STATE_ENUM +/** + * @ingroup iface_xdg_toplevel + * types of state on the surface + * + * The different state values used on the surface. This is designed for + * state values like maximized, fullscreen. It is paired with the + * configure event to ensure that both the client and the compositor + * setting the state can be synchronized. + * + * States set in this way are double-buffered, see wl_surface.commit. + */ +enum xdg_toplevel_state { + /** + * the surface is maximized + * the surface is maximized + * + * The surface is maximized. The window geometry specified in the + * configure event must be obeyed by the client, or the + * xdg_wm_base.invalid_surface_state error is raised. + * + * The client should draw without shadow or other decoration + * outside of the window geometry. + */ + XDG_TOPLEVEL_STATE_MAXIMIZED = 1, + /** + * the surface is fullscreen + * the surface is fullscreen + * + * The surface is fullscreen. The window geometry specified in + * the configure event is a maximum; the client cannot resize + * beyond it. For a surface to cover the whole fullscreened area, + * the geometry dimensions must be obeyed by the client. For more + * details, see xdg_toplevel.set_fullscreen. + */ + XDG_TOPLEVEL_STATE_FULLSCREEN = 2, + /** + * the surface is being resized + * the surface is being resized + * + * The surface is being resized. The window geometry specified in + * the configure event is a maximum; the client cannot resize + * beyond it. Clients that have aspect ratio or cell sizing + * configuration can use a smaller size, however. + */ + XDG_TOPLEVEL_STATE_RESIZING = 3, + /** + * the surface is now activated + * the surface is now activated + * + * Client window decorations should be painted as if the window + * is active. Do not assume this means that the window actually has + * keyboard or pointer focus. + */ + XDG_TOPLEVEL_STATE_ACTIVATED = 4, + /** + * the surface’s left edge is tiled + * + * The window is currently in a tiled layout and the left edge is + * considered to be adjacent to another part of the tiling grid. + * + * The client should draw without shadow or other decoration + * outside of the window geometry on the left edge. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_LEFT = 5, + /** + * the surface’s right edge is tiled + * + * The window is currently in a tiled layout and the right edge + * is considered to be adjacent to another part of the tiling grid. + * + * The client should draw without shadow or other decoration + * outside of the window geometry on the right edge. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_RIGHT = 6, + /** + * the surface’s top edge is tiled + * + * The window is currently in a tiled layout and the top edge is + * considered to be adjacent to another part of the tiling grid. + * + * The client should draw without shadow or other decoration + * outside of the window geometry on the top edge. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_TOP = 7, + /** + * the surface’s bottom edge is tiled + * + * The window is currently in a tiled layout and the bottom edge + * is considered to be adjacent to another part of the tiling grid. + * + * The client should draw without shadow or other decoration + * outside of the window geometry on the bottom edge. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_BOTTOM = 8, + /** + * surface repaint is suspended + * + * The surface is currently not ordinarily being repainted; for + * example because its content is occluded by another window, or + * its outputs are switched off due to screen locking. + * @since 6 + */ + XDG_TOPLEVEL_STATE_SUSPENDED = 9, + /** + * the surface’s left edge is constrained + * + * The left edge of the window is currently constrained, meaning + * it shouldn't attempt to resize from that edge. It can for + * example mean it's tiled next to a monitor edge on the + * constrained side of the window. + * @since 7 + */ + XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT = 10, + /** + * the surface’s right edge is constrained + * + * The right edge of the window is currently constrained, meaning + * it shouldn't attempt to resize from that edge. It can for + * example mean it's tiled next to a monitor edge on the + * constrained side of the window. + * @since 7 + */ + XDG_TOPLEVEL_STATE_CONSTRAINED_RIGHT = 11, + /** + * the surface’s top edge is constrained + * + * The top edge of the window is currently constrained, meaning + * it shouldn't attempt to resize from that edge. It can for + * example mean it's tiled next to a monitor edge on the + * constrained side of the window. + * @since 7 + */ + XDG_TOPLEVEL_STATE_CONSTRAINED_TOP = 12, + /** + * the surface’s bottom edge is constrained + * + * The bottom edge of the window is currently constrained, + * meaning it shouldn't attempt to resize from that edge. It can + * for example mean it's tiled next to a monitor edge on the + * constrained side of the window. + * @since 7 + */ + XDG_TOPLEVEL_STATE_CONSTRAINED_BOTTOM = 13, +}; +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_RIGHT_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_TOP_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_BOTTOM_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_SUSPENDED_SINCE_VERSION 6 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT_SINCE_VERSION 7 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_CONSTRAINED_RIGHT_SINCE_VERSION 7 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_CONSTRAINED_TOP_SINCE_VERSION 7 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_CONSTRAINED_BOTTOM_SINCE_VERSION 7 +#endif /* XDG_TOPLEVEL_STATE_ENUM */ + +#ifndef XDG_TOPLEVEL_WM_CAPABILITIES_ENUM +#define XDG_TOPLEVEL_WM_CAPABILITIES_ENUM +enum xdg_toplevel_wm_capabilities { + /** + * show_window_menu is available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU = 1, + /** + * set_maximized and unset_maximized are available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE = 2, + /** + * set_fullscreen and unset_fullscreen are available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN = 3, + /** + * set_minimized is available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE = 4, +}; +#endif /* XDG_TOPLEVEL_WM_CAPABILITIES_ENUM */ + +/** + * @ingroup iface_xdg_toplevel + * @struct xdg_toplevel_listener + */ +struct xdg_toplevel_listener { + /** + * suggest a surface change + * + * This configure event asks the client to resize its toplevel + * surface or to change its state. The configured state should not + * be applied immediately. See xdg_surface.configure for details. + * + * The width and height arguments specify a hint to the window + * about how its surface should be resized in window geometry + * coordinates. See set_window_geometry. + * + * If the width or height arguments are zero, it means the client + * should decide its own window dimension. This may happen when the + * compositor needs to configure the state of the surface but + * doesn't have any information about any previous or expected + * dimension. + * + * The states listed in the event specify how the width/height + * arguments should be interpreted, and possibly how it should be + * drawn. + * + * Clients must send an ack_configure in response to this event. + * See xdg_surface.configure and xdg_surface.ack_configure for + * details. + */ + void (*configure)(void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, + int32_t height, + struct wl_array *states); + /** + * surface wants to be closed + * + * The close event is sent by the compositor when the user wants + * the surface to be closed. This should be equivalent to the user + * clicking the close button in client-side decorations, if your + * application has any. + * + * This is only a request that the user intends to close the + * window. The client may choose to ignore this request, or show a + * dialog to ask the user to save their data, etc. + */ + void (*close)(void *data, + struct xdg_toplevel *xdg_toplevel); + /** + * recommended window geometry bounds + * + * The configure_bounds event may be sent prior to a + * xdg_toplevel.configure event to communicate the bounds a window + * geometry size is recommended to constrain to. + * + * The passed width and height are in surface coordinate space. If + * width and height are 0, it means bounds is unknown and + * equivalent to as if no configure_bounds event was ever sent for + * this surface. + * + * The bounds can for example correspond to the size of a monitor + * excluding any panels or other shell components, so that a + * surface isn't created in a way that it cannot fit. + * + * The bounds may change at any point, and in such a case, a new + * xdg_toplevel.configure_bounds will be sent, followed by + * xdg_toplevel.configure and xdg_surface.configure. + * @since 4 + */ + void (*configure_bounds)(void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, + int32_t height); + /** + * compositor capabilities + * + * This event advertises the capabilities supported by the + * compositor. If a capability isn't supported, clients should hide + * or disable the UI elements that expose this functionality. For + * instance, if the compositor doesn't advertise support for + * minimized toplevels, a button triggering the set_minimized + * request should not be displayed. + * + * The compositor will ignore requests it doesn't support. For + * instance, a compositor which doesn't advertise support for + * minimized will ignore set_minimized requests. + * + * Compositors must send this event once before the first + * xdg_surface.configure event. When the capabilities change, + * compositors must send this event again and then send an + * xdg_surface.configure event. + * + * The configured state should not be applied immediately. See + * xdg_surface.configure for details. + * + * The capabilities are sent as an array of 32-bit unsigned + * integers in native endianness. + * @param capabilities array of 32-bit capabilities + * @since 5 + */ + void (*wm_capabilities)(void *data, + struct xdg_toplevel *xdg_toplevel, + struct wl_array *capabilities); +}; + +/** + * @ingroup iface_xdg_toplevel + */ +static inline int +xdg_toplevel_add_listener(struct xdg_toplevel *xdg_toplevel, + const struct xdg_toplevel_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_toplevel, + (void (**)(void)) listener, data); +} + +#define XDG_TOPLEVEL_DESTROY 0 +#define XDG_TOPLEVEL_SET_PARENT 1 +#define XDG_TOPLEVEL_SET_TITLE 2 +#define XDG_TOPLEVEL_SET_APP_ID 3 +#define XDG_TOPLEVEL_SHOW_WINDOW_MENU 4 +#define XDG_TOPLEVEL_MOVE 5 +#define XDG_TOPLEVEL_RESIZE 6 +#define XDG_TOPLEVEL_SET_MAX_SIZE 7 +#define XDG_TOPLEVEL_SET_MIN_SIZE 8 +#define XDG_TOPLEVEL_SET_MAXIMIZED 9 +#define XDG_TOPLEVEL_UNSET_MAXIMIZED 10 +#define XDG_TOPLEVEL_SET_FULLSCREEN 11 +#define XDG_TOPLEVEL_UNSET_FULLSCREEN 12 +#define XDG_TOPLEVEL_SET_MINIMIZED 13 + +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CONFIGURE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CLOSE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION 4 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION 5 + +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_PARENT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_TITLE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_APP_ID_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SHOW_WINDOW_MENU_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_MOVE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_RESIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MAX_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MIN_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MAXIMIZED_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_UNSET_MAXIMIZED_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_FULLSCREEN_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_UNSET_FULLSCREEN_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MINIMIZED_SINCE_VERSION 1 + +/** @ingroup iface_xdg_toplevel */ +static inline void +xdg_toplevel_set_user_data(struct xdg_toplevel *xdg_toplevel, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_toplevel, user_data); +} + +/** @ingroup iface_xdg_toplevel */ +static inline void * +xdg_toplevel_get_user_data(struct xdg_toplevel *xdg_toplevel) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_toplevel); +} + +static inline uint32_t +xdg_toplevel_get_version(struct xdg_toplevel *xdg_toplevel) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_toplevel); +} + +/** + * @ingroup iface_xdg_toplevel + * + * This request destroys the role surface and unmaps the surface; + * see "Unmapping" behavior in interface section for details. + */ +static inline void +xdg_toplevel_destroy(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set the "parent" of this surface. This surface should be stacked + * above the parent surface and all other ancestor surfaces. + * + * Parent surfaces should be set on dialogs, toolboxes, or other + * "auxiliary" surfaces, so that the parent is raised when the dialog + * is raised. + * + * Setting a null parent for a child surface unsets its parent. Setting + * a null parent for a surface which currently has no parent is a no-op. + * + * Only mapped surfaces can have child surfaces. Setting a parent which + * is not mapped is equivalent to setting a null parent. If a surface + * becomes unmapped, its children's parent is set to the parent of + * the now-unmapped surface. If the now-unmapped surface has no parent, + * its children's parent is unset. If the now-unmapped surface becomes + * mapped again, its parent-child relationship is not restored. + * + * The parent toplevel must not be one of the child toplevel's + * descendants, and the parent must be different from the child toplevel, + * otherwise the invalid_parent protocol error is raised. + */ +static inline void +xdg_toplevel_set_parent(struct xdg_toplevel *xdg_toplevel, struct xdg_toplevel *parent) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_PARENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, parent); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a short title for the surface. + * + * This string may be used to identify the surface in a task bar, + * window list, or other user interface elements provided by the + * compositor. + * + * The string must be encoded in UTF-8. + */ +static inline void +xdg_toplevel_set_title(struct xdg_toplevel *xdg_toplevel, const char *title) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_TITLE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, title); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set an application identifier for the surface. + * + * The app ID identifies the general class of applications to which + * the surface belongs. The compositor can use this to group multiple + * surfaces together, or to determine how to launch a new application. + * + * For D-Bus activatable applications, the app ID is used as the D-Bus + * service name. + * + * The compositor shell will try to group application surfaces together + * by their app ID. As a best practice, it is suggested to select app + * ID's that match the basename of the application's .desktop file. + * For example, "org.freedesktop.FooViewer" where the .desktop file is + * "org.freedesktop.FooViewer.desktop". + * + * Like other properties, a set_app_id request can be sent after the + * xdg_toplevel has been mapped to update the property. + * + * See the desktop-entry specification [0] for more details on + * application identifiers and how they relate to well-known D-Bus + * names and .desktop files. + * + * [0] https://standards.freedesktop.org/desktop-entry-spec/ + */ +static inline void +xdg_toplevel_set_app_id(struct xdg_toplevel *xdg_toplevel, const char *app_id) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_APP_ID, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, app_id); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Clients implementing client-side decorations might want to show + * a context menu when right-clicking on the decorations, giving the + * user a menu that they can use to maximize or minimize the window. + * + * This request asks the compositor to pop up such a window menu at + * the given position, relative to the local surface coordinates of + * the parent surface. There are no guarantees as to what menu items + * the window menu contains, or even if a window menu will be drawn + * at all. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. + */ +static inline void +xdg_toplevel_show_window_menu(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, int32_t x, int32_t y) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SHOW_WINDOW_MENU, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, x, y); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Start an interactive, user-driven move of the surface. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. The passed + * serial is used to determine the type of interactive move (touch, + * pointer, etc). + * + * The server may ignore move requests depending on the state of + * the surface (e.g. fullscreen or maximized), or if the passed serial + * is no longer valid. + * + * If triggered, the surface will lose the focus of the device + * (wl_pointer, wl_touch, etc) used for the move. It is up to the + * compositor to visually indicate that the move is taking place, such as + * updating a pointer cursor, during the move. There is no guarantee + * that the device focus will return when the move is completed. + */ +static inline void +xdg_toplevel_move(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_MOVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Start a user-driven, interactive resize of the surface. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. The passed + * serial is used to determine the type of interactive resize (touch, + * pointer, etc). + * + * The server may ignore resize requests depending on the state of + * the surface (e.g. fullscreen or maximized). + * + * If triggered, the client will receive configure events with the + * "resize" state enum value and the expected sizes. See the "resize" + * enum value for more details about what is required. The client + * must also acknowledge configure events using "ack_configure". After + * the resize is completed, the client will receive another "configure" + * event without the resize state. + * + * If triggered, the surface also will lose the focus of the device + * (wl_pointer, wl_touch, etc) used for the resize. It is up to the + * compositor to visually indicate that the resize is taking place, + * such as updating a pointer cursor, during the resize. There is no + * guarantee that the device focus will return when the resize is + * completed. + * + * The edges parameter specifies how the surface should be resized, and + * is one of the values of the resize_edge enum. Values not matching + * a variant of the enum will cause the invalid_resize_edge protocol error. + * The compositor may use this information to update the surface position + * for example when dragging the top left corner. The compositor may also + * use this information to adapt its behavior, e.g. choose an appropriate + * cursor image. + */ +static inline void +xdg_toplevel_resize(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, uint32_t edges) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_RESIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, edges); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a maximum size for the window. + * + * The client can specify a maximum size so that the compositor does + * not try to configure the window beyond this size. + * + * The width and height arguments are in window geometry coordinates. + * See xdg_surface.set_window_geometry. + * + * Values set in this way are double-buffered, see wl_surface.commit. + * + * The compositor can use this information to allow or disallow + * different states like maximize or fullscreen and draw accurate + * animations. + * + * Similarly, a tiling window manager may use this information to + * place and resize client windows in a more effective way. + * + * The client should not rely on the compositor to obey the maximum + * size. The compositor may decide to ignore the values set by the + * client and request a larger size. + * + * If never set, or a value of zero in the request, means that the + * client has no expected maximum size in the given dimension. + * As a result, a client wishing to reset the maximum size + * to an unspecified state can use zero for width and height in the + * request. + * + * Requesting a maximum size to be smaller than the minimum size of + * a surface is illegal and will result in an invalid_size error. + * + * The width and height must be greater than or equal to zero. Using + * strictly negative values for width or height will result in a + * invalid_size error. + */ +static inline void +xdg_toplevel_set_max_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MAX_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a minimum size for the window. + * + * The client can specify a minimum size so that the compositor does + * not try to configure the window below this size. + * + * The width and height arguments are in window geometry coordinates. + * See xdg_surface.set_window_geometry. + * + * Values set in this way are double-buffered, see wl_surface.commit. + * + * The compositor can use this information to allow or disallow + * different states like maximize or fullscreen and draw accurate + * animations. + * + * Similarly, a tiling window manager may use this information to + * place and resize client windows in a more effective way. + * + * The client should not rely on the compositor to obey the minimum + * size. The compositor may decide to ignore the values set by the + * client and request a smaller size. + * + * If never set, or a value of zero in the request, means that the + * client has no expected minimum size in the given dimension. + * As a result, a client wishing to reset the minimum size + * to an unspecified state can use zero for width and height in the + * request. + * + * Requesting a minimum size to be larger than the maximum size of + * a surface is illegal and will result in an invalid_size error. + * + * The width and height must be greater than or equal to zero. Using + * strictly negative values for width and height will result in a + * invalid_size error. + */ +static inline void +xdg_toplevel_set_min_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MIN_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Maximize the surface. + * + * After requesting that the surface should be maximized, the compositor + * will respond by emitting a configure event. Whether this configure + * actually sets the window maximized is subject to compositor policies. + * The client must then update its content, drawing in the configured + * state. The client must also acknowledge the configure when committing + * the new content (see ack_configure). + * + * It is up to the compositor to decide how and where to maximize the + * surface, for example which output and what region of the screen should + * be used. + * + * If the surface was already maximized, the compositor will still emit + * a configure event with the "maximized" state. + * + * If the surface is in a fullscreen state, this request has no direct + * effect. It may alter the state the surface is returned to when + * unmaximized unless overridden by the compositor. + */ +static inline void +xdg_toplevel_set_maximized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Unmaximize the surface. + * + * After requesting that the surface should be unmaximized, the compositor + * will respond by emitting a configure event. Whether this actually + * un-maximizes the window is subject to compositor policies. + * If available and applicable, the compositor will include the window + * geometry dimensions the window had prior to being maximized in the + * configure event. The client must then update its content, drawing it in + * the configured state. The client must also acknowledge the configure + * when committing the new content (see ack_configure). + * + * It is up to the compositor to position the surface after it was + * unmaximized; usually the position the surface had before maximizing, if + * applicable. + * + * If the surface was already not maximized, the compositor will still + * emit a configure event without the "maximized" state. + * + * If the surface is in a fullscreen state, this request has no direct + * effect. It may alter the state the surface is returned to when + * unmaximized unless overridden by the compositor. + */ +static inline void +xdg_toplevel_unset_maximized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_UNSET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Make the surface fullscreen. + * + * After requesting that the surface should be fullscreened, the + * compositor will respond by emitting a configure event. Whether the + * client is actually put into a fullscreen state is subject to compositor + * policies. The client must also acknowledge the configure when + * committing the new content (see ack_configure). + * + * The output passed by the request indicates the client's preference as + * to which display it should be set fullscreen on. If this value is NULL, + * it's up to the compositor to choose which display will be used to map + * this surface. + * + * If the surface doesn't cover the whole output, the compositor will + * position the surface in the center of the output and compensate with + * with border fill covering the rest of the output. The content of the + * border fill is undefined, but should be assumed to be in some way that + * attempts to blend into the surrounding area (e.g. solid black). + * + * If the fullscreened surface is not opaque, the compositor must make + * sure that other screen content not part of the same surface tree (made + * up of subsurfaces, popups or similarly coupled surfaces) are not + * visible below the fullscreened surface. + */ +static inline void +xdg_toplevel_set_fullscreen(struct xdg_toplevel *xdg_toplevel, struct wl_output *output) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, output); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Make the surface no longer fullscreen. + * + * After requesting that the surface should be unfullscreened, the + * compositor will respond by emitting a configure event. + * Whether this actually removes the fullscreen state of the client is + * subject to compositor policies. + * + * Making a surface unfullscreen sets states for the surface based on the following: + * * the state(s) it may have had before becoming fullscreen + * * any state(s) decided by the compositor + * * any state(s) requested by the client while the surface was fullscreen + * + * The compositor may include the previous window geometry dimensions in + * the configure event, if applicable. + * + * The client must also acknowledge the configure when committing the new + * content (see ack_configure). + */ +static inline void +xdg_toplevel_unset_fullscreen(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_UNSET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Request that the compositor minimize your surface. There is no + * way to know if the surface is currently minimized, nor is there + * any way to unset minimization on this surface. + * + * If you are looking to throttle redrawing when minimized, please + * instead use the wl_surface.frame event for this, as this will + * also work with live previews on windows in Alt-Tab, Expose or + * similar compositor features. + */ +static inline void +xdg_toplevel_set_minimized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MINIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +#ifndef XDG_POPUP_ERROR_ENUM +#define XDG_POPUP_ERROR_ENUM +enum xdg_popup_error { + /** + * tried to grab after being mapped + */ + XDG_POPUP_ERROR_INVALID_GRAB = 0, +}; +#endif /* XDG_POPUP_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_popup + * @struct xdg_popup_listener + */ +struct xdg_popup_listener { + /** + * configure the popup surface + * + * This event asks the popup surface to configure itself given + * the configuration. The configured state should not be applied + * immediately. See xdg_surface.configure for details. + * + * The x and y arguments represent the position the popup was + * placed at given the xdg_positioner rule, relative to the upper + * left corner of the window geometry of the parent surface. + * + * For version 2 or older, the configure event for an xdg_popup is + * only ever sent once for the initial configuration. Starting with + * version 3, it may be sent again if the popup is setup with an + * xdg_positioner with set_reactive requested, or in response to + * xdg_popup.reposition requests. + * @param x x position relative to parent surface window geometry + * @param y y position relative to parent surface window geometry + * @param width window geometry width + * @param height window geometry height + */ + void (*configure)(void *data, + struct xdg_popup *xdg_popup, + int32_t x, + int32_t y, + int32_t width, + int32_t height); + /** + * popup interaction is done + * + * The popup_done event is sent out when a popup is dismissed by + * the compositor. The client should destroy the xdg_popup object + * at this point. + */ + void (*popup_done)(void *data, + struct xdg_popup *xdg_popup); + /** + * signal the completion of a repositioned request + * + * The repositioned event is sent as part of a popup + * configuration sequence, together with xdg_popup.configure and + * lastly xdg_surface.configure to notify the completion of a + * reposition request. + * + * The repositioned event is to notify about the completion of a + * xdg_popup.reposition request. The token argument is the token + * passed in the xdg_popup.reposition request. + * + * Immediately after this event is emitted, xdg_popup.configure and + * xdg_surface.configure will be sent with the updated size and + * position, as well as a new configure serial. + * + * The client should optionally update the content of the popup, + * but must acknowledge the new popup configuration for the new + * position to take effect. See xdg_surface.ack_configure for + * details. + * @param token reposition request token + * @since 3 + */ + void (*repositioned)(void *data, + struct xdg_popup *xdg_popup, + uint32_t token); +}; + +/** + * @ingroup iface_xdg_popup + */ +static inline int +xdg_popup_add_listener(struct xdg_popup *xdg_popup, + const struct xdg_popup_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_popup, + (void (**)(void)) listener, data); +} + +#define XDG_POPUP_DESTROY 0 +#define XDG_POPUP_GRAB 1 +#define XDG_POPUP_REPOSITION 2 + +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_CONFIGURE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_POPUP_DONE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_REPOSITIONED_SINCE_VERSION 3 + +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_GRAB_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_REPOSITION_SINCE_VERSION 3 + +/** @ingroup iface_xdg_popup */ +static inline void +xdg_popup_set_user_data(struct xdg_popup *xdg_popup, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_popup, user_data); +} + +/** @ingroup iface_xdg_popup */ +static inline void * +xdg_popup_get_user_data(struct xdg_popup *xdg_popup) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_popup); +} + +static inline uint32_t +xdg_popup_get_version(struct xdg_popup *xdg_popup) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_popup); +} + +/** + * @ingroup iface_xdg_popup + * + * This destroys the popup. Explicitly destroying the xdg_popup + * object will also dismiss the popup, and unmap the surface. + * + * If this xdg_popup is not the "topmost" popup, the + * xdg_wm_base.not_the_topmost_popup protocol error will be sent. + */ +static inline void +xdg_popup_destroy(struct xdg_popup *xdg_popup) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup, + XDG_POPUP_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_popup + * + * This request makes the created popup take an explicit grab. An explicit + * grab will be dismissed when the user dismisses the popup, or when the + * client destroys the xdg_popup. This can be done by the user clicking + * outside the surface, using the keyboard, or even locking the screen + * through closing the lid or a timeout. + * + * If the compositor denies the grab, the popup will be immediately + * dismissed. + * + * This request must be used in response to some sort of user action like a + * button press, key press, or touch down event. The serial number of the + * event should be passed as 'serial'. + * + * The parent of a grabbing popup must either be an xdg_toplevel surface or + * another xdg_popup with an explicit grab. If the parent is another + * xdg_popup it means that the popups are nested, with this popup now being + * the topmost popup. + * + * Nested popups must be destroyed in the reverse order they were created + * in, e.g. the only popup you are allowed to destroy at all times is the + * topmost one. + * + * When compositors choose to dismiss a popup, they may dismiss every + * nested grabbing popup as well. When a compositor dismisses popups, it + * will follow the same dismissing order as required from the client. + * + * If the topmost grabbing popup is destroyed, the grab will be returned to + * the parent of the popup, if that parent previously had an explicit grab. + * + * If the parent is a grabbing popup which has already been dismissed, this + * popup will be immediately dismissed. If the parent is a popup that did + * not take an explicit grab, an error will be raised. + * + * During a popup grab, the client owning the grab will receive pointer + * and touch events for all their surfaces as normal (similar to an + * "owner-events" grab in X11 parlance), while the top most grabbing popup + * will always have keyboard focus. + */ +static inline void +xdg_popup_grab(struct xdg_popup *xdg_popup, struct wl_seat *seat, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup, + XDG_POPUP_GRAB, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), 0, seat, serial); +} + +/** + * @ingroup iface_xdg_popup + * + * Reposition an already-mapped popup. The popup will be placed given the + * details in the passed xdg_positioner object, and a + * xdg_popup.repositioned followed by xdg_popup.configure and + * xdg_surface.configure will be emitted in response. Any parameters set + * by the previous positioner will be discarded. + * + * The passed token will be sent in the corresponding + * xdg_popup.repositioned event. The new popup position will not take + * effect until the corresponding configure event is acknowledged by the + * client. See xdg_popup.repositioned for details. The token itself is + * opaque, and has no other special meaning. + * + * If multiple reposition requests are sent, the compositor may skip all + * but the last one. + * + * If the popup is repositioned in response to a configure event for its + * parent, the client should send an xdg_positioner.set_parent_configure + * and possibly an xdg_positioner.set_parent_size request to allow the + * compositor to properly constrain the popup. + * + * If the popup is repositioned together with a parent that is being + * resized, but not in response to a configure event, the client should + * send an xdg_positioner.set_parent_size request. + */ +static inline void +xdg_popup_reposition(struct xdg_popup *xdg_popup, struct xdg_positioner *positioner, uint32_t token) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup, + XDG_POPUP_REPOSITION, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), 0, positioner, token); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/vrto3d/src/presenter/wibblewobble_presenter.cpp b/vrto3d/src/presenter/wibblewobble_presenter.cpp new file mode 100644 index 00000000..42acad27 --- /dev/null +++ b/vrto3d/src/presenter/wibblewobble_presenter.cpp @@ -0,0 +1,517 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ + + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include + +#include "wibblewobble_presenter.h" + +#include +#include + +#include + +#include "dx11_renderer.h" +#include "focus_policy.h" +#include "vrto3dlib/debug_log.hpp" +#include "vrto3dlib/win32_helper.hpp" + +using Microsoft::WRL::ComPtr; + +namespace { + +// --------------------------------------------------------------------------- +// WibbleWobble C source format constants. Match WWSourceFormat in the WWVROU3 +// project's WibbleWobbleCapture.h. Vendored as plain ints so we don't have +// to include any of the WW headers. +// --------------------------------------------------------------------------- +constexpr int kWWSF_SideBySideFull = 3; + +// --------------------------------------------------------------------------- +// WibbleWobbleClient.dll C ABI. Resolved at runtime so we don't link against +// WibbleWobbleClient.lib or pull in its heavy header chain. +// --------------------------------------------------------------------------- +using WWClientHandle = void*; +using PFN_WWClient_Create = WWClientHandle (__cdecl*)(void* parent_hwnd, uint32_t server_process_id); +using PFN_WWClient_IsRunning = int (__cdecl*)(WWClientHandle); +using PFN_WWClient_Destroy = void (__cdecl*)(WWClientHandle); +using PFN_WWClient_SetSourceFormat = void (__cdecl*)(WWClientHandle, int wwsf); +using PFN_WWClient_PresentFrame = int (__cdecl*)(WWClientHandle, void* shared_handle, uint64_t frame_id); + +// --------------------------------------------------------------------------- +// Registry lookup for HKLM\SOFTWARE\PHARTGAMES\WibbleWobbleClient\install_path. +// Returns the install directory (with trailing backslash, matching the value +// the WibbleWobbleClient install .bat writes), or empty on failure. +// --------------------------------------------------------------------------- +std::wstring ReadInstallPathFromRegistry() { + HKEY hKey = nullptr; + LONG r = RegOpenKeyExW( + HKEY_LOCAL_MACHINE, + L"SOFTWARE\\PHARTGAMES\\WibbleWobbleClient", + 0, + KEY_READ | KEY_WOW64_64KEY, + &hKey); + if (r != ERROR_SUCCESS) return {}; + + WCHAR buf[MAX_PATH] = {}; + DWORD bytes = sizeof(buf) - sizeof(WCHAR); // leave room for null + DWORD type = 0; + r = RegQueryValueExW(hKey, L"install_path", nullptr, &type, + reinterpret_cast(buf), &bytes); + RegCloseKey(hKey); + if (r != ERROR_SUCCESS || type != REG_SZ) return {}; + + std::wstring path = buf; + if (!path.empty() && path.back() != L'\\' && path.back() != L'/') { + path.push_back(L'\\'); + } + return path; +} + +// --------------------------------------------------------------------------- +// WibbleWobble window WndProc subclass — intercepts WM_CLOSE (Alt+F4 or X +// button on the WW window) and triggers the same SteamVR shutdown path that +// our own present window uses. WM_CLOSE is passed through to the original +// WndProc so the WW window still closes normally. +// +// Single static slot — there's only ever one WW window at a time, and only +// one WibbleWobblePresenter instance. +// --------------------------------------------------------------------------- +WNDPROC g_ww_orig_wndproc = nullptr; +HWND g_ww_subclassed_hwnd = nullptr; + +LRESULT CALLBACK WwSubclassProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { + if (msg == WM_CLOSE) { + // If a game is connected, ask it to close first so SteamVR doesn't + // prompt — same flow as our own present window's WM_CLOSE. + const uint32_t pid = g_current_app_pid.load(); + LOG() << "WibbleWobble window WM_CLOSE — pid=" << pid; + std::thread([pid]{ RequestSteamVRShutdownWithApp(pid); }).detach(); + } + WNDPROC orig = g_ww_orig_wndproc; + if (!orig) return DefWindowProcW(hwnd, msg, wp, lp); + return CallWindowProcW(orig, hwnd, msg, wp, lp); +} + +void InstallWwSubclass(HWND hwnd) { + if (!hwnd || g_ww_subclassed_hwnd == hwnd) return; + g_ww_orig_wndproc = reinterpret_cast( + SetWindowLongPtrW(hwnd, GWLP_WNDPROC, + reinterpret_cast(WwSubclassProc))); + if (g_ww_orig_wndproc) { + g_ww_subclassed_hwnd = hwnd; + LOG() << "WibbleWobblePresenter: installed WM_CLOSE subclass on WW window"; + } else { + LOG() << "WibbleWobblePresenter: SetWindowLongPtrW failed GLE=" + << GetLastError() << " — Alt+F4 on WW window won't quit SteamVR"; + } +} + +void RemoveWwSubclass() { + if (!g_ww_subclassed_hwnd || !g_ww_orig_wndproc) return; + // Only restore if our proc is still in place — the WW client could have + // re-subclassed us, in which case touching it would corrupt the chain. + auto current = reinterpret_cast( + GetWindowLongPtrW(g_ww_subclassed_hwnd, GWLP_WNDPROC)); + if (current == WwSubclassProc && IsWindow(g_ww_subclassed_hwnd)) { + SetWindowLongPtrW(g_ww_subclassed_hwnd, GWLP_WNDPROC, + reinterpret_cast(g_ww_orig_wndproc)); + } + g_ww_subclassed_hwnd = nullptr; + g_ww_orig_wndproc = nullptr; +} + +} // namespace + +namespace vrto3d { + +// --------------------------------------------------------------------------- +// pImpl — keeps Windows.h confined to the .cpp. +// --------------------------------------------------------------------------- +struct WibbleWobblePresenter::Impl { + HMODULE client_dll = nullptr; + WWClientHandle client_handle = nullptr; + PFN_WWClient_Create pCreate = nullptr; + PFN_WWClient_IsRunning pIsRunning = nullptr; + PFN_WWClient_Destroy pDestroy = nullptr; + PFN_WWClient_SetSourceFormat pSetSourceFormat = nullptr; + PFN_WWClient_PresentFrame pPresentFrame = nullptr; + + // GPU completion fence. The client samples our shared ring textures + // on its own internal D3D11 device; without an explicit Flush + wait, + // the CopyResource (out_sbs_ -> ring[idx]) and the upstream renderer + // work (L+R copies + OSD composite into out_sbs_) may still be queued + // on our immediate context when the client reads. Mirrors the + // COPY_WAIT path in WibbleWobbleVR.h. + Microsoft::WRL::ComPtr copy_wait_query; + + // Triple-buffer ring of shared SBS textures. Each PresentFrame copies + // out_sbs_ into ring[ring_idx] and hands ring_handles[ring_idx] to the + // WW client, then advances. The client samples the slot on its own + // internal D3D11 device at its own display rate; by the time we wrap + // back to a slot it has moved on. + // + // Without the ring, the *next* compositor frame's L+R CopyResources + // into out_sbs_ replace (not blend) the pixels the OSD additive-blend + // pass wrote on top — if WW reads in that window the OSD blinks out + // on alternating frames. The Flush + event-wait above only fixes our + // half of the race (queued draws on our side); the ring fixes the + // other half (next-frame writes vs. WW's still-pending read). + static constexpr int kRingSize = 3; + Microsoft::WRL::ComPtr ring[kRingSize]; + void* ring_handles[kRingSize] = {}; + int ring_idx = 0; +}; + +WibbleWobblePresenter::WibbleWobblePresenter() = default; + +WibbleWobblePresenter::~WibbleWobblePresenter() { + Shutdown(); +} + +bool WibbleWobblePresenter::Init(Dx11Renderer& renderer, + const StereoDisplayDriverConfiguration& cfg, + const FocusContext& focus) +{ + renderer_ = &renderer; + device_ = renderer.Device(); + context_ = renderer.Context(); + auto_focus_ = cfg.auto_focus; + focus_ = focus; + if (!device_ || !context_) { + LOG() << "WibbleWobblePresenter: Init failed — Dx11Renderer device/context unavailable"; + return false; + } + + std::wstring install_path = ReadInstallPathFromRegistry(); + if (install_path.empty()) { + LOG() << "WibbleWobblePresenter: HKLM\\SOFTWARE\\PHARTGAMES\\WibbleWobbleClient\\install_path " + "not found — install WibbleWobbleClient first"; + return false; + } + + impl_ = std::make_unique(); + + // Lazily create the GPU-completion fence used in PresentFrame. + { + D3D11_QUERY_DESC qd{}; + qd.Query = D3D11_QUERY_EVENT; + device_->CreateQuery(&qd, impl_->copy_wait_query.GetAddressOf()); + if (!impl_->copy_wait_query) { + LOG() << "WibbleWobblePresenter: CreateQuery(D3D11_QUERY_EVENT) failed — " + "OSD may flicker due to missing cross-device GPU sync"; + } + } + + auto fail = [&](const char* what, DWORD err = ERROR_SUCCESS) { + LOG() << "WibbleWobblePresenter: " << what + << (err ? " err=" : "") << (err ? std::to_string(err) : std::string{}); + if (impl_->client_handle && impl_->pDestroy) impl_->pDestroy(impl_->client_handle); + if (impl_->client_dll) FreeLibrary(impl_->client_dll); + impl_.reset(); + return false; + }; + + // Use install_path as DLL search root so dependent WibbleWobble*.dll + // family + Data/ resolve from there rather than our own SteamVR drivers + // folder. + SetDllDirectoryW(install_path.c_str()); + std::wstring dll_path = install_path + L"WibbleWobbleClient.dll"; + impl_->client_dll = LoadLibraryW(dll_path.c_str()); + SetDllDirectoryW(nullptr); + if (!impl_->client_dll) { + return fail("LoadLibrary(WibbleWobbleClient.dll) failed", GetLastError()); + } + + impl_->pCreate = reinterpret_cast (GetProcAddress(impl_->client_dll, "WWClient_Create")); + impl_->pIsRunning = reinterpret_cast (GetProcAddress(impl_->client_dll, "WWClient_IsRunning")); + impl_->pDestroy = reinterpret_cast (GetProcAddress(impl_->client_dll, "WWClient_Destroy")); + impl_->pSetSourceFormat = reinterpret_cast(GetProcAddress(impl_->client_dll, "WWClient_SetSourceFormat")); + impl_->pPresentFrame = reinterpret_cast (GetProcAddress(impl_->client_dll, "WWClient_PresentFrame")); + if (!impl_->pCreate || !impl_->pIsRunning || !impl_->pDestroy + || !impl_->pSetSourceFormat || !impl_->pPresentFrame) { + return fail("WibbleWobbleClient.dll missing one of WWClient_Create/IsRunning/Destroy/" + "SetSourceFormat/PresentFrame — rebuild WibbleWobbleClient with the latest " + "C-API shim"); + } + + impl_->client_handle = impl_->pCreate(nullptr, GetCurrentProcessId()); + if (!impl_->client_handle) { + return fail("WWClient_Create returned null"); + } + + // VRto3D always produces canonical 2W x H side-by-side full. Tell the + // client once at startup; SubmitExternalFrame uses this to decode. + impl_->pSetSourceFormat(impl_->client_handle, kWWSF_SideBySideFull); + + LOG() << "WibbleWobblePresenter: WibbleWobbleClient.dll loaded in-process"; + + pump_stop_.store(false); + pump_thread_ = std::thread(&WibbleWobblePresenter::PumpThreadLoop, this); + + focus_stop_.store(false); + focus_thread_ = std::thread(&WibbleWobblePresenter::FocusThreadLoop, this); + + return true; +} + +void WibbleWobblePresenter::PumpThreadLoop() { + while (!pump_stop_.load(std::memory_order_relaxed)) { + if (renderer_) renderer_->WaitAndDrawPending(33); + + // Drain this thread's Windows message queue so the WH_MOUSE_LL + // hook installed by osd_input (lazily, only while the OSD + // menu is visible) can dispatch its callbacks. + MSG msg; + while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } +} + +void WibbleWobblePresenter::FocusThreadLoop() { + // The WibbleWobbleClient names its main window "WibbleWobble". Poll for + // it (it can take a moment to appear after Create), then mirror the + // focus pattern from LeiaSrPresenter::FocusThreadLoop: + // - Track FocusContext (is_on_top / man_on_top / app_pid) + // - BringToTop / NotTopmost based on those flags + auto_focus + // - Re-assert topmost periodically (other apps can bump us) + // Plus: feed the discovered HWND to Dx11Renderer so the OSD's cursor + // coord mapping works against the actual lightfield surface rect. + HWND ww_hwnd = nullptr; + int reassert_counter = 0; + FocusLatchState focus_latch; // auto-focus per-PID latch (focus_policy.h) + bool was_on_top = false; + + while (!focus_stop_.load(std::memory_order_relaxed)) { + if (!ww_hwnd || !IsWindow(ww_hwnd)) { + ww_hwnd = FindWindowW(nullptr, L"WibbleWobble"); + if (ww_hwnd && renderer_) { + renderer_->SetOsdHeadsetHwnd(ww_hwnd); + LOG() << "WibbleWobblePresenter: located WibbleWobble window hwnd=0x" + << std::hex << reinterpret_cast(ww_hwnd); + InstallWwSubclass(ww_hwnd); + } + } + + if (ww_hwnd) { + // Shared decision (see focus_policy.h) — kept identical to + // WindowPresenter / the Linux VkRenderer focus block so they + // can't drift. + FocusInputs fi; + fi.is_on_top = focus_.is_on_top && focus_.is_on_top->load(); + fi.man_on_top = focus_.man_on_top && focus_.man_on_top->load(); + fi.app_pid = focus_.app_pid ? focus_.app_pid->load() : 0; + fi.auto_focus = focus_.auto_focus ? focus_.auto_focus->load() : auto_focus_; + fi.app_running = platform::IsProcessRunning(fi.app_pid); + fi.force_on_top = false; // topmost-model window: an open OSD is + // made visible/clickable via + // ApplyMenuVisibility, not by forcing + // topmost. + bool set_is = false, set_man = false; + const bool want_on_top = + ComputeWantOnTop(fi, focus_latch, &set_is, &set_man); + if (set_is && focus_.is_on_top) focus_.is_on_top->store(true); + if (set_man && focus_.man_on_top) focus_.man_on_top->store(true); + const uint32_t pid = fi.app_pid; + + // Reconcile against the window's actual WS_EX_TOPMOST style each + // tick — the WibbleWobbleClient sets topmost on its window + // asynchronously after creation, so a transition-based check + // (was_on_top vs want_on_top) misses the WW client flipping the + // bit on us. Reading the live style means we always converge. + const LONG_PTR ex = GetWindowLongPtrW(ww_hwnd, GWL_EXSTYLE); + const bool actually_topmost = (ex & WS_EX_TOPMOST) != 0; + const bool actually_transparent = (ex & WS_EX_TRANSPARENT) != 0; + + if (actually_topmost != want_on_top) { + SetWindowPos(ww_hwnd, + want_on_top ? HWND_TOPMOST : HWND_NOTOPMOST, + 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW); + reassert_counter = 0; + } else if (want_on_top && ++reassert_counter >= 20) { + // Periodic re-assert pushes us above any newer topmost + // windows (dialogs, dashboards) inside the topmost group. + reassert_counter = 0; + SetWindowPos(ww_hwnd, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW); + } + + // Mirror WindowPresenter / LeiaSrPresenter input semantics: + // click-through while on-top (so input falls through to the + // game window underneath) and solid otherwise. The WW client + // window ships with WS_EX_TRANSPARENT set, so we have to + // clear it on first contact to make it solid by default. + // Like the topmost reconciliation above, this runs every tick + // to override any async re-asserts from the WW client. + if (actually_transparent != want_on_top) { + SetWindowLongPtrW(ww_hwnd, GWL_EXSTYLE, + want_on_top + ? (ex | WS_EX_TRANSPARENT) + : (ex & ~WS_EX_TRANSPARENT)); + } + + // On the rising edge of want_on_top, spawn the same 15-iteration + // ForceFocus watchdog that WindowPresenter / LeiaSrPresenter use. + // The WW window being WS_EX_TOPMOST isn't enough — the game also + // needs to be the foreground window so keyboard/mouse input + // reaches it. When SteamVR is launched by the game the game + // window may not exist yet at the first transition, and + // SteamVR's bring-up (vrmonitor / status window) can grab + // foreground after the first attempt — re-asserting once per + // second for 15s handles both cases. + if (want_on_top && !was_on_top && pid != 0) { + std::thread([pid, man_on_top = focus_.man_on_top]() { + for (int i = 0; i < 15; ++i) { + if (man_on_top && !man_on_top->load()) return; + HWND game_hwnd = GetHWNDFromPID(pid); + if (game_hwnd && GetForegroundWindow() != game_hwnd) { + ForceFocus(game_hwnd, + GetCurrentThreadId(), + GetWindowThreadProcessId(game_hwnd, nullptr)); + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + }).detach(); + } + was_on_top = want_on_top; + } + + Sleep(50); + } +} + +void WibbleWobblePresenter::RecordComposite(ID3D11Texture2D* sbs_input) { + if (!impl_ || !sbs_input || !impl_->client_handle || !impl_->pPresentFrame) return; + + // Liveness check on the in-process client. If its worker thread exited, + // stop the pump so we don't keep doing pointless work. + static std::atomic client_dead_logged{false}; + if (impl_->pIsRunning(impl_->client_handle) == 0) { + bool e = false; + if (client_dead_logged.compare_exchange_strong(e, true)) { + LOG() << "WibbleWobblePresenter: WibbleWobbleClient worker stopped — " + "stopping pump thread (restart SteamVR to retry)"; + pump_stop_.store(true, std::memory_order_relaxed); + } + return; + } + + // (Re)create the ring whenever the source texture changes — Dx11Renderer + // recreates out_sbs_ on resize / format change (and on the first frame), + // which always changes the pointer. Slots are sized to match sbs_input + // so CopyResource below is a straight blit. + if (sbs_input != last_sbs_input_) { + D3D11_TEXTURE2D_DESC src{}; + sbs_input->GetDesc(&src); + + D3D11_TEXTURE2D_DESC d = src; + d.MipLevels = 1; + d.ArraySize = 1; + d.SampleDesc = {1, 0}; + d.Usage = D3D11_USAGE_DEFAULT; + d.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; + d.CPUAccessFlags = 0; + d.MiscFlags = D3D11_RESOURCE_MISC_SHARED; + + for (int i = 0; i < Impl::kRingSize; ++i) { + impl_->ring[i].Reset(); + impl_->ring_handles[i] = nullptr; + HRESULT hr = device_->CreateTexture2D(&d, nullptr, impl_->ring[i].GetAddressOf()); + if (FAILED(hr) || !impl_->ring[i]) { + LOG() << "WibbleWobblePresenter: CreateTexture2D(ring[" << i << "]) failed hr=0x" + << std::hex << hr; + continue; + } + ComPtr dxgi_res; + hr = impl_->ring[i]->QueryInterface( + __uuidof(IDXGIResource), + reinterpret_cast(dxgi_res.GetAddressOf())); + HANDLE h = nullptr; + if (SUCCEEDED(hr) && dxgi_res) { + hr = dxgi_res->GetSharedHandle(&h); + } + if (FAILED(hr) || !h) { + LOG() << "WibbleWobblePresenter: GetSharedHandle(ring[" << i << "]) failed hr=0x" + << std::hex << hr; + continue; + } + impl_->ring_handles[i] = h; + } + impl_->ring_idx = 0; + last_sbs_input_ = sbs_input; + LOG() << "WibbleWobblePresenter: SBS ring (re)created " << d.Width << "x" << d.Height + << " fmt=" << d.Format; + } + + const int idx = impl_->ring_idx; + if (!impl_->ring[idx] || !impl_->ring_handles[idx] || !context_) return; + + // Snapshot out_sbs_ (which now holds the freshly-composited stereo + + // OSD) into the current ring slot. The Flush + event-wait below covers + // the upstream renderer's queued L+R copies + OSD composite AND this + // copy, so the client's cross-device sample sees committed bytes. + context_->CopyResource(impl_->ring[idx].Get(), sbs_input); + context_->Flush(); + if (impl_->copy_wait_query) { + context_->End(impl_->copy_wait_query.Get()); + while (context_->GetData(impl_->copy_wait_query.Get(), nullptr, 0, 0) != S_OK) { + Sleep(0); // yield rather than hot-spin + } + } + + // Hand the slot off and advance. WW continues sampling ring[idx] for + // an indeterminate time on its own device; we won't overwrite the slot + // until we wrap (kRingSize frames from now), which decouples its read + // from the next compositor frame's writes into out_sbs_. + impl_->pPresentFrame(impl_->client_handle, impl_->ring_handles[idx], ++frame_id_); + impl_->ring_idx = (idx + 1) % Impl::kRingSize; +} + +void WibbleWobblePresenter::Shutdown() { + pump_stop_.store(true); + focus_stop_.store(true); + if (pump_thread_.joinable()) pump_thread_.join(); + if (focus_thread_.joinable()) focus_thread_.join(); + if (renderer_) renderer_->SetOsdHeadsetHwnd(nullptr); + RemoveWwSubclass(); + + if (impl_) { + if (impl_->client_handle && impl_->pDestroy) { + impl_->pDestroy(impl_->client_handle); + impl_->client_handle = nullptr; + } + if (impl_->client_dll) { + FreeLibrary(impl_->client_dll); + impl_->client_dll = nullptr; + } + impl_.reset(); + } + + last_sbs_input_ = nullptr; + frame_id_ = 0; + renderer_ = nullptr; + device_ = nullptr; + context_ = nullptr; +} + +} // namespace vrto3d + diff --git a/vrto3d/src/presenter/wibblewobble_presenter.h b/vrto3d/src/presenter/wibblewobble_presenter.h new file mode 100644 index 00000000..7be152c3 --- /dev/null +++ b/vrto3d/src/presenter/wibblewobble_presenter.h @@ -0,0 +1,94 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + + +#include +#include +#include +#include + +#include +#include +#include + +#include "presenter/output_presenter.h" +#include "vrto3dlib/stereo_config.h" + +class Dx11Renderer; + +namespace vrto3d { + +// WibbleWobble lightfield bridge. +// +// Loads WibbleWobbleClient.dll in-process (path discovered via the +// HKLM\SOFTWARE\PHARTGAMES\WibbleWobbleClient\install_path registry value) +// and pushes the canonical 2W x H side-by-side texture each frame via the +// WWClient_PresentFrame C-ABI shim — no shared-memory IPC, no per-frame +// CopyResource into a destination ring buffer. The client opens our shared +// HANDLE on its own internal D3D11 device. +class WibbleWobblePresenter : public IOutputPresenter { +public: + WibbleWobblePresenter(); + ~WibbleWobblePresenter() override; + + bool Init(Dx11Renderer& renderer, + const StereoDisplayDriverConfiguration& cfg, + const FocusContext& focus) override; + void RecordComposite(ID3D11Texture2D* sbs_input) override; + void Present() override {} // WW client owns presentation + void Shutdown() override; + +private: + void PumpThreadLoop(); + void FocusThreadLoop(); + + // Borrowed from Dx11Renderer; valid for the lifetime of this presenter. + Dx11Renderer* renderer_ = nullptr; + ID3D11Device* device_ = nullptr; + ID3D11DeviceContext* context_ = nullptr; + + // Drives Dx11Renderer::WaitAndDrawPending so OSD composite + PresentFrame + // get called. Other presenters do this from their window/render thread; + // we have no window so we run a bare pump. + std::thread pump_thread_; + std::atomic pump_stop_{false}; + + // Tracks the WibbleWobbleClient's visible window (title "WibbleWobble"), + // applies BringToTop focus logic per FocusContext, and feeds the HWND + // to the OSD as headset_hwnd so cursor coords map correctly. + std::thread focus_thread_; + std::atomic focus_stop_{false}; + bool auto_focus_ = true; + FocusContext focus_{}; + + // Per-frame state for WWClient_PresentFrame. + // last_sbs_input_ is the most recent out_sbs_ pointer we've seen; when + // it changes (resize / format change) we recreate the SBS ring held in + // Impl. frame_id_ is the WW client's pair-atomicity counter. + ID3D11Texture2D* last_sbs_input_ = nullptr; + uint64_t frame_id_ = 0; + + // pImpl holds Windows.h-flavored state (HMODULE, function pointers, + // PROCESS_INFORMATION). Confined to the .cpp so this header doesn't + // drag in . + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace vrto3d + diff --git a/vrto3d/src/presenter/wibblewobble_presenter_linux.cpp b/vrto3d/src/presenter/wibblewobble_presenter_linux.cpp new file mode 100644 index 00000000..20ddb3fa --- /dev/null +++ b/vrto3d/src/presenter/wibblewobble_presenter_linux.cpp @@ -0,0 +1,368 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#include "presenter/wibblewobble_presenter_linux.h" + +#include +#include + +#include + +#include "vk/vk_context.h" +#include "presenter/vk_swapchain_util.h" // PresenterLog +#include "presenter/wwclient.h" // WWSF_* format constants + +namespace vrto3d { + +WibbleWobblePresenter::~WibbleWobblePresenter() +{ + Shutdown(); +} + +bool WibbleWobblePresenter::LoadClient() +{ + lib_ = dlopen("libwwclient.so.0", RTLD_NOW | RTLD_LOCAL); + if (!lib_) + lib_ = dlopen("libwwclient.so", RTLD_NOW | RTLD_LOCAL); + if (!lib_) { + PresenterLog("wibblewobble: libwwclient.so not found (install WibbleWobbleLinux) — %s", + dlerror()); + return false; + } + p_create_ = (decltype(p_create_))dlsym(lib_, "WWClient_Create"); + p_running_ = (decltype(p_running_))dlsym(lib_, "WWClient_IsRunning"); + p_destroy_ = (decltype(p_destroy_))dlsym(lib_, "WWClient_Destroy"); + p_setfmt_ = (decltype(p_setfmt_))dlsym(lib_, "WWClient_SetSourceFormat"); + p_regbuf_ = (decltype(p_regbuf_))dlsym(lib_, "WWClient_RegisterBuffer"); + p_present_ = (decltype(p_present_))dlsym(lib_, "WWClient_PresentFrame"); + p_busy_ = (decltype(p_busy_))dlsym(lib_, "WWClient_BufferBusy"); + if (!p_create_ || !p_running_ || !p_destroy_ || !p_setfmt_ || !p_regbuf_ || !p_present_ || + !p_busy_) { + PresenterLog("wibblewobble: libwwclient missing expected symbols"); + return false; + } + return true; +} + +bool WibbleWobblePresenter::Init(vrto3d::vk::DeviceCtx* ctx, + const StereoDisplayDriverConfiguration& cfg) +{ + ctx_ = ctx; + // Canonical SbS extent = 2 * per-eye render size (what the compositor is + // told to render and what out_sbs_ ends up being). If the live frame + // differs the repack scales into this — matching wwserver's expectation of + // a full side-by-side WWSF_SideBySideFull frame. + extent_ = {(uint32_t)cfg.render_width * 2u, (uint32_t)cfg.render_height}; + if (extent_.width == 0 || extent_.height == 0) + extent_ = {3840, 1080}; + + if (!LoadClient()) + return false; + + // Render pass matching the swapchain presenters (loadOp DONT_CARE — the + // repack overwrites every pixel; final layout COLOR_ATTACHMENT so we can + // blit out of it afterwards). + VkAttachmentDescription att{}; + att.format = format_; + att.samples = VK_SAMPLE_COUNT_1_BIT; + att.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + att.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + att.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + att.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + att.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + att.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + VkAttachmentReference ref{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkSubpassDescription sub{}; + sub.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + sub.colorAttachmentCount = 1; + sub.pColorAttachments = &ref; + VkSubpassDependency dep{}; + dep.srcSubpass = VK_SUBPASS_EXTERNAL; + dep.dstSubpass = 0; + dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + VkRenderPassCreateInfo rp{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + rp.attachmentCount = 1; + rp.pAttachments = &att; + rp.subpassCount = 1; + rp.pSubpasses = ⊂ + rp.dependencyCount = 1; + rp.pDependencies = &dep; + if (vkCreateRenderPass(ctx_->device, &rp, nullptr, &render_pass_) != VK_SUCCESS) { + PresenterLog("wibblewobble: render pass creation failed"); + return false; + } + + VkCommandPoolCreateInfo cpci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + cpci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + cpci.queueFamilyIndex = ctx_->queue_family; + vkCreateCommandPool(ctx_->device, &cpci, nullptr, &cmd_pool_); + + if (!CreateFrames()) + return false; + + ww_ = reinterpret_cast(p_create_("vrto3d")); + if (!ww_) { + PresenterLog("wibblewobble: WWClient_Create failed"); + return false; + } + // XRGB8888 <- B8G8R8A8_UNORM. Full side-by-side. + p_setfmt_(ww_, WWSF_SIDE_BY_SIDE_FULL, extent_.width, extent_.height, + DRM_FORMAT_XRGB8888, DRM_FORMAT_MOD_LINEAR); + for (int i = 0; i < kRing; ++i) { + const int idx = p_regbuf_(ww_, frames_[i].dmabuf_fd, frames_[i].stride, 0, + (uint64_t)frames_[i].stride * extent_.height); + if (idx != i) + PresenterLog("wibblewobble: buffer %d registered as %d", i, idx); + } + PresenterLog("wibblewobble: streaming %ux%u SbS to wwserver", extent_.width, extent_.height); + return true; +} + +bool WibbleWobblePresenter::CreateFrames() +{ + frames_.resize(kRing); + auto* get_fd = (PFN_vkGetMemoryFdKHR)vkGetDeviceProcAddr(ctx_->device, "vkGetMemoryFdKHR"); + if (!get_fd) { + PresenterLog("wibblewobble: vkGetMemoryFdKHR unavailable"); + return false; + } + + VkCommandBufferAllocateInfo cbai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; + cbai.commandPool = cmd_pool_; + cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + cbai.commandBufferCount = 1; + + for (auto& f : frames_) { + // ---- render target (OPTIMAL, COLOR_ATTACHMENT + TRANSFER_SRC) ---- + vrto3d::vk::Image2D render; + if (!vrto3d::vk::CreateImage2D(*ctx_, extent_.width, extent_.height, format_, + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + VK_IMAGE_TILING_OPTIMAL, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + /*make_view=*/true, &render)) + return false; + f.render_img = render.image; + f.render_mem = render.memory; + f.render_view = render.view; + VkFramebufferCreateInfo fbi{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + fbi.renderPass = render_pass_; + fbi.attachmentCount = 1; + fbi.pAttachments = &f.render_view; + fbi.width = extent_.width; + fbi.height = extent_.height; + fbi.layers = 1; + if (vkCreateFramebuffer(ctx_->device, &fbi, nullptr, &f.framebuffer) != VK_SUCCESS) + return false; + + // ---- exportable LINEAR dmabuf (TRANSFER_DST) handed to wwserver ---- + VkExternalMemoryImageCreateInfo ext{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO}; + ext.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; + VkImageCreateInfo dici{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + dici.pNext = &ext; + dici.imageType = VK_IMAGE_TYPE_2D; + dici.format = format_; + dici.extent = {extent_.width, extent_.height, 1}; + dici.mipLevels = 1; + dici.arrayLayers = 1; + dici.samples = VK_SAMPLE_COUNT_1_BIT; + dici.tiling = VK_IMAGE_TILING_LINEAR; // portable cross-process import + dici.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT; + dici.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + if (vkCreateImage(ctx_->device, &dici, nullptr, &f.dmabuf_img) != VK_SUCCESS) + return false; + VkMemoryRequirements dreqs{}; + vkGetImageMemoryRequirements(ctx_->device, f.dmabuf_img, &dreqs); + VkExportMemoryAllocateInfo emai{VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO}; + emai.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; + VkMemoryDedicatedAllocateInfo ded{VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO}; + ded.image = f.dmabuf_img; + ded.pNext = &emai; + VkMemoryAllocateInfo dmai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + dmai.pNext = &ded; + dmai.allocationSize = dreqs.size; + dmai.memoryTypeIndex = ctx_->FindMemoryType(dreqs.memoryTypeBits, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + if (dmai.memoryTypeIndex == UINT32_MAX) + dmai.memoryTypeIndex = ctx_->FindMemoryType(dreqs.memoryTypeBits, 0); + if (vkAllocateMemory(ctx_->device, &dmai, nullptr, &f.dmabuf_mem) != VK_SUCCESS) + return false; + vkBindImageMemory(ctx_->device, f.dmabuf_img, f.dmabuf_mem, 0); + + VkMemoryGetFdInfoKHR gfi{VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR}; + gfi.memory = f.dmabuf_mem; + gfi.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; + if (get_fd(ctx_->device, &gfi, &f.dmabuf_fd) != VK_SUCCESS || f.dmabuf_fd < 0) { + PresenterLog("wibblewobble: dmabuf export failed"); + return false; + } + VkImageSubresource sr{VK_IMAGE_ASPECT_COLOR_BIT, 0, 0}; + VkSubresourceLayout layout{}; + vkGetImageSubresourceLayout(ctx_->device, f.dmabuf_img, &sr, &layout); + f.stride = (uint32_t)layout.rowPitch; + + vkAllocateCommandBuffers(ctx_->device, &cbai, &f.cmd); + VkFenceCreateInfo fci{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + fci.flags = VK_FENCE_CREATE_SIGNALED_BIT; + vkCreateFence(ctx_->device, &fci, nullptr, &f.fence); + } + return true; +} + +bool WibbleWobblePresenter::PumpEvents() +{ + // No window; the pipeline lives as long as wwserver is reachable. Report + // liveness so the renderer keeps feeding (a dropped server just means + // frames are discarded until it returns). + return true; +} + +bool WibbleWobblePresenter::AcquireNext(FrameTarget* out, VkSemaphore signal_sem) +{ + // Round-robin, skipping buffers wwserver still holds. + int chosen = -1; + for (int t = 0; t < kRing; ++t) { + const int i = (next_ + t) % kRing; + if (!p_busy_(ww_, i)) { chosen = i; break; } + } + if (chosen < 0) + chosen = next_; // all busy: reuse oldest (server will drop it) + next_ = (chosen + 1) % kRing; + Frame& f = frames_[chosen]; + + // Wait for our own prior blit on this slot to finish, then signal the + // renderer's acquire semaphore (our images are ready as soon as the fence + // clears — but the binary semaphore still needs a queue signal). + if (f.in_flight) { + vkWaitForFences(ctx_->device, 1, &f.fence, VK_TRUE, UINT64_MAX); + f.in_flight = false; + } + VkSubmitInfo sig{VK_STRUCTURE_TYPE_SUBMIT_INFO}; + sig.signalSemaphoreCount = 1; + sig.pSignalSemaphores = &signal_sem; + { + std::lock_guard lock(ctx_->queue_mutex); + vkQueueSubmit(ctx_->queue, 1, &sig, VK_NULL_HANDLE); + } + + out->image = f.render_img; + out->view = f.render_view; + out->framebuffer = f.framebuffer; + out->index = (uint32_t)chosen; + return true; +} + +bool WibbleWobblePresenter::Present(uint32_t image_index, VkSemaphore wait_sem) +{ + Frame& f = frames_[image_index]; + + // Blit the just-rendered SbS (TRANSFER_SRC after the render pass) into the + // linear dmabuf, waiting on the repack's render semaphore (consumes it). + vkResetFences(ctx_->device, 1, &f.fence); + vkResetCommandBuffer(f.cmd, 0); + VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; + begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(f.cmd, &begin); + + VkImageMemoryBarrier to_dst{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + to_dst.srcAccessMask = 0; + to_dst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + to_dst.oldLayout = f.dmabuf_initialized ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED; + to_dst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + to_dst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + to_dst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + to_dst.image = f.dmabuf_img; + to_dst.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdPipelineBarrier(f.cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &to_dst); + + VkImageCopy copy{}; + copy.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.extent = {extent_.width, extent_.height, 1}; + vkCmdCopyImage(f.cmd, f.render_img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, f.dmabuf_img, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + + // Hand back to GENERAL so the importing process (wwserver) samples a + // well-defined layout; implicit dmabuf sync orders its read after this. + VkImageMemoryBarrier to_general{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + to_general.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + to_general.dstAccessMask = 0; + to_general.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + to_general.newLayout = VK_IMAGE_LAYOUT_GENERAL; + to_general.srcQueueFamilyIndex = ctx_->queue_family; + to_general.dstQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL; + to_general.image = f.dmabuf_img; + to_general.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdPipelineBarrier(f.cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + 0, 0, nullptr, 0, nullptr, 1, &to_general); + vkEndCommandBuffer(f.cmd); + f.dmabuf_initialized = true; + + VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_TRANSFER_BIT; + VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; + submit.waitSemaphoreCount = 1; + submit.pWaitSemaphores = &wait_sem; + submit.pWaitDstStageMask = &wait_stage; + submit.commandBufferCount = 1; + submit.pCommandBuffers = &f.cmd; + { + std::lock_guard lock(ctx_->queue_mutex); + if (vkQueueSubmit(ctx_->queue, 1, &submit, f.fence) != VK_SUCCESS) + return false; + } + f.in_flight = true; + + if (p_running_(ww_)) + p_present_(ww_, (int)image_index, ++frame_id_); + return true; +} + +void WibbleWobblePresenter::DestroyFrames() +{ + for (auto& f : frames_) { + if (f.fence) vkDestroyFence(ctx_->device, f.fence, nullptr); + if (f.framebuffer) vkDestroyFramebuffer(ctx_->device, f.framebuffer, nullptr); + if (f.render_view) vkDestroyImageView(ctx_->device, f.render_view, nullptr); + if (f.render_img) vkDestroyImage(ctx_->device, f.render_img, nullptr); + if (f.render_mem) vkFreeMemory(ctx_->device, f.render_mem, nullptr); + if (f.dmabuf_img) vkDestroyImage(ctx_->device, f.dmabuf_img, nullptr); + if (f.dmabuf_mem) vkFreeMemory(ctx_->device, f.dmabuf_mem, nullptr); + // dmabuf_fd ownership passed to libwwclient (it dup'd); close ours. + if (f.dmabuf_fd >= 0) close(f.dmabuf_fd); + } + frames_.clear(); +} + +void WibbleWobblePresenter::Shutdown() +{ + if (ctx_ && ctx_->device != VK_NULL_HANDLE) { + std::lock_guard lock(ctx_->queue_mutex); + vkQueueWaitIdle(ctx_->queue); + } + if (ww_ && p_destroy_) { + p_destroy_(ww_); + ww_ = nullptr; + } + if (ctx_ && ctx_->device != VK_NULL_HANDLE) { + DestroyFrames(); + if (cmd_pool_) { vkDestroyCommandPool(ctx_->device, cmd_pool_, nullptr); cmd_pool_ = VK_NULL_HANDLE; } + if (render_pass_) { vkDestroyRenderPass(ctx_->device, render_pass_, nullptr); render_pass_ = VK_NULL_HANDLE; } + } + if (lib_) { dlclose(lib_); lib_ = nullptr; } +} + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/wibblewobble_presenter_linux.h b/vrto3d/src/presenter/wibblewobble_presenter_linux.h new file mode 100644 index 00000000..5f5fb82f --- /dev/null +++ b/vrto3d/src/presenter/wibblewobble_presenter_linux.h @@ -0,0 +1,99 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +// Linux WibbleWobble output: instead of presenting to a local display, hand the +// composited side-by-side frame to a running WibbleWobbleLinux server +// (wwserver) over libwwclient, which does the frame-sequential presentation + +// shutter-glasses sync. VRto3D's repack pass renders the canonical SbS into a +// ring of exportable dmabuf images (this presenter's FrameTargets); Present() +// blits into a linear dmabuf and announces it to wwserver as WWSF_SideBySideFull. +// +// libwwclient is dlopen'd — if WibbleWobbleLinux isn't installed, Init fails +// gracefully (the driver logs and the output mode is inert). + +#include +#include + +#include + +#include "presenter/vk_presenter.h" + +namespace vrto3d { + +class WibbleWobblePresenter final : public IVkPresenter { +public: + ~WibbleWobblePresenter() override; + + bool Init(vrto3d::vk::DeviceCtx* ctx, const StereoDisplayDriverConfiguration& cfg) override; + void Shutdown() override; + bool PumpEvents() override; + bool AcquireNext(FrameTarget* out, VkSemaphore signal_sem) override; + bool Present(uint32_t image_index, VkSemaphore wait_sem) override; + + VkRenderPass RenderPass() const override { return render_pass_; } + VkExtent2D Extent() const override { return extent_; } + VkFormat Format() const override { return format_; } + const char* Name() const override { return "wibblewobble"; } + +private: + struct Frame { + // Render target the repack pass draws the SbS into (OPTIMAL). + VkImage render_img = VK_NULL_HANDLE; + VkDeviceMemory render_mem = VK_NULL_HANDLE; + VkImageView render_view = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + // Exportable linear dmabuf handed to wwserver. + VkImage dmabuf_img = VK_NULL_HANDLE; + VkDeviceMemory dmabuf_mem = VK_NULL_HANDLE; + int dmabuf_fd = -1; + uint32_t stride = 0; + bool dmabuf_initialized = false; // layout tracking for blit + VkCommandBuffer cmd = VK_NULL_HANDLE; // blit + acquire-signal + VkFence fence = VK_NULL_HANDLE; + bool in_flight = false; + }; + + bool LoadClient(); + bool CreateFrames(); + void DestroyFrames(); + + vrto3d::vk::DeviceCtx* ctx_ = nullptr; + VkExtent2D extent_{}; + VkFormat format_ = VK_FORMAT_B8G8R8A8_UNORM; + VkRenderPass render_pass_ = VK_NULL_HANDLE; + VkCommandPool cmd_pool_ = VK_NULL_HANDLE; + + static constexpr int kRing = 3; + std::vector frames_; + int next_ = 0; + uint64_t frame_id_ = 0; + + // libwwclient (dlopen'd) + void* lib_ = nullptr; + struct WWClientOpaque* ww_ = nullptr; + // resolved symbols + void* (*p_create_)(const char*) = nullptr; + int (*p_running_)(void*) = nullptr; + void (*p_destroy_)(void*) = nullptr; + void (*p_setfmt_)(void*, int, uint32_t, uint32_t, uint32_t, uint64_t) = nullptr; + int (*p_regbuf_)(void*, int, uint32_t, uint32_t, uint64_t) = nullptr; + int (*p_present_)(void*, int, uint64_t) = nullptr; + int (*p_busy_)(void*, int) = nullptr; +}; + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/window_presenter.cpp b/vrto3d/src/presenter/window_presenter.cpp new file mode 100644 index 00000000..3f93918b --- /dev/null +++ b/vrto3d/src/presenter/window_presenter.cpp @@ -0,0 +1,886 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ + +#include "window_presenter.h" + +#include +#include + +#include +#pragma comment(lib, "d3dcompiler.lib") + +#include "dx11_renderer.h" +#include "focus_policy.h" +#include "hmd_device_driver.h" +#include "vrto3dlib/debug_log.hpp" +#include "vrto3dlib/win32_helper.hpp" + +using Microsoft::WRL::ComPtr; + +namespace vrto3d { + +namespace { + +// Mode constants matching OutputMode values the shader handles (LeiaSR / NvidiaDX9 +// are handled by their own presenters — not this one). +// +// DualDisplay / DualDisplayFlip share the SbS shader path; their distinction is +// the window spans two contiguous monitors (handled at window-creation time). +// DualDisplayFlip flips the left half vertically. +constexpr uint32_t kModeSbS = 0; +constexpr uint32_t kModeTaB = 1; +constexpr uint32_t kModeRowInterlaced = 2; +constexpr uint32_t kModeColInterlaced = 3; +constexpr uint32_t kModeCheckerboard = 4; +constexpr uint32_t kModeVirtualDesktop = 5; +constexpr uint32_t kModeDualDisplayFlip = 6; +constexpr uint32_t kModeAnaglyphRedCyan = 7; +constexpr uint32_t kModeAnaglyphRedCyanDubois = 8; +constexpr uint32_t kModeAnaglyphRedCyanDeghosted = 9; +constexpr uint32_t kModeAnaglyphRedCyanCompromise = 10; +constexpr uint32_t kModeAnaglyphGreenMagenta = 11; +constexpr uint32_t kModeAnaglyphGreenMagentaDubois = 12; +constexpr uint32_t kModeAnaglyphGreenMagentaDeghosted = 13; +constexpr uint32_t kModeAnaglyphBlueAmber = 14; +constexpr uint32_t kModeMono = 15; + +uint32_t ModeToShaderEnum(OutputMode m) +{ + switch (m) { + case OutputMode::SbS: return kModeSbS; + case OutputMode::TaB: return kModeTaB; + case OutputMode::RowInterlaced: return kModeRowInterlaced; + case OutputMode::ColInterlaced: return kModeColInterlaced; + case OutputMode::Checkerboard: return kModeCheckerboard; + case OutputMode::VirtualDesktop: return kModeVirtualDesktop; + case OutputMode::FramePacked720p60: return kModeTaB; + case OutputMode::FramePacked1080p24: return kModeTaB; + case OutputMode::FramePacked1080p60: return kModeTaB; + case OutputMode::FramePacked1080p60CVT: return kModeTaB; + case OutputMode::DualDisplay: return kModeSbS; + case OutputMode::DualDisplayFlip: return kModeDualDisplayFlip; + case OutputMode::AnaglyphRedCyan: return kModeAnaglyphRedCyan; + case OutputMode::AnaglyphRedCyanDubois: return kModeAnaglyphRedCyanDubois; + case OutputMode::AnaglyphRedCyanDeghosted: return kModeAnaglyphRedCyanDeghosted; + case OutputMode::AnaglyphRedCyanCompromise: return kModeAnaglyphRedCyanCompromise; + case OutputMode::AnaglyphGreenMagenta: return kModeAnaglyphGreenMagenta; + case OutputMode::AnaglyphGreenMagentaDubois: return kModeAnaglyphGreenMagentaDubois; + case OutputMode::AnaglyphGreenMagentaDeghosted: return kModeAnaglyphGreenMagentaDeghosted; + case OutputMode::AnaglyphBlueAmber: return kModeAnaglyphBlueAmber; + case OutputMode::Mono: return kModeMono; + default: return kModeSbS; + } +} + +// Vertex shader: generates a full-screen triangle from SV_VertexID (no vertex buffer). +const char* kVsSource = R"( +struct VSOut { float4 pos : SV_Position; float2 uv : TEXCOORD0; }; +VSOut main(uint vid : SV_VertexID) { + VSOut o; + float2 p = float2((vid << 1) & 2, vid & 2); + o.uv = p; + o.pos = float4(p * float2(2, -2) + float2(-1, 1), 0, 1); + return o; +} +)"; + +// Pixel shader: samples the 2W x H SbS input texture and repacks according to `mode`. +// cbuffer layout mirrors CBParams in the header. +const char* kPsSource = R"( +Texture2D g_sbs : register(t0); +SamplerState g_smp : register(s0); + +cbuffer Params : register(b0) { + uint mode; + uint framepack_offset; + uint eye_swap; + float out_width; + float out_height; + float aspect_ratio; +}; + +// Given a half (0=left, 1=right), sample the input SbS at the given (u_half, v). +// u_half in [0,1] = position within the eye's half of the input. +float4 SampleEye(uint half_idx, float u_half, float v) { + // eye_swap flips which half-source we read. + uint eye = (eye_swap != 0) ? (1u - half_idx) : half_idx; + float u_src = 0.5 * u_half + 0.5 * float(eye); + return g_sbs.Sample(g_smp, float2(u_src, v)); +} + +float4 main(float4 pos : SV_Position, float2 uv : TEXCOORD0) : SV_Target { + // SbS + if (mode == 0) { + uint half_idx = (uv.x < 0.5) ? 0u : 1u; + float u_half = (uv.x < 0.5) ? (uv.x * 2.0) : ((uv.x - 0.5) * 2.0); + return SampleEye(half_idx, u_half, uv.y); + } + + // TaB + if (mode == 1) { + float gap = (framepack_offset > 0) ? (float(framepack_offset) / out_height) : 0.0; + float half_height = (1.0 - gap) * 0.5; + if (uv.y < half_height) { + // Top half -> left eye + return SampleEye(0u, uv.x, uv.y / half_height); + } else if (uv.y >= half_height + gap) { + // Bottom half -> right eye + float v_rel = (uv.y - (half_height + gap)) / half_height; + return SampleEye(1u, uv.x, v_rel); + } else { + return float4(0,0,0,1); // framepack gap + } + } + + // Row-interlaced: alternating rows; odd rows = right eye. + if (mode == 2) { + uint row = (uint)floor(uv.y * out_height); + uint half_idx = (row & 1u); + return SampleEye(half_idx, uv.x, uv.y); + } + + // Col-interlaced + if (mode == 3) { + uint col = (uint)floor(uv.x * out_width); + uint half_idx = (col & 1u); + return SampleEye(half_idx, uv.x, uv.y); + } + + // Checkerboard + if (mode == 4) { + uint row = (uint)floor(uv.y * out_height); + uint col = (uint)floor(uv.x * out_width); + uint half_idx = ((row + col) & 1u); + return SampleEye(half_idx, uv.x, uv.y); + } + + // VirtualDesktop: SbS in center band of a 2W x 2H window (rows [0.25, 0.75)). + if (mode == 5) { + if (uv.y < 0.25 || uv.y >= 0.75) return float4(0,0,0,1); + float v = (uv.y - 0.25) * 2.0; + uint half_idx = (uv.x < 0.5) ? 0u : 1u; + float u_half = (uv.x < 0.5) ? (uv.x * 2.0) : ((uv.x - 0.5) * 2.0); + return SampleEye(half_idx, u_half, v); + } + + // DualDisplayFlip: same horizontal split as SbS/DualDisplay, but the LEFT + // half is sampled with v inverted (vertical flip). + if (mode == 6) { + uint half_idx = (uv.x < 0.5) ? 0u : 1u; + float u_half = (uv.x < 0.5) ? (uv.x * 2.0) : ((uv.x - 0.5) * 2.0); + float v_src = (half_idx == 0u) ? (1.0 - uv.y) : uv.y; + return SampleEye(half_idx, u_half, v_src); + } + + // ----- Anaglyph variants (3DToElse / iaian7+vectorform formulas) ----- + // Sample full image of each eye; cA = left, cB = right. + float4 cA = SampleEye(0u, uv.x, uv.y); + float4 cB = SampleEye(1u, uv.x, uv.y); + + // Anaglyph red-cyan (simple) + if (mode == 7) { + return float4(cA.r, cB.g, cB.b, 1.0); + } + + // Anaglyph red-cyan Dubois + if (mode == 8) { + float r = saturate( 0.437 * cA.r + 0.449 * cA.g + 0.164 * cA.b + - 0.011 * cB.r - 0.032 * cB.g - 0.007 * cB.b ); + float g = saturate(-0.062 * cA.r - 0.062 * cA.g - 0.024 * cA.b + + 0.377 * cB.r + 0.761 * cB.g + 0.009 * cB.b ); + float b = saturate(-0.048 * cA.r - 0.050 * cA.g - 0.017 * cA.b + - 0.026 * cB.r - 0.093 * cB.g + 1.234 * cB.b ); + return float4(r, g, b, 1.0); + } + + // Anaglyph red-cyan deghosted (iaian7 / vectorform) + if (mode == 9) { + float Contrast = 1.0; + float DeGhost = 0.06 * 0.1; + float contrast = (Contrast * 0.5) + 0.5; + float LOne = contrast * 0.45; + float ROne = contrast; + + float4 image = float4(0,0,0,1); + float4 accum; + accum = saturate(cA * float4(LOne, (1.0-LOne)*0.5, (1.0-LOne)*0.5, 1.0)); + image.r = pow(accum.r + accum.g + accum.b, 1.00); + accum = saturate(cB * float4(1.0-ROne, ROne, 0.0, 1.0)); + image.g = pow(accum.r + accum.g + accum.b, 1.15); + accum = saturate(cB * float4(1.0-ROne, 0.0, ROne, 1.0)); + image.b = pow(accum.r + accum.g + accum.b, 1.15); + + accum = image; + image.r = accum.r + (accum.r * DeGhost) + (accum.g * (DeGhost * -0.5)) + (accum.b * (DeGhost * -0.5)); + image.g = accum.g + (accum.r * (DeGhost * -0.25)) + (accum.g * (DeGhost * 0.5)) + (accum.b * (DeGhost * -0.25)); + image.b = accum.b + (accum.r * (DeGhost * -0.25)) + (accum.g * (DeGhost * -0.25)) + (accum.b * (DeGhost * 0.5)); + image.a = 1.0; + return image; + } + + // Anaglyph red-cyan compromise + if (mode == 10) { + float3x3 lf = float3x3( + float3(0.439, 0.447, 0.148), // r_a = dot(left_rgb, this row) + float3(0.0, 0.0, 0.0 ), // g_a + float3(0.0, 0.0, 0.0 )); // b_a + float3x3 rf = float3x3( + float3(0.0, 0.0, 0.0 ), // r_a + float3(0.095, 0.934, -0.005), // g_a = dot(right_rgb, this row) + float3(-0.018, -0.028, 1.057)); // b_a = dot(right_rgb, this row) + return float4(saturate(mul(lf, cA.rgb) + mul(rf, cB.rgb)), 1.0); + } + + // Anaglyph green-magenta (simple) + if (mode == 11) { + return float4(cB.r, cA.g, cB.b, 1.0); + } + + // Anaglyph green-magenta Dubois + if (mode == 12) { + float r = saturate(-0.062 * cA.r - 0.158 * cA.g - 0.039 * cA.b + + 0.529 * cB.r + 0.705 * cB.g + 0.024 * cB.b ); + float g = saturate( 0.284 * cA.r + 0.668 * cA.g + 0.143 * cA.b + - 0.016 * cB.r - 0.015 * cB.g + 0.065 * cB.b ); + float b = saturate(-0.015 * cA.r - 0.027 * cA.g + 0.021 * cA.b + + 0.009 * cB.r + 0.075 * cB.g + 0.937 * cB.b ); + return float4(r, g, b, 1.0); + } + + // Anaglyph green-magenta deghosted + if (mode == 13) { + float Contrast = 1.0; + float DeGhost = 0.06 * 0.275; + float contrast = (Contrast * 0.5) + 0.5; + float LOne = contrast * 0.45; + float ROne = contrast * 0.8; + + float4 image = float4(0,0,0,1); + float4 accum; + accum = saturate(cB * float4(ROne, 1.0-ROne, 0.0, 1.0)); + image.r = pow(accum.r + accum.g + accum.b, 1.15); + accum = saturate(cA * float4((1.0-LOne)*0.5, LOne, (1.0-LOne)*0.5, 1.0)); + image.g = pow(accum.r + accum.g + accum.b, 1.05); + accum = saturate(cB * float4(0.0, 1.0-ROne, ROne, 1.0)); + image.b = pow(accum.r + accum.g + accum.b, 1.15); + + accum = image; + image.r = accum.r + (accum.r * (DeGhost * 0.5)) + (accum.g * (DeGhost * -0.25)) + (accum.b * (DeGhost * -0.25)); + image.g = accum.g + (accum.r * (DeGhost * -0.5)) + (accum.g * (DeGhost * 0.25)) + (accum.b * (DeGhost * -0.5)); + image.b = accum.b + (accum.r * (DeGhost * -0.25))+ (accum.g * (DeGhost * -0.25)) + (accum.b * (DeGhost * 0.5)); + image.a = 1.0; + return image; + } + + // Anaglyph blue-amber (ColorCode style) + if (mode == 14) { + float Contrast = 1.0; + float DeGhost = 0.06 * 0.275; + float contrast = (Contrast * 0.5) + 0.5; + float LOne = contrast * 0.45; + float ROne = contrast; + + float4 image = float4(0,0,0,1); + float4 accum; + accum = saturate(cA * float4(ROne, 0.0, 1.0-ROne, 1.0)); + image.r = pow(accum.r + accum.g + accum.b, 1.05); + accum = saturate(cA * float4(0.0, ROne, 1.0-ROne, 1.0)); + image.g = pow(accum.r + accum.g + accum.b, 1.10); + accum = saturate(cB * float4((1.0-LOne)*0.5, (1.0-LOne)*0.5, LOne, 1.0)); + image.b = pow(accum.r + accum.g + accum.b, 1.0); + image.b = lerp(pow(image.b, (DeGhost * 0.15) + 1.0), + 1.0 - pow(abs(1.0 - image.b), (DeGhost * 0.15) + 1.0), + image.b); + + accum = image; + image.r = accum.r + (accum.r * (DeGhost * 1.5)) + (accum.g * (DeGhost * -0.75)) + (accum.b * (DeGhost * -0.75)); + image.g = accum.g + (accum.r * (DeGhost * -0.75)) + (accum.g * (DeGhost * 1.5)) + (accum.b * (DeGhost * -0.75)); + image.b = accum.b + (accum.r * (DeGhost * -1.5)) + (accum.g * (DeGhost * -1.5)) + (accum.b * (DeGhost * 3.0)); + return saturate(image); + } + + // Mono — single-eye view. eye_swap selects right (1) instead of left (0). + if (mode == 15) { + return SampleEye(0u, uv.x, uv.y); + } + + // Fallback + return float4(uv, 0, 1); +} +)"; + + +bool CompileShader(const char* src, const char* entry, const char* profile, + ComPtr& out_blob) +{ + ComPtr errors; + UINT flags = D3DCOMPILE_ENABLE_STRICTNESS; +#ifdef _DEBUG + flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; +#else + flags |= D3DCOMPILE_OPTIMIZATION_LEVEL3; +#endif + HRESULT hr = D3DCompile(src, std::strlen(src), nullptr, nullptr, nullptr, + entry, profile, flags, 0, &out_blob, &errors); + if (FAILED(hr)) { + if (errors) { + LOG() << "D3DCompile(" << entry << ") error: " + << static_cast(errors->GetBufferPointer()); + } + return false; + } + return true; +} + +} // namespace + + +bool WindowPresenter::CreateShaders() +{ + ID3D11Device* dev = renderer_->Device(); + + ComPtr vs_blob, ps_blob; + if (!CompileShader(kVsSource, "main", "vs_5_0", vs_blob)) return false; + if (!CompileShader(kPsSource, "main", "ps_5_0", ps_blob)) return false; + + if (FAILED(dev->CreateVertexShader(vs_blob->GetBufferPointer(), vs_blob->GetBufferSize(), nullptr, &vs_))) return false; + if (FAILED(dev->CreatePixelShader(ps_blob->GetBufferPointer(), ps_blob->GetBufferSize(), nullptr, &ps_))) return false; + + D3D11_SAMPLER_DESC sd{}; + sd.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + sd.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + sd.ComparisonFunc = D3D11_COMPARISON_NEVER; + sd.MinLOD = 0; + sd.MaxLOD = D3D11_FLOAT32_MAX; + if (FAILED(dev->CreateSamplerState(&sd, &sampler_))) return false; + + D3D11_BUFFER_DESC bd{}; + bd.ByteWidth = (sizeof(CBParams) + 15u) & ~15u; + bd.Usage = D3D11_USAGE_DYNAMIC; + bd.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + if (FAILED(dev->CreateBuffer(&bd, nullptr, &cb_))) return false; + + D3D11_RASTERIZER_DESC rd{}; + rd.FillMode = D3D11_FILL_SOLID; + rd.CullMode = D3D11_CULL_NONE; + rd.DepthClipEnable = TRUE; + if (FAILED(dev->CreateRasterizerState(&rd, &rasterizer_))) return false; + + D3D11_BLEND_DESC bl{}; + bl.RenderTarget[0].BlendEnable = FALSE; + bl.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + if (FAILED(dev->CreateBlendState(&bl, &blend_))) return false; + + return true; +} + + +bool WindowPresenter::CreateSwapChain(Dx11Renderer& renderer) +{ + ID3D11Device* dev = renderer.Device(); + + ComPtr dxgi_dev; + if (FAILED(dev->QueryInterface(IID_PPV_ARGS(&dxgi_dev)))) return false; + ComPtr adapter; + if (FAILED(dxgi_dev->GetAdapter(&adapter))) return false; + ComPtr factory; + if (FAILED(adapter->GetParent(IID_PPV_ARGS(&factory)))) return false; + + // Flip-model + waitable swap chain. The waitable handle (released once per + // display refresh) gates pacing on the window thread, so Present(0, 0) + // queues the flip and returns immediately — tear-free without blocking + // inside Present (which is what would stall the compositor thread via + // the shared context_mutex_). + DXGI_SWAP_CHAIN_DESC1 scd{}; + scd.Width = window_->Width(); + scd.Height = window_->Height(); + scd.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + scd.SampleDesc = { 1, 0 }; + scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + scd.BufferCount = 2; + scd.Scaling = DXGI_SCALING_STRETCH; + scd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + scd.AlphaMode = DXGI_ALPHA_MODE_IGNORE; + scd.Flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT; + + HWND hwnd = static_cast(window_->NativeHandle()); + HRESULT hr = factory->CreateSwapChainForHwnd(dev, hwnd, &scd, nullptr, nullptr, &swapchain_); + if (FAILED(hr)) { + LOG() << "CreateSwapChainForHwnd failed hr=" << std::hex << hr; + return false; + } + factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_WINDOW_CHANGES); + + if (FAILED(swapchain_.As(&swapchain2_)) || !swapchain2_) { + LOG() << "WindowPresenter: QueryInterface(IDXGISwapChain2) failed"; + return false; + } + swapchain2_->SetMaximumFrameLatency(1); + frame_latency_wait_ = swapchain2_->GetFrameLatencyWaitableObject(); + if (!frame_latency_wait_) { + LOG() << "WindowPresenter: GetFrameLatencyWaitableObject returned null"; + } + + DXGI_SWAP_CHAIN_DESC1 actual{}; + swapchain_->GetDesc1(&actual); + LOG() << "WindowPresenter: swapchain created " << actual.Width << "x" << actual.Height + << " (FLIP_DISCARD, " << actual.BufferCount << " buffers, waitable=" + << (frame_latency_wait_ ? "yes" : "no") << ")"; + + ComPtr bb; + if (FAILED(swapchain_->GetBuffer(0, IID_PPV_ARGS(&bb)))) return false; + if (FAILED(dev->CreateRenderTargetView(bb.Get(), nullptr, &swapchain_rtv_))) return false; + + swap_width_ = window_->Width(); + swap_height_ = window_->Height(); + return true; +} + + +bool WindowPresenter::Init(Dx11Renderer& renderer, + const StereoDisplayDriverConfiguration& cfg, + const FocusContext& focus) +{ + renderer_ = &renderer; + mode_ = cfg.output_mode; + eye_swap_ = cfg.eye_swap; + aspect_ratio_ = cfg.aspect_ratio; + spans_two_monitors_ = (mode_ == OutputMode::DualDisplay + || mode_ == OutputMode::DualDisplayFlip); + auto_focus_ = cfg.auto_focus; + focus_ = focus; + + // Derive framepack_offset from the timing spec for FramePacked modes. + const bool is_framepack = IsFramePackedMode(mode_); + const FramePackTimingSpec* fp_spec = is_framepack ? GetFramePackTimingSpec(mode_) : nullptr; + framepack_offset_ = fp_spec ? fp_spec->gap_pixels : 0; + + // For FramePacked modes, attempt to switch the display to the custom timing. + // This is non-fatal — if the modeset fails, we still create the window and + // render TaB at the current desktop resolution (just without the HDMI 3D + // InfoFrame that triggers the TV's frame-packing decoder). + if (is_framepack && fp_spec) { + if (timing_helper_.Apply(*fp_spec, cfg.display_index)) { + LOG() << "WindowPresenter: display timing applied via " + << (timing_helper_.GetBackend() == DisplayTimingHelper::Backend::Nvidia ? "NVIDIA" : + timing_helper_.GetBackend() == DisplayTimingHelper::Backend::Amd ? "AMD" : + timing_helper_.GetBackend() == DisplayTimingHelper::Backend::CruFallback ? "CRU" : "???"); + } else { + LOG() << "WindowPresenter: display timing apply FAILED — rendering TaB at desktop res"; + } + } + + // Resolve target monitor(s) — reuses cfg.display_index. The dual-display + // modes ask for a contiguous-right secondary so the window spans both. + platform::MonitorInfo primary{}, secondary{}; + if (!platform::ResolveTargetMonitors(cfg.display_index, spans_two_monitors_, primary, secondary)) { + LOG() << "WindowPresenter::Init: ResolveTargetMonitors failed"; + timing_helper_.Revert(); + return false; + } + + // For FramePacked modes where the timing was successfully applied, override + // the monitor info to match the frame-pack active resolution so the window + // and swapchain are created at the correct size (e.g. 1920x2205). + if (is_framepack && timing_helper_.IsActive() && fp_spec) { + primary.width = static_cast(fp_spec->active_w); + primary.height = static_cast(fp_spec->active_h); + LOG() << "WindowPresenter: overriding monitor dimensions to " + << primary.width << "x" << primary.height << " for frame packing"; + } + + // Win32 windows are tied to their creating thread — only that thread can + // pump them, and Windows tears them down if their owner thread doesn't + // service messages. vrserver.exe's activation thread doesn't pump, so we + // spin a dedicated thread here that creates the window, builds the + // swapchain, runs the message loop, and stays alive for the window's life. + window_stop_.store(false); + window_ready_.store(false); + window_failed_.store(false); + window_thread_ = std::thread(&WindowPresenter::WindowThreadLoop, this, &renderer, + primary, secondary); + + // Wait up to 5s for the window thread to finish setup. + for (int i = 0; i < 500; ++i) { + if (window_ready_.load() || window_failed_.load()) break; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + if (window_failed_.load() || !window_ready_.load()) { + LOG() << "WindowPresenter::Init: window thread failed to come up"; + Shutdown(); + return false; + } + + LOG() << "WindowPresenter: window " << (window_ ? window_->Width() : 0) << "x" + << (window_ ? window_->Height() : 0) + << " on display_index=" << cfg.display_index + << " spans_two=" << (spans_two_monitors_ ? "yes" : "no") + << " mode=" << OutputModeToString(mode_); + + // Kick focus thread (z-order asserts; window thread already pumps messages). + focus_stop_.store(false); + focus_thread_ = std::thread(&WindowPresenter::FocusThreadLoop, this); + + LOG() << "WindowPresenter: window thread up; focus thread running"; + return true; +} + + +void WindowPresenter::WindowThreadLoop(Dx11Renderer* renderer, + platform::MonitorInfo primary, + platform::MonitorInfo secondary) +{ + platform::EnablePerMonitorV2DpiAwareness(); + + window_ = platform::CreatePresentWindow( + primary, + (secondary.width > 0 ? &secondary : nullptr), + "VRto3D"); + if (!window_) { + LOG() << "WindowThreadLoop: CreatePresentWindow failed"; + window_failed_.store(true); + return; + } + + if (!CreateShaders() || !CreateSwapChain(*renderer)) { + LOG() << "WindowThreadLoop: shader/swapchain init failed"; + window_failed_.store(true); + window_.reset(); + return; + } + + // Initial black frame so DWM composites something for our HWND. + { + ID3D11DeviceContext* ctx = renderer->Context(); + const float clear[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; + ctx->ClearRenderTargetView(swapchain_rtv_.Get(), clear); + ID3D11RenderTargetView* null_rtv[] = { nullptr }; + ctx->OMSetRenderTargets(1, null_rtv, nullptr); + swapchain_rtv_.Reset(); // release ref to back buffer before flip + swapchain_->Present(0, 0); + } + // Window starts non-topmost; FocusThreadLoop asserts topmost when + // is_on_top_ / man_on_top_ / auto_focus path triggers. + window_ready_.store(true); + + // For frame-packed modes started without a tracked app, take the + // foreground ourselves so Alt+F4 reaches our WndProc (which then + // tears the modeset down via timing_helper_.Revert()). Other modes + // are typically driven by an app that needs the foreground, so we + // only do this for the framepack case. + const bool is_framepack = IsFramePackedMode(mode_); + const uint32_t startup_pid = focus_.app_pid ? focus_.app_pid->load() : 0; + if (is_framepack && startup_pid == 0) { + HWND self_hwnd = static_cast(window_->NativeHandle()); + if (self_hwnd) { + ForceFocus(self_hwnd, + GetCurrentThreadId(), + GetWindowThreadProcessId(self_hwnd, nullptr)); + LOG() << "WindowPresenter: forced focus to our window for " + "standalone frame-pack mode (Alt+F4 will quit)"; + } + } + + // This thread owns the window message pump AND drives rendering. Win32 + // requires the same thread that created the window to service its message + // queue, and DXGI flip-model Present must be issued by the thread that + // created the swap chain. + while (!window_stop_.load(std::memory_order_relaxed)) { + // DWM signals frame_latency_wait_ once per display refresh; this is + // what gates pacing now (no vsync block inside Present). 100 ms cap + // so we still pump messages if DWM is paused (occlusion / lock + // screen) — alertable so Shutdown's thread-stop wakes us promptly. + if (frame_latency_wait_) { + WaitForSingleObjectEx(frame_latency_wait_, 100, TRUE); + } + renderer->WaitAndDrawPending(33); + + // Pump window messages so the window stays responsive even if the + // compositor stops submitting frames. + if (window_) window_->PollEvents(); + } + + // Tear down on this thread (DestroyWindow must be called on the creating thread). + if (frame_latency_wait_) { + CloseHandle(frame_latency_wait_); + frame_latency_wait_ = nullptr; + } + swapchain_rtv_.Reset(); + swapchain2_.Reset(); + swapchain_.Reset(); + rasterizer_.Reset(); + blend_.Reset(); + cb_.Reset(); + sampler_.Reset(); + ps_.Reset(); + vs_.Reset(); + window_.reset(); + LOG() << "WindowThreadLoop: exited"; +} + + +void WindowPresenter::RecordComposite(ID3D11Texture2D* sbs_input) +{ + if (!swapchain_ || !sbs_input) { + static std::atomic logged_null{false}; + bool expected = false; + if (logged_null.compare_exchange_strong(expected, true)) { + LOG() << "RecordComposite: early return swapchain=" << (void*)swapchain_.Get() + << " sbs_input=" << (void*)sbs_input; + } + return; + } + + ID3D11Device* dev = renderer_->Device(); + ID3D11DeviceContext* ctx = renderer_->Context(); + + // FLIP_DISCARD rotates buffer 0 every Present, so refresh the RTV each + // frame against the current back buffer. + ComPtr bb; + if (FAILED(swapchain_->GetBuffer(0, IID_PPV_ARGS(&bb))) || !bb) return; + swapchain_rtv_.Reset(); + if (FAILED(dev->CreateRenderTargetView(bb.Get(), nullptr, &swapchain_rtv_))) return; + + ComPtr srv; + D3D11_SHADER_RESOURCE_VIEW_DESC sv{}; + D3D11_TEXTURE2D_DESC td{}; + sbs_input->GetDesc(&td); + sv.Format = td.Format; + sv.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + sv.Texture2D.MipLevels = static_cast(-1); // all mips + sv.Texture2D.MostDetailedMip = 0; + HRESULT srv_hr = dev->CreateShaderResourceView(sbs_input, &sv, &srv); + if (FAILED(srv_hr)) { + static std::atomic logged_srv{false}; + bool expected = false; + if (logged_srv.compare_exchange_strong(expected, true)) { + LOG() << "RecordComposite: CreateShaderResourceView failed hr=0x" + << std::hex << srv_hr + << " fmt=" << std::dec << td.Format + << " bind=0x" << std::hex << td.BindFlags; + } + return; + } + + // Update constant buffer. + D3D11_MAPPED_SUBRESOURCE mapped{}; + if (SUCCEEDED(ctx->Map(cb_.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) { + CBParams p{}; + p.mode = ModeToShaderEnum(mode_); + p.framepack_offset = framepack_offset_; + // Poll eye_swap from the live config so the OSD's "Swap Eyes" + // checkbox takes effect immediately (no presenter restart). + bool live_swap = eye_swap_; + if (renderer_ && renderer_->Component()) { + live_swap = renderer_->Component()->GetConfig().eye_swap; + } + p.eye_swap = live_swap ? 1u : 0u; + p.out_width = static_cast(swap_width_); + p.out_height = static_cast(swap_height_); + p.aspect_ratio = aspect_ratio_; + std::memcpy(mapped.pData, &p, sizeof(p)); + ctx->Unmap(cb_.Get(), 0); + } + + D3D11_VIEWPORT vp{}; + vp.Width = static_cast(swap_width_); + vp.Height = static_cast(swap_height_); + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + + ID3D11RenderTargetView* rtv = swapchain_rtv_.Get(); + const float clear[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; + ctx->ClearRenderTargetView(rtv, clear); + + ctx->OMSetRenderTargets(1, &rtv, nullptr); + ctx->RSSetViewports(1, &vp); + ctx->RSSetState(rasterizer_.Get()); + float blend_factor[4] = { 1,1,1,1 }; + ctx->OMSetBlendState(blend_.Get(), blend_factor, 0xFFFFFFFF); + ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + ctx->IASetInputLayout(nullptr); + ctx->VSSetShader(vs_.Get(), nullptr, 0); + ctx->PSSetShader(ps_.Get(), nullptr, 0); + ID3D11Buffer* cbs[] = { cb_.Get() }; + ctx->PSSetConstantBuffers(0, 1, cbs); + ID3D11ShaderResourceView* srvs[] = { srv.Get() }; + ctx->PSSetShaderResources(0, 1, srvs); + ID3D11SamplerState* samps[] = { sampler_.Get() }; + ctx->PSSetSamplers(0, 1, samps); + + ctx->Draw(3, 0); + + // Unbind SRV + RTV so the back buffer reference is released before + // Present (FLIP requires no outstanding RTV references on flip). + ID3D11ShaderResourceView* null_srv[] = { nullptr }; + ctx->PSSetShaderResources(0, 1, null_srv); + ID3D11RenderTargetView* null_rtv[] = { nullptr }; + ctx->OMSetRenderTargets(1, null_rtv, nullptr); + swapchain_rtv_.Reset(); +} + + +void WindowPresenter::Present() +{ + if (!swapchain_) return; + + // Flip-model + waitable: SyncInterval=0 queues the flip and returns + // immediately; DWM consumes it at the next vblank. Pacing is gated by + // frame_latency_wait_ on the window thread, not by Present blocking. + HRESULT hr = swapchain_->Present(0, 0); + if (FAILED(hr)) { + static std::atomic logged_present_fail{false}; + bool expected = false; + if (logged_present_fail.compare_exchange_strong(expected, true)) { + LOG() << "WindowPresenter: swapchain->Present failed hr=" << std::hex << hr; + } + } else if (hr == DXGI_STATUS_OCCLUDED) { + static std::atomic logged_occluded{false}; + bool expected = false; + if (logged_occluded.compare_exchange_strong(expected, true)) { + LOG() << "WindowPresenter: swapchain occluded (window hidden / minimized)"; + } + } +} + + +void WindowPresenter::Shutdown() +{ + if (!window_thread_.joinable() && !window_) return; // idempotent + LOG() << "WindowPresenter: Shutdown called"; + + focus_stop_.store(true); + if (focus_thread_.joinable()) focus_thread_.join(); + + // Window thread owns the window + swapchain; signal it to exit and join. + window_stop_.store(true); + if (window_thread_.joinable()) window_thread_.join(); + + // Revert display timing after the window is torn down so the desktop + // returns to its original resolution. Safe to call even if Apply was + // never called or failed. + timing_helper_.Revert(); + + renderer_ = nullptr; +} + + +void WindowPresenter::FocusThreadLoop() +{ + using namespace std::chrono_literals; + + bool was_on_top = false; + bool nudged = false; + int reassert_counter = 0; + FocusLatchState focus_latch; // auto-focus per-PID latch (focus_policy.h) + + while (!focus_stop_.load(std::memory_order_relaxed)) { + if (!window_) break; + + // Note: do NOT pump messages here — the window's message queue must + // be serviced from its creating thread (WindowThreadLoop). + + const bool is_on_top = focus_.is_on_top && focus_.is_on_top->load(); + const bool man_on_top = focus_.man_on_top && focus_.man_on_top->load(); + const uint32_t pid = focus_.app_pid ? focus_.app_pid->load() : 0; + // Live mirror — falls back to the init-time cache if the driver + // didn't plumb the pointer (older code paths / unit tests). + const bool auto_focus = focus_.auto_focus ? focus_.auto_focus->load() : auto_focus_; + + // Multi-display placement nudge, once after first show. + if (spans_two_monitors_ && !nudged) { + window_->MultiDisplayNudge(); + nudged = true; + } + + const bool app_running = platform::IsProcessRunning(pid); + + // Shared decision (see focus_policy.h) — kept identical to the Linux + // VkRenderer focus block so the two can't drift. + FocusInputs fi; + fi.is_on_top = is_on_top; + fi.man_on_top = man_on_top; + fi.auto_focus = auto_focus; + fi.app_pid = pid; + fi.app_running = app_running; + fi.force_on_top = false; // no OSD-forced-on-top signal on this path + bool set_is_on_top = false, set_man_on_top = false; + const bool want_on_top = + ComputeWantOnTop(fi, focus_latch, &set_is_on_top, &set_man_on_top); + if (set_is_on_top && focus_.is_on_top) focus_.is_on_top->store(true); + if (set_man_on_top && focus_.man_on_top) focus_.man_on_top->store(true); + + if (want_on_top != was_on_top) { + HWND vr_hwnd = static_cast(window_->NativeHandle()); + if (want_on_top) { + window_->BringToTop(); + // Make the VR window click-through so input falls through to + // the game window underneath. + if (vr_hwnd) { + LONG_PTR ex = GetWindowLongPtrW(vr_hwnd, GWL_EXSTYLE); + SetWindowLongPtrW(vr_hwnd, GWL_EXSTYLE, + ex | WS_EX_LAYERED | WS_EX_TRANSPARENT); + SetLayeredWindowAttributes(vr_hwnd, 0, 255, LWA_ALPHA); + } + // When SteamVR is launched by the app, the game window may + // not exist yet at the +8s mark, or SteamVR's bring-up + // (vrmonitor / status window) may grab foreground after + // our first ForceFocus. Run a watch loop that re-asserts + // focus whenever the foreground drifts off the game. + std::thread([pid, man_on_top = focus_.man_on_top]() { + for (int i = 0; i < 15; ++i) { + if (man_on_top && !man_on_top->load()) return; + HWND game_hwnd = GetHWNDFromPID(pid); + if (game_hwnd && GetForegroundWindow() != game_hwnd) { + ForceFocus(game_hwnd, + GetCurrentThreadId(), + GetWindowThreadProcessId(game_hwnd, nullptr)); + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + }).detach(); + } else { + window_->ReleaseTopmost(); + if (vr_hwnd) { + // WS_EX_LAYERED stays on for the window's lifetime; only + // WS_EX_TRANSPARENT toggles. Tearing down the layered + // surface on a DPI-scaled display can leave it recreated + // at virtualized dimensions. + LONG_PTR ex = GetWindowLongPtrW(vr_hwnd, GWL_EXSTYLE); + SetWindowLongPtrW(vr_hwnd, GWL_EXSTYLE, + ex & ~WS_EX_TRANSPARENT); + } + } + was_on_top = want_on_top; + reassert_counter = 0; + } + // While we want to be on top, re-assert ~once per second so other + // apps that grab HWND_TOPMOST (dialogs, dashboards) don't push us + // behind for long. + else if (want_on_top && ++reassert_counter >= 20) { + reassert_counter = 0; + window_->BringToTop(); + } + + std::this_thread::sleep_for(50ms); + } +} + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/window_presenter.h b/vrto3d/src/presenter/window_presenter.h new file mode 100644 index 00000000..2dcc6b49 --- /dev/null +++ b/vrto3d/src/presenter/window_presenter.h @@ -0,0 +1,106 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#include "platform.h" +#include "presenter/display_timing_helper.h" +#include "presenter/output_presenter.h" +#include "vrto3dlib/stereo_config.h" + + +namespace vrto3d { + +class WindowPresenter : public IOutputPresenter { +public: + WindowPresenter() = default; + ~WindowPresenter() override { Shutdown(); } + + bool Init(Dx11Renderer& renderer, + const StereoDisplayDriverConfiguration& cfg, + const FocusContext& focus) override; + + void RecordComposite(ID3D11Texture2D* sbs_input) override; + void Present() override; + + void Shutdown() override; + +private: + struct CBParams { + uint32_t mode; + uint32_t framepack_offset; + uint32_t eye_swap; + float out_width; + float out_height; + float aspect_ratio; + }; + + bool CreateShaders(); + bool CreateSwapChain(Dx11Renderer& renderer); + void FocusThreadLoop(); + void WindowThreadLoop(Dx11Renderer* renderer, + platform::MonitorInfo primary, + platform::MonitorInfo secondary); + + Dx11Renderer* renderer_ = nullptr; + OutputMode mode_ = OutputMode::SbS; + bool eye_swap_ = false; + uint32_t framepack_offset_ = 0; + float aspect_ratio_ = 1.7777f; + bool spans_two_monitors_ = false; // true for DualDisplay / DualDisplayFlip + bool auto_focus_ = true; + + FocusContext focus_{}; + std::unique_ptr window_; + Microsoft::WRL::ComPtr swapchain_; + Microsoft::WRL::ComPtr swapchain2_; + // FLIP_DISCARD rotates buffer 0 each present, so we recreate the RTV per + // frame instead of caching one at swap-chain creation. + Microsoft::WRL::ComPtr swapchain_rtv_; + // DWM-signaled handle that releases once per display refresh — gates the + // window thread's pacing without blocking inside Present. + HANDLE frame_latency_wait_ = nullptr; + uint32_t swap_width_ = 0; + uint32_t swap_height_ = 0; + + Microsoft::WRL::ComPtr vs_; + Microsoft::WRL::ComPtr ps_; + Microsoft::WRL::ComPtr sampler_; + Microsoft::WRL::ComPtr cb_; + Microsoft::WRL::ComPtr rasterizer_; + Microsoft::WRL::ComPtr blend_; + + DisplayTimingHelper timing_helper_; + + std::thread focus_thread_; + std::atomic focus_stop_{false}; + + std::thread window_thread_; + std::atomic window_stop_{false}; + std::atomic window_ready_{false}; + std::atomic window_failed_{false}; +}; + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/wwclient.h b/vrto3d/src/presenter/wwclient.h new file mode 100644 index 00000000..e3568e4c --- /dev/null +++ b/vrto3d/src/presenter/wwclient.h @@ -0,0 +1,91 @@ +/* + * This file is part of WibbleWobbleLinux. + * + * WibbleWobbleLinux is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * WibbleWobbleLinux is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with WibbleWobbleLinux. If not, see . + */ +#ifndef WWCLIENT_H +#define WWCLIENT_H + +/* libwwclient — C ABI for feeding stereo frames to wwserver. Shape mirrors + * the Windows WibbleWobbleClientCApi (Create/IsRunning/Destroy/ + * SetSourceFormat/PresentFrame) with dmabuf buffer registration replacing + * D3D shared handles. Thread-safe per-handle; PresentFrame is non-blocking + * (drops when the server is behind — the server's frame-selection policy + * decides what displays). + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define WWCLIENT_EXPORT __attribute__((visibility("default"))) + +typedef struct WWClientOpaque* WWClientHandle; + +/* Source formats — mirror ww::proto::SourceFormat / Windows WWSourceFormat. */ +#define WWSF_SINGLE 0 +#define WWSF_SIDE_BY_SIDE_HALF 1 +#define WWSF_CHECKERBOARD 2 +#define WWSF_SIDE_BY_SIDE_FULL 3 + +/* Connects to wwserver ($XDG_RUNTIME_DIR/wibblewobble-0.sock). Returns NULL + * only on allocation failure — connection is established lazily/retried, so + * a client may Create before the server is up. `source_name` is diagnostic + * (may be NULL). */ +WWCLIENT_EXPORT WWClientHandle WWClient_Create(const char* source_name); + +/* 1 when connected to a live server, else 0 (reconnects internally). */ +WWCLIENT_EXPORT int WWClient_IsRunning(WWClientHandle h); + +WWCLIENT_EXPORT void WWClient_Destroy(WWClientHandle h); + +/* Declare the source geometry/format. Must be called before RegisterBuffer; + * calling again re-handshakes (drops registered buffers). drm_fourcc is a + * DRM_FORMAT_* code; modifier a DRM_FORMAT_MOD_* value. */ +WWCLIENT_EXPORT void WWClient_SetSourceFormat(WWClientHandle h, int wwsf, + uint32_t width, uint32_t height, + uint32_t drm_fourcc, + uint64_t drm_modifier); + +/* Register a dmabuf as a reusable frame buffer (single-plane convenience). + * The fd is dup()ed; caller keeps ownership of its copy. Returns buffer + * index >= 0, or -1 on failure/limit. */ +WWCLIENT_EXPORT int WWClient_RegisterBuffer(WWClientHandle h, int dmabuf_fd, + uint32_t stride, uint32_t offset, + uint64_t size); + +/* Multi-plane variant. */ +WWCLIENT_EXPORT int WWClient_RegisterBufferPlanes(WWClientHandle h, int num_planes, + const int* dmabuf_fds, + const uint32_t* strides, + const uint32_t* offsets, + const uint64_t* sizes); + +/* Present buffer `index` as frame `frame_id` (monotonic — drives the + * server's pair-atomicity guard). Returns 1 on send, 0 when not connected. + * Do not reuse the buffer until the server releases it (poll + * WWClient_BufferBusy or just cycle >= 3 buffers). */ +WWCLIENT_EXPORT int WWClient_PresentFrame(WWClientHandle h, int buffer_index, + uint64_t frame_id); + +/* 1 while the server still holds the buffer, 0 when free/unknown. */ +WWCLIENT_EXPORT int WWClient_BufferBusy(WWClientHandle h, int buffer_index); + +#ifdef __cplusplus +} +#endif + +#endif /* WWCLIENT_H */ diff --git a/vrto3d/src/presenter/x11_modeline.cpp b/vrto3d/src/presenter/x11_modeline.cpp new file mode 100644 index 00000000..ff4f3f15 --- /dev/null +++ b/vrto3d/src/presenter/x11_modeline.cpp @@ -0,0 +1,183 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#include "presenter/x11_modeline.h" + +#include +#include + +#include +#include +#include + +#include "vrto3dlib/debug_log.hpp" + +namespace vrto3d { + +namespace { + +// Resolve the RROutput for display_index using the same 1-based semantics as +// platform::EnumerateMonitors (0 = primary). +RROutput PickOutput(Display* dpy, XRRScreenResources* res, int32_t display_index) +{ + RROutput primary = XRRGetOutputPrimary(dpy, DefaultRootWindow(dpy)); + int connected_seen = 0; + RROutput first_connected = 0; + for (int i = 0; i < res->noutput; ++i) { + XRROutputInfo* info = XRRGetOutputInfo(dpy, res, res->outputs[i]); + if (!info) + continue; + const bool connected = info->connection == RR_Connected && info->crtc != 0; + XRRFreeOutputInfo(info); + if (!connected) + continue; + ++connected_seen; + if (!first_connected) + first_connected = res->outputs[i]; + if (display_index > 0 && connected_seen == display_index) + return res->outputs[i]; + if (display_index == 0 && res->outputs[i] == primary) + return res->outputs[i]; + } + return display_index == 0 && primary ? first_connected : first_connected; +} + +} // namespace + +bool ApplyFramePackedModeX11(void* xdisplay, int32_t display_index, + const FramePackTimingSpec& spec, X11ModelineState& state) +{ + Display* dpy = static_cast(xdisplay); + if (!dpy) + return false; + + XRRScreenResources* res = XRRGetScreenResourcesCurrent(dpy, DefaultRootWindow(dpy)); + if (!res) + return false; + + RROutput output = PickOutput(dpy, res, display_index); + if (!output) { + XRRFreeScreenResources(res); + return false; + } + XRROutputInfo* out_info = XRRGetOutputInfo(dpy, res, output); + if (!out_info || out_info->crtc == 0) { + if (out_info) XRRFreeOutputInfo(out_info); + XRRFreeScreenResources(res); + return false; + } + XRRCrtcInfo* crtc_info = XRRGetCrtcInfo(dpy, res, out_info->crtc); + if (!crtc_info) { + XRRFreeOutputInfo(out_info); + XRRFreeScreenResources(res); + return false; + } + + state.output = output; + state.crtc = out_info->crtc; + state.previous_mode = crtc_info->mode; + state.prev_x = crtc_info->x; + state.prev_y = crtc_info->y; + state.prev_rotation = crtc_info->rotation; + + const unsigned long dot_clock = + (unsigned long)spec.h_total * spec.v_total * (unsigned long)(spec.refresh_hz + 0.5f); + + // Reuse an existing mode with matching geometry + clock if present + // (e.g. from a previous run that didn't clean up). + RRMode mode_id = 0; + for (int i = 0; i < res->nmode; ++i) { + const XRRModeInfo& m = res->modes[i]; + if (m.width == spec.active_w && m.height == spec.active_h && + m.hTotal == spec.h_total && m.vTotal == spec.v_total && + m.dotClock == dot_clock) { + mode_id = m.id; + break; + } + } + + if (!mode_id) { + char name[64]; + snprintf(name, sizeof(name), "VRto3D_FP_%ux%u_%.0f", spec.active_w, spec.active_h, + (double)spec.refresh_hz); + XRRModeInfo mode{}; + mode.width = spec.active_w; + mode.height = spec.active_h; + mode.dotClock = dot_clock; + mode.hSyncStart = spec.active_w + spec.h_front_porch; + mode.hSyncEnd = mode.hSyncStart + spec.h_sync_width; + mode.hTotal = spec.h_total; + mode.vSyncStart = spec.active_h + spec.v_front_porch; + mode.vSyncEnd = mode.vSyncStart + spec.v_sync_width; + mode.vTotal = spec.v_total; + mode.modeFlags = RR_HSyncPositive | RR_VSyncPositive; + mode.name = name; + mode.nameLength = (int)strlen(name); + mode_id = XRRCreateMode(dpy, DefaultRootWindow(dpy), &mode); + if (!mode_id) { + LOG() << "x11_modeline: XRRCreateMode failed (driver mode validation?)"; + XRRFreeCrtcInfo(crtc_info); + XRRFreeOutputInfo(out_info); + XRRFreeScreenResources(res); + return false; + } + state.custom_mode = mode_id; + XRRAddOutputMode(dpy, output, mode_id); + XSync(dpy, False); + } + + Status st = XRRSetCrtcConfig(dpy, res, out_info->crtc, CurrentTime, crtc_info->x, + crtc_info->y, mode_id, crtc_info->rotation, &output, 1); + XSync(dpy, False); + XRRFreeCrtcInfo(crtc_info); + XRRFreeOutputInfo(out_info); + XRRFreeScreenResources(res); + + state.active = (st == RRSetConfigSuccess); + if (state.active) { + LOG() << "x11_modeline: frame-packed mode " << spec.active_w << "x" << spec.active_h + << "@" << spec.refresh_hz << " active"; + } else { + LOG() << "x11_modeline: XRRSetCrtcConfig failed (" << (int)st + << ") — falling back to current mode. On NVIDIA, xorg.conf " + "ModeValidation overrides may be required."; + } + return state.active; +} + +void RestoreModeX11(void* xdisplay, X11ModelineState& state) +{ + Display* dpy = static_cast(xdisplay); + if (!dpy || !state.active) + return; + + XRRScreenResources* res = XRRGetScreenResourcesCurrent(dpy, DefaultRootWindow(dpy)); + if (!res) + return; + RROutput output = (RROutput)state.output; + XRRSetCrtcConfig(dpy, res, (RRCrtc)state.crtc, CurrentTime, state.prev_x, state.prev_y, + (RRMode)state.previous_mode, state.prev_rotation, &output, 1); + if (state.custom_mode) { + XRRDeleteOutputMode(dpy, output, (RRMode)state.custom_mode); + XRRDestroyMode(dpy, (RRMode)state.custom_mode); + } + XSync(dpy, False); + XRRFreeScreenResources(res); + state.active = false; + LOG() << "x11_modeline: previous mode restored"; +} + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/x11_modeline.h b/vrto3d/src/presenter/x11_modeline.h new file mode 100644 index 00000000..e7c9555c --- /dev/null +++ b/vrto3d/src/presenter/x11_modeline.h @@ -0,0 +1,54 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +// Runtime frame-packed modelines via XRandR — the Linux equivalent of the +// NVAPI/CRU custom-timing path in display_timing_helper.cpp. Only works in a +// real X11 session (XWayland exposes virtual outputs with no hardware +// modesetting); the EDID-firmware override documented in the README's Linux +// section is the fallback for Wayland or stubborn sinks. +// +// Note: this sets the raw 1280x1470/1920x2205 timing. Like CRU-without-3D-flag +// on Windows, no HDMI 3D InfoFrame is emitted (not controllable from +// userspace) — most frame-packed TVs sync to the timing or offer a manual 3D +// mode selection. + +#include + +#include "vrto3dlib/stereo_config.h" + +namespace vrto3d { + +struct X11ModelineState { + unsigned long output = 0; // RROutput + unsigned long crtc = 0; // RRCrtc + unsigned long previous_mode = 0; // RRMode active before we switched + unsigned long custom_mode = 0; // RRMode we created (0 if reused existing) + int prev_x = 0, prev_y = 0; + unsigned short prev_rotation = 1; // RR_Rotate_0 + bool active = false; +}; + +// Applies `spec` to the output selected by display_index (1-based, 0 = primary) +// on an already-open Display*. Saves restore state into `state`. +bool ApplyFramePackedModeX11(void* xdisplay, int32_t display_index, + const FramePackTimingSpec& spec, X11ModelineState& state); + +// Restores the pre-switch mode and deletes the custom mode if we created one. +void RestoreModeX11(void* xdisplay, X11ModelineState& state); + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/x11_presenter.cpp b/vrto3d/src/presenter/x11_presenter.cpp new file mode 100644 index 00000000..b7e28be9 --- /dev/null +++ b/vrto3d/src/presenter/x11_presenter.cpp @@ -0,0 +1,489 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#include "presenter/x11_presenter.h" +#include "presenter/x11_modeline.h" + +#include +#include + +#include +#include +#include +#include +#include + +#define VK_USE_PLATFORM_XLIB_KHR +#include +#include + +namespace vrto3d { + +namespace { + +// EWMH _NET_WM_STATE client-message actions. +constexpr long kNetWmStateRemove = 0; +constexpr long kNetWmStateAdd = 1; + +// Motif window-manager hints — decorations off. +struct MotifWmHints { + unsigned long flags; + unsigned long functions; + unsigned long decorations; + long input_mode; + unsigned long status; +}; +constexpr unsigned long kMwmHintsDecorations = 1ul << 1; + +std::once_flag g_xinit_once; + +} // namespace + + +X11Presenter::~X11Presenter() +{ + Shutdown(); +} + + +std::vector X11Presenter::EnumerateOutputs() const +{ + std::vector out; + if (!dpy_) return out; + + Display* dpy = dpy_; + Window root = DefaultRootWindow(dpy); + + XRRScreenResources* res = XRRGetScreenResourcesCurrent(dpy, root); + if (!res) return out; + + RROutput primary = XRRGetOutputPrimary(dpy, root); + + for (int i = 0; i < res->noutput; ++i) { + XRROutputInfo* oi = XRRGetOutputInfo(dpy, res, res->outputs[i]); + if (!oi) continue; + if (oi->connection == RR_Connected && oi->crtc != 0) { + XRRCrtcInfo* ci = XRRGetCrtcInfo(dpy, res, oi->crtc); + if (ci && ci->width > 0 && ci->height > 0) { + OutputGeom g; + g.x = ci->x; + g.y = ci->y; + g.width = ci->width; + g.height = ci->height; + g.name = oi->name ? std::string(oi->name, static_cast(oi->nameLen)) + : std::string(); + g.is_primary = (res->outputs[i] == primary); + g.xinerama_index = static_cast(out.size()); + + // Refresh from the CRTC's mode line: dotClock / (hTotal * vTotal). + for (int m = 0; m < res->nmode; ++m) { + const XRRModeInfo& mi = res->modes[m]; + if (mi.id == ci->mode && mi.hTotal > 0 && mi.vTotal > 0) { + g.refresh_hz = static_cast( + static_cast(mi.dotClock) + / (static_cast(mi.hTotal) * static_cast(mi.vTotal))); + break; + } + } + out.push_back(g); + } + if (ci) XRRFreeCrtcInfo(ci); + } + XRRFreeOutputInfo(oi); + } + XRRFreeScreenResources(res); + return out; +} + + +void X11Presenter::SendNetWmState(long action, unsigned long property, unsigned long property2) const +{ + if (!dpy_ || !window_) return; + Display* dpy = dpy_; + + XEvent ev{}; + ev.xclient.type = ClientMessage; + ev.xclient.window = static_cast(window_); + ev.xclient.message_type = XInternAtom(dpy, "_NET_WM_STATE", False); + ev.xclient.format = 32; + ev.xclient.data.l[0] = action; + ev.xclient.data.l[1] = static_cast(property); + ev.xclient.data.l[2] = static_cast(property2); + ev.xclient.data.l[3] = 1; // source: normal application + + XSendEvent(dpy, DefaultRootWindow(dpy), False, + SubstructureRedirectMask | SubstructureNotifyMask, &ev); +} + + +void X11Presenter::SendFullscreenMonitors(const OutputGeom& left, const OutputGeom& right) const +{ + if (!dpy_ || !window_) return; + Display* dpy = dpy_; + + // _NET_WM_FULLSCREEN_MONITORS takes Xinerama monitor indices; the + // connected-output enumeration order matches them on typical XRandR 1.5 + // servers (best effort — WMs ignore unknown indices). + XEvent ev{}; + ev.xclient.type = ClientMessage; + ev.xclient.window = static_cast(window_); + ev.xclient.message_type = XInternAtom(dpy, "_NET_WM_FULLSCREEN_MONITORS", False); + ev.xclient.format = 32; + ev.xclient.data.l[0] = left.xinerama_index; // top + ev.xclient.data.l[1] = left.xinerama_index; // bottom + ev.xclient.data.l[2] = left.xinerama_index; // left + ev.xclient.data.l[3] = right.xinerama_index; // right + ev.xclient.data.l[4] = 1; // source: normal application + + XSendEvent(dpy, DefaultRootWindow(dpy), False, + SubstructureRedirectMask | SubstructureNotifyMask, &ev); +} + + +bool X11Presenter::Init(vrto3d::vk::DeviceCtx* ctx, const StereoDisplayDriverConfiguration& cfg) +{ + ctx_ = ctx; + + // Xlib is used from the present thread while other threads may create + // their own connections — must be the first Xlib call in the process. + std::call_once(g_xinit_once, []() { XInitThreads(); }); + + dpy_ = XOpenDisplay(nullptr); + if (!dpy_) { + PresenterLog("X11Presenter: XOpenDisplay failed (DISPLAY=%s)", + getenv("DISPLAY") ? getenv("DISPLAY") : ""); + return false; + } + Display* dpy = dpy_; + + std::vector outputs = EnumerateOutputs(); + if (outputs.empty()) { + PresenterLog("X11Presenter: no connected XRandR outputs"); + Shutdown(); + return false; + } + + // 0 = primary; 1..N = connected-output enumeration order. + const OutputGeom* chosen = nullptr; + if (cfg.display_index > 0 && static_cast(cfg.display_index) <= outputs.size()) { + chosen = &outputs[static_cast(cfg.display_index - 1)]; + } else { + for (const auto& o : outputs) { + if (o.is_primary) { chosen = &o; break; } + } + if (!chosen) chosen = &outputs.front(); + } + + // DualDisplay modes span the chosen output plus its contiguous right + // neighbor with identical geometry (mirrors the Windows presenter). + const bool spans_two = (cfg.output_mode == OutputMode::DualDisplay + || cfg.output_mode == OutputMode::DualDisplayFlip); + const OutputGeom* secondary = nullptr; + if (spans_two) { + const int32_t right_x = chosen->x + static_cast(chosen->width); + for (const auto& o : outputs) { + if (&o == chosen) continue; + if (o.y == chosen->y && o.x == right_x + && o.width == chosen->width && o.height == chosen->height) { + secondary = &o; + break; + } + } + if (!secondary) { + PresenterLog("X11Presenter: DualDisplay requested but no contiguous right " + "neighbor with matching geometry — single-monitor window"); + } + } + + width_ = chosen->width + (secondary ? secondary->width : 0); + height_ = chosen->height; + + // Frame-packed output: switch the target output to the HDMI 1.4 custom + // timing at runtime (the Linux analog of the NVAPI/CRU path). Restored in + // Shutdown. Only possible in a real X session — under XWayland the mode + // set fails and we fall back to the current mode (EDID override route + // documented in the README's Linux section). + if (const FramePackTimingSpec* spec = GetFramePackTimingSpec(cfg.output_mode)) { + if (ApplyFramePackedModeX11(dpy, cfg.display_index, *spec, modeline_state_)) { + width_ = spec->active_w; + height_ = spec->active_h; + } + } + + PresenterLog("X11Presenter: output '%s' %ux%u+%d+%d @%.2fHz%s (display_index=%d)", + chosen->name.c_str(), width_, height_, chosen->x, chosen->y, + chosen->refresh_hz, secondary ? " (spanning two outputs)" : "", + cfg.display_index); + + // --- Window creation -------------------------------------------------- + Window root = DefaultRootWindow(dpy); + const int screen = DefaultScreen(dpy); + + XSetWindowAttributes attrs{}; + attrs.background_pixel = BlackPixel(dpy, screen); + attrs.border_pixel = BlackPixel(dpy, screen); + attrs.event_mask = StructureNotifyMask; + attrs.override_redirect = False; + + Window win = XCreateWindow(dpy, root, chosen->x, chosen->y, width_, height_, 0, + CopyFromParent, InputOutput, + reinterpret_cast(CopyFromParent), + CWBackPixel | CWBorderPixel | CWEventMask | CWOverrideRedirect, + &attrs); + if (!win) { + PresenterLog("X11Presenter: XCreateWindow failed"); + Shutdown(); + return false; + } + window_ = static_cast(win); + + // EWMH state BEFORE mapping — compliant WMs read the property at map time. + Atom net_wm_state = XInternAtom(dpy, "_NET_WM_STATE", False); + Atom state_fullscreen = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False); + Atom state_above = XInternAtom(dpy, "_NET_WM_STATE_ABOVE", False); + Atom states[2] = { state_fullscreen, state_above }; + XChangeProperty(dpy, win, net_wm_state, XA_ATOM, 32, PropModeReplace, + reinterpret_cast(states), 2); + + // No decorations (belt and braces alongside _NET_WM_STATE_FULLSCREEN). + Atom motif = XInternAtom(dpy, "_MOTIF_WM_HINTS", False); + MotifWmHints hints{}; + hints.flags = kMwmHintsDecorations; + hints.decorations = 0; + XChangeProperty(dpy, win, motif, motif, 32, PropModeReplace, + reinterpret_cast(&hints), 5); + + // Respect our chosen position/size (some WMs re-place fullscreen windows + // onto the "current" monitor otherwise). + XSizeHints* size_hints = XAllocSizeHints(); + if (size_hints) { + size_hints->flags = USPosition | USSize | PPosition | PSize; + size_hints->x = chosen->x; + size_hints->y = chosen->y; + size_hints->width = static_cast(width_); + size_hints->height = static_cast(height_); + XSetWMNormalHints(dpy, win, size_hints); + XFree(size_hints); + } + + XStoreName(dpy, win, "VRto3D"); + XClassHint class_hint; + char wm_name[] = "vrto3d"; + char wm_class[] = "VRto3D"; + class_hint.res_name = wm_name; + class_hint.res_class = wm_class; + XSetClassHint(dpy, win, &class_hint); + + // Non-focusable display surface (ICCCM "No Input" model: InputHint + + // input=False, WM_TAKE_FOCUS unset). The window is a pure output overlay — + // all of VRto3D's own input (OSD clicks, hotkeys) arrives globally via + // evdev, never through X11 window focus — so the WM must never park + // keyboard focus here, or the flat game underneath would be starved of + // keys. Mirrors the Wayland layer surface's keyboard_interactivity=NONE. + if (XWMHints* wm_hints = XAllocWMHints()) { + wm_hints->flags = InputHint; + wm_hints->input = False; + XSetWMHints(dpy, win, wm_hints); + XFree(wm_hints); + } + + Atom wm_delete = XInternAtom(dpy, "WM_DELETE_WINDOW", False); + wm_delete_window_ = static_cast(wm_delete); + XSetWMProtocols(dpy, win, &wm_delete, 1); + + // Click-through by default (menu closed): empty XShape input region so + // pointer events fall through to the game beneath. SetInputCapture(true) + // restores the full region when the OSD opens. + int shape_event = 0, shape_err = 0; + have_xshape_ = XShapeQueryExtension(dpy, &shape_event, &shape_err); + if (have_xshape_) { + XShapeCombineRectangles(dpy, win, ShapeInput, 0, 0, nullptr, 0, ShapeSet, Unsorted); + } + + XMapRaised(dpy, win); + + // Move first, then (re-)assert fullscreen for running WMs: moving after + // map beats WMs that snap fullscreen windows to the pointer's monitor. + XMoveResizeWindow(dpy, win, chosen->x, chosen->y, width_, height_); + SendNetWmState(kNetWmStateAdd, state_fullscreen, state_above); + if (secondary) { + SendFullscreenMonitors(*chosen, *secondary); + } + XRaiseWindow(dpy, win); + XFlush(dpy); + + // --- Vulkan surface + swapchain ---------------------------------------- + VkXlibSurfaceCreateInfoKHR sci{ VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR }; + sci.dpy = dpy; + sci.window = win; + VkResult r = vkCreateXlibSurfaceKHR(ctx_->instance, &sci, nullptr, &vk_surface_); + if (r != VK_SUCCESS) { + PresenterLog("X11Presenter: vkCreateXlibSurfaceKHR failed (r=%d)", static_cast(r)); + Shutdown(); + return false; + } + + if (!swapchain_.Create(ctx_, vk_surface_, width_, height_)) { + Shutdown(); + return false; + } + + PresenterLog("X11Presenter: up, %ux%u", swapchain_.extent.width, swapchain_.extent.height); + return true; +} + + +void X11Presenter::Shutdown() +{ + if (swapchain_.ctx) { + swapchain_.Destroy(); + } + if (vk_surface_ != VK_NULL_HANDLE && ctx_ && ctx_->instance != VK_NULL_HANDLE) { + vkDestroySurfaceKHR(ctx_->instance, vk_surface_, nullptr); + } + vk_surface_ = VK_NULL_HANDLE; + + if (dpy_) { + RestoreModeX11(dpy_, modeline_state_); + if (window_) { + XDestroyWindow(dpy_, static_cast(window_)); + window_ = 0; + } + XCloseDisplay(dpy_); + dpy_ = nullptr; + } + closed_ = false; + ctx_ = nullptr; +} + + +bool X11Presenter::PumpEvents() +{ + if (!dpy_ || closed_) return false; + Display* dpy = dpy_; + + while (XPending(dpy) > 0) { + XEvent ev; + XNextEvent(dpy, &ev); + switch (ev.type) { + case ConfigureNotify: { + const auto& c = ev.xconfigure; + if (c.window == static_cast(window_) + && c.width > 0 && c.height > 0 + && (static_cast(c.width) != width_ + || static_cast(c.height) != height_)) { + width_ = static_cast(c.width); + height_ = static_cast(c.height); + swapchain_.SetDesiredExtent(width_, height_); + } + break; + } + case ClientMessage: { + if (static_cast(ev.xclient.data.l[0]) == wm_delete_window_) { + PresenterLog("X11Presenter: WM_DELETE_WINDOW — stopping"); + closed_ = true; + } + break; + } + case DestroyNotify: { + if (ev.xdestroywindow.window == static_cast(window_)) { + closed_ = true; + } + break; + } + default: + break; + } + } + return !closed_; +} + + +// AcquireNext / Present / RenderPass / Extent / Format inherited from +// SwapchainPresenterBase (delegate to swapchain_). + + +void X11Presenter::BringToTop() +{ + if (!dpy_ || !window_) return; + SendNetWmState(kNetWmStateAdd, + XInternAtom(dpy_, "_NET_WM_STATE_ABOVE", False), 0); + XRaiseWindow(dpy_, static_cast(window_)); + XFlush(dpy_); +} + +void X11Presenter::SetAlwaysOnTop(bool on_top) +{ + if (!dpy_ || !window_) return; + Atom above = XInternAtom(dpy_, "_NET_WM_STATE_ABOVE", False); + if (on_top) { + SendNetWmState(kNetWmStateAdd, above, 0); + XRaiseWindow(dpy_, static_cast(window_)); + // Belt-and-braces for KDE: a plain XRaiseWindow from a buried, non- + // focusable background window can be swallowed by KWin's focus-stealing + // prevention, leaving the overlay stuck behind the desktop. A + // _NET_ACTIVE_WINDOW request with source=pager (2) bypasses that and + // forces the stack change; input=False means it still can't grab the + // keyboard away from the game. + Window w = static_cast(window_); + XEvent act{}; + act.xclient.type = ClientMessage; + act.xclient.window = w; + act.xclient.message_type = XInternAtom(dpy_, "_NET_ACTIVE_WINDOW", False); + act.xclient.format = 32; + act.xclient.data.l[0] = 2; // source indication: pager + act.xclient.data.l[1] = 0; // timestamp (0 = CurrentTime) + XSendEvent(dpy_, DefaultRootWindow(dpy_), False, + SubstructureRedirectMask | SubstructureNotifyMask, &act); + } else { + // Drop behind normal windows so the flat game shows through. Surface + // stays mapped/presentable — we keep rendering behind it. + SendNetWmState(kNetWmStateRemove, above, 0); + XLowerWindow(dpy_, static_cast(window_)); + } + XFlush(dpy_); +} + +void X11Presenter::SetInputCapture(bool capture) +{ + if (!dpy_ || !window_ || !have_xshape_) return; + if (capture) { + // Full input region — the overlay captures the pointer (game shielded + // from OSD clicks). None mask = reset to the default full-window shape. + XShapeCombineMask(dpy_, static_cast(window_), ShapeInput, 0, 0, None, ShapeSet); + // Take keyboard so OSD text entry doesn't leak to the game (input=False + // hint keeps the WM from doing this automatically; force it directly). + XSetInputFocus(dpy_, static_cast(window_), RevertToPointerRoot, CurrentTime); + } else { + // Empty input region — click-through; pointer/keyboard reach the game. + XShapeCombineRectangles(dpy_, static_cast(window_), ShapeInput, 0, 0, nullptr, 0, + ShapeSet, Unsorted); + // Hand keyboard back; PointerRoot follows the pointer (over the game). + XSetInputFocus(dpy_, PointerRoot, RevertToPointerRoot, CurrentTime); + } + XFlush(dpy_); +} + + +void X11Presenter::ReleaseTopmost() +{ + if (!dpy_ || !window_) return; + SendNetWmState(kNetWmStateRemove, + XInternAtom(dpy_, "_NET_WM_STATE_ABOVE", False), 0); + XFlush(dpy_); +} + +} // namespace vrto3d diff --git a/vrto3d/src/presenter/x11_presenter.h b/vrto3d/src/presenter/x11_presenter.h new file mode 100644 index 00000000..c09b0e00 --- /dev/null +++ b/vrto3d/src/presenter/x11_presenter.h @@ -0,0 +1,87 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include +#include +#include + +#include "presenter/swapchain_presenter_base.h" +#include "presenter/vk_swapchain_util.h" +#include "presenter/x11_modeline.h" + +// Xlib types — kept out of the header so it doesn't leak X11 macros +// (None/Bool/Status...) into every includer. XID-derived handles are +// unsigned long by definition. +struct _XDisplay; + +namespace vrto3d { + +// Xlib implementation of IVkPresenter: a borderless, topmost, +// _NET_WM_STATE_FULLSCREEN window positioned on the XRandR output selected +// by cfg.display_index (1-based connected-output order; 0 = the primary +// output). DualDisplay/DualDisplayFlip span the chosen output plus its +// contiguous right neighbor when one with identical geometry exists. +class X11Presenter final : public SwapchainPresenterBase { +public: + X11Presenter() = default; + ~X11Presenter() override; + + bool Init(vrto3d::vk::DeviceCtx* ctx, const StereoDisplayDriverConfiguration& cfg) override; + void Shutdown() override; + + bool PumpEvents() override; + // AcquireNext/Present/RenderPass/Extent/Format inherited from + // SwapchainPresenterBase (delegate to swapchain_). + const char* Name() const override { return "X11Presenter"; } + + void BringToTop() override; + void ReleaseTopmost() override; + void SetAlwaysOnTop(bool on_top) override; + void SetInputCapture(bool capture) override; + +private: + struct OutputGeom { + int32_t x = 0; + int32_t y = 0; + uint32_t width = 0; + uint32_t height = 0; + float refresh_hz = 60.0f; + std::string name; + bool is_primary = false; + int32_t xinerama_index = 0; // connected-output order, 0-based + }; + + // Connected XRandR outputs in enumeration order. + std::vector EnumerateOutputs() const; + void SendNetWmState(long action, unsigned long property, unsigned long property2) const; + void SendFullscreenMonitors(const OutputGeom& left, const OutputGeom& right) const; + + // ctx_ + swapchain_ live in SwapchainPresenterBase. + _XDisplay* dpy_ = nullptr; + X11ModelineState modeline_state_; + unsigned long window_ = 0; // Window (XID) + unsigned long wm_delete_window_ = 0; // Atom + bool have_xshape_ = false; + uint32_t width_ = 0; + uint32_t height_ = 0; + bool closed_ = false; + + VkSurfaceKHR vk_surface_ = VK_NULL_HANDLE; +}; + +} // namespace vrto3d diff --git a/vrto3d/src/screenshot.cpp b/vrto3d/src/screenshot.cpp new file mode 100644 index 00000000..5ab67878 --- /dev/null +++ b/vrto3d/src/screenshot.cpp @@ -0,0 +1,317 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ + +#include "screenshot.h" + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#pragma comment(lib, "windowscodecs.lib") +#pragma comment(lib, "shell32.lib") + +#include +#include +#include + +#include "vrto3dlib/debug_log.hpp" +#include "vrto3dlib/win32_helper.hpp" // GetSteamInstallPath + +namespace vrto3d::screenshot { + +namespace { + +// Map an 8-bit DXGI format to its WIC pixel-format GUID. Returns nullptr for +// formats we don't handle (HDR, 10-bit, etc.) — caller logs and skips. +const GUID* WicGuidForDxgi(DXGI_FORMAT f) { + switch (f) { + case DXGI_FORMAT_R8G8B8A8_UNORM: + case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: + return &GUID_WICPixelFormat32bppRGBA; + case DXGI_FORMAT_B8G8R8A8_UNORM: + case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: + return &GUID_WICPixelFormat32bppBGRA; + default: + return nullptr; + } +} + +std::wstring Utf8ToWide(const std::string& s) { + if (s.empty()) return std::wstring(); + int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), nullptr, 0); + std::wstring w(n, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), w.data(), n); + return w; +} + +std::wstring SanitizeFilename(std::wstring s) { + for (auto& c : s) { + if (c < 32 || wcschr(L"\\/:*?\"<>|", c)) c = L'_'; + } + if (s.empty()) s = L"vrto3d"; + return s; +} + +void EnsureCoInitOnThread() { + static thread_local bool inited = false; + if (!inited) { + // Either apartment works for WIC; ignore RPC_E_CHANGED_MODE / S_FALSE. + CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + inited = true; + } +} + +// Build a tightly-packed (row_pitch == width*4) BGRA8 buffer from a source SBS +// image, scaled so the *per-eye* aspect ratio equals `target_eye_aspect`. Only +// enlarges: whichever dimension is "too small" gets stretched up; the other +// dimension is left at full source resolution. Returns false on failure. +// +// Output dims are written to *out_w / *out_h. The buffer layout stays SBS: +// the left half occupies cols [0, out_w/2) and the right half [out_w/2, out_w). +bool ScaleSbsToBgra(IWICImagingFactory* factory, + UINT src_w, UINT src_h, const GUID& src_format, + const BYTE* src_data, UINT src_row_pitch, + float target_eye_aspect, + std::vector& out_buf, + UINT* out_w, UINT* out_h) { + if (src_w < 2 || (src_w & 1u) != 0 || src_h == 0) return false; + + const UINT src_eye_w = src_w / 2; + const float src_eye_aspect = static_cast(src_eye_w) / static_cast(src_h); + + // Target SBS dims: enlarge the smaller axis only. + UINT dst_eye_w = src_eye_w; + UINT dst_h = src_h; + if (target_eye_aspect > 0.0f) { + if (src_eye_aspect > target_eye_aspect) { + // Source is wider than target → stretch height up. + dst_h = static_cast(std::lround(static_cast(src_eye_w) / target_eye_aspect)); + if (dst_h < src_h) dst_h = src_h; // never shrink + } else if (src_eye_aspect < target_eye_aspect) { + // Source is narrower than target → stretch per-eye width up. + dst_eye_w = static_cast(std::lround(static_cast(src_h) * target_eye_aspect)); + if (dst_eye_w < src_eye_w) dst_eye_w = src_eye_w; + } + } + const UINT dst_w = dst_eye_w * 2; + + Microsoft::WRL::ComPtr src_bmp; + HRESULT hr = factory->CreateBitmapFromMemory(src_w, src_h, src_format, + src_row_pitch, + src_row_pitch * src_h, + const_cast(src_data), + &src_bmp); + if (FAILED(hr)) return false; + + Microsoft::WRL::ComPtr source = src_bmp; + + if (!IsEqualGUID(src_format, GUID_WICPixelFormat32bppBGRA)) { + Microsoft::WRL::ComPtr conv; + if (FAILED(factory->CreateFormatConverter(&conv))) return false; + if (FAILED(conv->Initialize(source.Get(), GUID_WICPixelFormat32bppBGRA, + WICBitmapDitherTypeNone, nullptr, 0.0, + WICBitmapPaletteTypeMedianCut))) return false; + source = conv; + } + + if (dst_w != src_w || dst_h != src_h) { + Microsoft::WRL::ComPtr scaler; + if (FAILED(factory->CreateBitmapScaler(&scaler))) return false; + if (FAILED(scaler->Initialize(source.Get(), dst_w, dst_h, + WICBitmapInterpolationModeHighQualityCubic))) return false; + source = scaler; + } + + const UINT dst_pitch = dst_w * 4; + out_buf.assign(static_cast(dst_pitch) * dst_h, 0); + if (FAILED(source->CopyPixels(nullptr, dst_pitch, + static_cast(out_buf.size()), + out_buf.data()))) { + return false; + } + *out_w = dst_w; + *out_h = dst_h; + return true; +} + +bool EncodePngToFile(IWICImagingFactory* factory, + const std::wstring& path, + UINT width, UINT height, + const GUID& src_format, + const BYTE* data, UINT row_pitch) { + Microsoft::WRL::ComPtr stream; + if (FAILED(factory->CreateStream(&stream))) return false; + HRESULT hr = stream->InitializeFromFilename(path.c_str(), GENERIC_WRITE); + if (FAILED(hr)) { + LOG() << "Screenshot: open '" << path.c_str() << "' hr=0x" << std::hex << hr; + return false; + } + + Microsoft::WRL::ComPtr encoder; + if (FAILED(factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, &encoder))) return false; + if (FAILED(encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache))) return false; + + Microsoft::WRL::ComPtr frame; + Microsoft::WRL::ComPtr options; + if (FAILED(encoder->CreateNewFrame(&frame, &options))) return false; + if (FAILED(frame->Initialize(options.Get()))) return false; + if (FAILED(frame->SetSize(width, height))) return false; + + GUID requested = src_format; + if (FAILED(frame->SetPixelFormat(&requested))) return false; + + if (IsEqualGUID(requested, src_format)) { + if (FAILED(hr = frame->WritePixels(height, row_pitch, + row_pitch * height, + const_cast(data)))) { + LOG() << "Screenshot: WritePixels hr=0x" << std::hex << hr; + return false; + } + } else { + Microsoft::WRL::ComPtr bmp; + if (FAILED(factory->CreateBitmapFromMemory(width, height, src_format, + row_pitch, row_pitch * height, + const_cast(data), &bmp))) return false; + Microsoft::WRL::ComPtr conv; + if (FAILED(factory->CreateFormatConverter(&conv))) return false; + if (FAILED(conv->Initialize(bmp.Get(), requested, + WICBitmapDitherTypeNone, nullptr, 0.0, + WICBitmapPaletteTypeMedianCut))) return false; + if (FAILED(frame->WriteSource(conv.Get(), nullptr))) return false; + } + + if (FAILED(frame->Commit())) return false; + return SUCCEEDED(encoder->Commit()); +} + +// Pick the lowest 4-digit index N such that neither +// "_NNNN.png" nor "_NNNN_crossview.png" exists in `dir`. +int FindNextScreenshotIndex(const std::wstring& dir, const std::wstring& base) { + for (int i = 1; i < 10000; ++i) { + wchar_t suf[16]; + swprintf_s(suf, L"_%04d.png", i); + std::wstring p1 = dir + L"\\" + base + suf; + wchar_t xv_suf[32]; + swprintf_s(xv_suf, L"_%04d_crossview.png", i); + std::wstring p2 = dir + L"\\" + base + xv_suf; + if (GetFileAttributesW(p1.c_str()) == INVALID_FILE_ATTRIBUTES && + GetFileAttributesW(p2.c_str()) == INVALID_FILE_ATTRIBUTES) { + return i; + } + } + return -1; +} + +} // namespace + + +SaveResult SaveStereoPair(const std::string& app_name, + uint32_t sbs_width, + uint32_t sbs_height, + DXGI_FORMAT dxgi_format, + const void* data, + uint32_t row_pitch, + float target_eye_aspect) +{ + SaveResult r; + if (!data || sbs_width < 2 || (sbs_width & 1u) != 0 || sbs_height == 0) { + LOG() << "Screenshot: invalid input " << sbs_width << "x" << sbs_height; + return r; + } + const GUID* wic_fmt = WicGuidForDxgi(dxgi_format); + if (!wic_fmt) { + LOG() << "Screenshot: unsupported texture format " << dxgi_format << " — skipping"; + return r; + } + + std::string steam = GetSteamInstallPath(); + if (steam.empty()) { + LOG() << "Screenshot: Steam install path not found"; + return r; + } + std::wstring wdir = Utf8ToWide(steam) + L"\\steamapps\\common\\SteamVR\\screenshots"; + SHCreateDirectoryExW(nullptr, wdir.c_str(), nullptr); + r.dir = steam + "\\steamapps\\common\\SteamVR\\screenshots"; + + std::wstring base = SanitizeFilename(Utf8ToWide(app_name)); + int idx = FindNextScreenshotIndex(wdir, base); + if (idx < 0) { + LOG() << "Screenshot: no free index available"; + return r; + } + r.index = idx; + + EnsureCoInitOnThread(); + + Microsoft::WRL::ComPtr factory; + if (FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, + CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&factory)))) { + LOG() << "Screenshot: WIC factory create failed"; + return r; + } + + std::vector bgra_buf; + UINT out_w = 0, out_h = 0; + if (!ScaleSbsToBgra(factory.Get(), + sbs_width, sbs_height, *wic_fmt, + static_cast(data), row_pitch, + target_eye_aspect, + bgra_buf, &out_w, &out_h)) { + LOG() << "Screenshot: scale/convert failed"; + return r; + } + const UINT out_pitch = out_w * 4; + const UINT half_bytes = (out_w / 2) * 4; + r.out_width = out_w; + r.out_height = out_h; + + // Normal (parallel-view). + wchar_t suf[16]; + swprintf_s(suf, L"_%04d.png", idx); + std::wstring p_normal = wdir + L"\\" + base + suf; + bool ok1 = EncodePngToFile(factory.Get(), p_normal, out_w, out_h, + GUID_WICPixelFormat32bppBGRA, + bgra_buf.data(), out_pitch); + + // Cross-view: swap halves per row into a scratch buffer. + std::vector swapped(bgra_buf.size()); + for (UINT y = 0; y < out_h; ++y) { + const BYTE* src_row = bgra_buf.data() + y * out_pitch; + BYTE* dst_row = swapped.data() + y * out_pitch; + memcpy(dst_row, src_row + half_bytes, half_bytes); + memcpy(dst_row + half_bytes, src_row, half_bytes); + } + wchar_t xv_suf[32]; + swprintf_s(xv_suf, L"_%04d_crossview.png", idx); + std::wstring p_cross = wdir + L"\\" + base + xv_suf; + bool ok2 = EncodePngToFile(factory.Get(), p_cross, out_w, out_h, + GUID_WICPixelFormat32bppBGRA, + swapped.data(), out_pitch); + + r.ok = ok1 && ok2; + if (r.ok) { + LOG() << "Screenshot: saved index " << idx << " for '" << app_name << "' " + << sbs_width << "x" << sbs_height << " -> " << out_w << "x" << out_h + << " (eye aspect target=" << target_eye_aspect << ") to " << r.dir; + } else { + LOG() << "Screenshot: encode failed (normal=" << ok1 << " crossview=" << ok2 << ")"; + } + return r; +} + +} // namespace vrto3d::screenshot diff --git a/vrto3d/src/screenshot.h b/vrto3d/src/screenshot.h new file mode 100644 index 00000000..67b115aa --- /dev/null +++ b/vrto3d/src/screenshot.h @@ -0,0 +1,56 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include +#include + +#include + +namespace vrto3d::screenshot { + +// Result of writing a stereo screenshot pair. +struct SaveResult { + bool ok = false; // both files written successfully + int index = -1; // selected counter (filename suffix) + std::string dir; // destination directory (UTF-8) + uint32_t out_width = 0; // final SBS width after aspect-ratio scaling + uint32_t out_height = 0; // final SBS height after aspect-ratio scaling +}; + +// Save a stereo SBS screenshot pair to \steamapps\common\SteamVR\screenshots. +// +// `data`/`row_pitch` describe the raw mapped staging texture. `dxgi_format` +// must be one of DXGI_FORMAT_{R,B}8G8B8A8_UNORM[_SRGB] — others are rejected. +// +// `target_eye_aspect` (>0) corrects the per-eye aspect by *enlarging* the +// smaller axis only. Pass 0 to skip scaling. +// +// Two files are written next to each other: +// _NNNN.png (parallel-view, as-is) +// _NNNN_crossview.png (eyes swapped) +// NNNN starts at 0001 and increments until both filenames are free, so +// existing screenshots are never overwritten. +SaveResult SaveStereoPair(const std::string& app_name, + uint32_t sbs_width, + uint32_t sbs_height, + DXGI_FORMAT dxgi_format, + const void* data, + uint32_t row_pitch, + float target_eye_aspect); + +} // namespace vrto3d::screenshot diff --git a/vrto3d/src/vk/direct_mode_component_vk.cpp b/vrto3d/src/vk/direct_mode_component_vk.cpp new file mode 100644 index 00000000..7ad56472 --- /dev/null +++ b/vrto3d/src/vk/direct_mode_component_vk.cpp @@ -0,0 +1,352 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#include "vk/direct_mode_component_vk.h" + +#include +#include + +#include "vk/vk_renderer.h" +#include "vrto3dlib/debug_log.hpp" + +namespace { + +// The IPC resource manager initializes asynchronously inside vrserver — it is +// typically NULL during IServerTrackedDeviceProvider::Init and ready by the +// first CreateSwapTextureSet. Always fetch it fresh. +vr::IVRIPCResourceManagerClient* ResourceManager() +{ + return vr::VRIPCResourceManager(); +} + +// The whole present chain is byte-preserving in gamma space, matching the +// Windows D3D11 path (raw copies, raw sampling, anaglyph matrices designed +// for gamma-encoded values). Importing the compositor's sRGB images with the +// UNORM twin format keeps Vulkan from silently linearizing at the blit. +VkFormat RawTwin(uint32_t vk_format) +{ + switch ((VkFormat)vk_format) { + case VK_FORMAT_R8G8B8A8_SRGB: return VK_FORMAT_R8G8B8A8_UNORM; + case VK_FORMAT_B8G8R8A8_SRGB: return VK_FORMAT_B8G8R8A8_UNORM; + default: return (VkFormat)vk_format; + } +} + +} // namespace + +DirectModeComponentVk::DirectModeComponentVk(VkRenderer* renderer) + : renderer_(renderer) +{ +} + +DirectModeComponentVk::~DirectModeComponentVk() +{ + std::lock_guard lock(mutex_); + for (auto& [handle, tex] : textures_) + ReleaseTexture(tex); + textures_.clear(); +} + +void DirectModeComponentVk::CreateSwapTextureSet(uint32_t unPid, + const SwapTextureSetDesc_t* desc, + SwapTextureSet_t* out) +{ + auto* rm = ResourceManager(); + if (!rm) { + LOG() << "direct_mode_vk: VRIPCResourceManager unavailable in CreateSwapTextureSet"; + return; + } + if (renderer_ && renderer_->IsDeviceDead()) + return; + + LOG() << "direct_mode_vk: CreateSwapTextureSet pid=" << unPid << " " << desc->nWidth + << "x" << desc->nHeight << " fmt=" << desc->nFormat + << " samples=" << desc->nSampleCount; + + vr::SharedTextureHandle_t handles[3] = {}; + for (int i = 0; i < 3; ++i) { + // nFormat arrives as a VkFormat on Linux (M0: 43 = R8G8B8A8_SRGB). + if (!rm->NewSharedVulkanImage(desc->nFormat, desc->nWidth, desc->nHeight, + /*bRenderable=*/true, /*bMappable=*/false, + /*bComputeAccess=*/true, /*unMipLevels=*/1, + /*unArrayLayerCount=*/1, 0, 0, &handles[i])) { + LOG() << "direct_mode_vk: NewSharedVulkanImage failed (i=" << i << ")"; + for (int j = 0; j < i; ++j) + rm->UnrefResource(handles[j]); + return; + } + } + + { + std::lock_guard lock(mutex_); + for (int i = 0; i < 3; ++i) { + if (!ImportHandle(handles[i], unPid, desc->nWidth, desc->nHeight, desc->nFormat, + handles[0])) { + LOG() << "direct_mode_vk: dmabuf import failed for handle 0x" << std::hex + << handles[i] << std::dec << " — set unusable"; + } + } + } + + for (int i = 0; i < 3; ++i) + out->rSharedTextureHandles[i] = handles[i]; + out->unTextureFlags = 0; +} + +DirectModeComponentVk::ImportedTexture* DirectModeComponentVk::ImportHandle( + vr::SharedTextureHandle_t handle, uint32_t pid, uint32_t width, uint32_t height, + uint32_t vk_format, vr::SharedTextureHandle_t set_key) +{ + auto* rm = ResourceManager(); + auto& ctx = renderer_->Ctx(); + if (!rm || ctx.device == VK_NULL_HANDLE) + return nullptr; + + uint64_t ipc_handle = 0; + if (!rm->RefResource(handle, &ipc_handle)) { + LOG() << "direct_mode_vk: RefResource failed"; + return nullptr; + } + int fd = -1; + if (!rm->ReceiveSharedFd(ipc_handle, &fd) || fd < 0) { + LOG() << "direct_mode_vk: ReceiveSharedFd failed"; + rm->UnrefResource(handle); + return nullptr; + } + + // Same-driver dmabuf import: the image was allocated by vrserver's Vulkan + // device on the same GPU, so OPTIMAL tiling round-trips correctly on RADV. + // If a driver ever mismatches layouts here the + // symptom is garbled sampling — the fallback would be + // VK_EXT_image_drm_format_modifier with explicit modifiers. + VkExternalMemoryImageCreateInfo ext_img{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO}; + ext_img.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; + + VkImageCreateInfo ici{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + ici.pNext = &ext_img; + ici.imageType = VK_IMAGE_TYPE_2D; + ici.format = RawTwin(vk_format); + ici.extent = {width, height, 1}; + ici.mipLevels = 1; + ici.arrayLayers = 1; + ici.samples = VK_SAMPLE_COUNT_1_BIT; + ici.tiling = VK_IMAGE_TILING_OPTIMAL; + ici.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + ici.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + ici.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + VkImage image = VK_NULL_HANDLE; + if (vrto3d::vk::LogIfFailed(vkCreateImage(ctx.device, &ici, nullptr, &image), + "import vkCreateImage") != VK_SUCCESS) { + close(fd); + rm->UnrefResource(handle); + return nullptr; + } + + VkMemoryRequirements reqs{}; + vkGetImageMemoryRequirements(ctx.device, image, &reqs); + + PFN_vkGetMemoryFdPropertiesKHR get_fd_props = + (PFN_vkGetMemoryFdPropertiesKHR)vkGetDeviceProcAddr(ctx.device, + "vkGetMemoryFdPropertiesKHR"); + VkMemoryFdPropertiesKHR fd_props{VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR}; + if (get_fd_props) + get_fd_props(ctx.device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, fd, &fd_props); + + uint32_t type_bits = reqs.memoryTypeBits & (fd_props.memoryTypeBits ? fd_props.memoryTypeBits + : reqs.memoryTypeBits); + uint32_t mem_type = ctx.FindMemoryType(type_bits, 0); + if (mem_type == UINT32_MAX) { + LOG() << "direct_mode_vk: no compatible memory type for dmabuf import"; + vkDestroyImage(ctx.device, image, nullptr); + close(fd); + rm->UnrefResource(handle); + return nullptr; + } + + VkMemoryDedicatedAllocateInfo dedicated{VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO}; + dedicated.image = image; + VkImportMemoryFdInfoKHR import_info{VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR}; + import_info.pNext = &dedicated; + import_info.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; + import_info.fd = fd; // ownership transfers to Vulkan on success + + VkMemoryAllocateInfo alloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + alloc.pNext = &import_info; + alloc.allocationSize = reqs.size; + alloc.memoryTypeIndex = mem_type; + + VkDeviceMemory memory = VK_NULL_HANDLE; + if (vrto3d::vk::LogIfFailed(vkAllocateMemory(ctx.device, &alloc, nullptr, &memory), + "import vkAllocateMemory") != VK_SUCCESS) { + vkDestroyImage(ctx.device, image, nullptr); + close(fd); + rm->UnrefResource(handle); + return nullptr; + } + if (vrto3d::vk::LogIfFailed(vkBindImageMemory(ctx.device, image, memory, 0), + "import vkBindImageMemory") != VK_SUCCESS) { + vkFreeMemory(ctx.device, memory, nullptr); + vkDestroyImage(ctx.device, image, nullptr); + rm->UnrefResource(handle); + return nullptr; + } + + VkImageViewCreateInfo vci{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + vci.image = image; + vci.viewType = VK_IMAGE_VIEW_TYPE_2D; + vci.format = RawTwin(vk_format); + vci.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageView view = VK_NULL_HANDLE; + if (vrto3d::vk::LogIfFailed(vkCreateImageView(ctx.device, &vci, nullptr, &view), + "import vkCreateImageView") != VK_SUCCESS) { + vkFreeMemory(ctx.device, memory, nullptr); + vkDestroyImage(ctx.device, image, nullptr); + rm->UnrefResource(handle); + return nullptr; + } + + ImportedTexture tex; + tex.handle = handle; + tex.image = image; + tex.memory = memory; + tex.view = view; + tex.width = width; + tex.height = height; + tex.format = RawTwin(vk_format); + tex.pid = pid; + tex.set_key = set_key; + auto [it, inserted] = textures_.insert_or_assign(handle, tex); + return &it->second; +} + +void DirectModeComponentVk::ReleaseTexture(ImportedTexture& tex) +{ + auto& ctx = renderer_->Ctx(); + if (ctx.device != VK_NULL_HANDLE) { + if (tex.view) + vkDestroyImageView(ctx.device, tex.view, nullptr); + if (tex.image) + vkDestroyImage(ctx.device, tex.image, nullptr); + if (tex.memory) + vkFreeMemory(ctx.device, tex.memory, nullptr); + } + if (auto* rm = ResourceManager()) + rm->UnrefResource(tex.handle); + tex.view = VK_NULL_HANDLE; + tex.image = VK_NULL_HANDLE; + tex.memory = VK_NULL_HANDLE; +} + +void DirectModeComponentVk::DestroySwapTextureSet(vr::SharedTextureHandle_t sharedTextureHandle) +{ + // The renderer may still have a submit in flight sampling these images. + renderer_->WaitIdleForTextureRelease(); + + std::lock_guard lock(mutex_); + auto it = textures_.find(sharedTextureHandle); + if (it == textures_.end()) + return; + const vr::SharedTextureHandle_t set_key = it->second.set_key; + for (auto iter = textures_.begin(); iter != textures_.end();) { + if (iter->second.set_key == set_key) { + ReleaseTexture(iter->second); + iter = textures_.erase(iter); + } else { + ++iter; + } + } +} + +void DirectModeComponentVk::DestroyAllSwapTextureSets(uint32_t unPid) +{ + LOG() << "direct_mode_vk: DestroyAllSwapTextureSets pid=" << unPid; + renderer_->WaitIdleForTextureRelease(); + + std::lock_guard lock(mutex_); + for (auto iter = textures_.begin(); iter != textures_.end();) { + if (iter->second.pid == unPid) { + ReleaseTexture(iter->second); + iter = textures_.erase(iter); + } else { + ++iter; + } + } +} + +void DirectModeComponentVk::GetNextSwapTextureSetIndex( + vr::SharedTextureHandle_t sharedTextureHandles[2], uint32_t (*pIndices)[2]) +{ + std::lock_guard lock(mutex_); + for (int eye = 0; eye < 2; ++eye) { + uint32_t index = 0; + auto it = textures_.find(sharedTextureHandles[eye]); + if (it != textures_.end()) { + auto set_it = textures_.find(it->second.set_key); + if (set_it != textures_.end()) { + set_it->second.set_next_index = (set_it->second.set_next_index + 1) % 3; + index = set_it->second.set_next_index; + } + } + (*pIndices)[eye] = index; + } +} + +void DirectModeComponentVk::SubmitLayer(const SubmitLayerPerEye_t (&perEye)[2]) +{ + // Single-layer model on Linux: keep only the most recent pair. (If the + // compositor ever submits multiple layers, the last one wins — log once.) + ++submit_count_; + if (have_layer_ && submit_count_ <= 3) + LOG() << "direct_mode_vk: multiple SubmitLayer calls per Present — unexpected on Linux"; + last_layer_[0] = perEye[0]; + last_layer_[1] = perEye[1]; + have_layer_ = true; +} + +void DirectModeComponentVk::Present(vr::SharedTextureHandle_t syncTexture) +{ + (void)syncTexture; // 0 on Linux (M0); sync is implicit via dmabuf + ++present_count_; + if (!have_layer_ || !renderer_) + return; + have_layer_ = false; + + VkRenderer::EyeLayer eyes[2]; + { + std::lock_guard lock(mutex_); + for (int i = 0; i < 2; ++i) { + auto it = textures_.find(last_layer_[i].hTexture); + if (it == textures_.end() || it->second.image == VK_NULL_HANDLE) { + if (present_count_ <= 5 || present_count_ % 600 == 0) + LOG() << "direct_mode_vk: Present with unknown texture handle 0x" << std::hex + << last_layer_[i].hTexture << std::dec; + return; + } + eyes[i].image = it->second.image; + eyes[i].view = it->second.view; + eyes[i].width = it->second.width; + eyes[i].height = it->second.height; + eyes[i].bounds = last_layer_[i].bounds; + } + } + renderer_->OnDirectModeFrame(eyes[0], eyes[1]); +} + +void DirectModeComponentVk::PostPresent(const Throttling_t* pThrottling) +{ + (void)pThrottling; +} diff --git a/vrto3d/src/vk/direct_mode_component_vk.h b/vrto3d/src/vk/direct_mode_component_vk.h new file mode 100644 index 00000000..8aafc9b4 --- /dev/null +++ b/vrto3d/src/vk/direct_mode_component_vk.h @@ -0,0 +1,85 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +// Linux IVRDriverDirectModeComponent. Swap textures are allocated centrally +// by vrserver via IVRIPCResourceManagerClient::NewSharedVulkanImage; the +// compositor renders into them and we import each one once as a dmabuf +// VkImage for sampling. On Linux the compositor pre-composites all layers +// and submits exactly one layer pair per frame from its own pid, so +// SubmitLayer/Present are much simpler than the Windows path — no +// multi-layer accumulation or pid sorting. + +#include +#include +#include + +#include + +#include "openvr_driver.h" + +class VkRenderer; + +class DirectModeComponentVk : public vr::IVRDriverDirectModeComponent +{ +public: + explicit DirectModeComponentVk(VkRenderer* renderer); + ~DirectModeComponentVk(); + + void CreateSwapTextureSet(uint32_t unPid, const SwapTextureSetDesc_t* pSwapTextureSetDesc, + SwapTextureSet_t* pOutSwapTextureSet) override; + void DestroySwapTextureSet(vr::SharedTextureHandle_t sharedTextureHandle) override; + void DestroyAllSwapTextureSets(uint32_t unPid) override; + void GetNextSwapTextureSetIndex(vr::SharedTextureHandle_t sharedTextureHandles[2], + uint32_t (*pIndices)[2]) override; + void SubmitLayer(const SubmitLayerPerEye_t (&perEye)[2]) override; + void Present(vr::SharedTextureHandle_t syncTexture) override; + void PostPresent(const Throttling_t* pThrottling) override; + +private: + struct ImportedTexture { + vr::SharedTextureHandle_t handle = 0; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + uint32_t width = 0; + uint32_t height = 0; + VkFormat format = VK_FORMAT_UNDEFINED; + uint32_t pid = 0; + vr::SharedTextureHandle_t set_key = 0; // first handle of the set + uint32_t set_next_index = 0; // valid on the set_key entry + }; + + // Import `handle` as a dmabuf-backed VkImage (RefResource -> + // ReceiveSharedFd -> vkCreateImage+import+bind+view). Returns nullptr on + // failure (entry not added). + ImportedTexture* ImportHandle(vr::SharedTextureHandle_t handle, uint32_t pid, + uint32_t width, uint32_t height, uint32_t vk_format, + vr::SharedTextureHandle_t set_key); + void ReleaseTexture(ImportedTexture& tex); + + VkRenderer* renderer_ = nullptr; + + std::mutex mutex_; + std::map textures_; + + // Present-time diagnostics (rate-limited logging). + uint64_t submit_count_ = 0; + uint64_t present_count_ = 0; + SubmitLayerPerEye_t last_layer_[2] = {}; + bool have_layer_ = false; +}; diff --git a/vrto3d/src/vk/vk_context.cpp b/vrto3d/src/vk/vk_context.cpp new file mode 100644 index 00000000..4879975b --- /dev/null +++ b/vrto3d/src/vk/vk_context.cpp @@ -0,0 +1,263 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#include "vk/vk_context.h" + +#include +#include + +#include "vrto3dlib/debug_log.hpp" + +namespace vrto3d::vk { + +VkResult LogIfFailed(VkResult r, const char* what) +{ + if (r != VK_SUCCESS) + LOG() << "vk: " << what << " failed (" << (int)r << ")"; + return r; +} + +bool DeviceCtx::Init() +{ + // ---- instance ---- + VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + app.pApplicationName = "vrto3d"; + app.apiVersion = VK_API_VERSION_1_1; + + uint32_t avail_count = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &avail_count, nullptr); + std::vector avail(avail_count); + vkEnumerateInstanceExtensionProperties(nullptr, &avail_count, avail.data()); + auto instance_ext_supported = [&](const char* name) { + for (const auto& e : avail) + if (strcmp(e.extensionName, name) == 0) return true; + return false; + }; + + std::vector inst_exts = {VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME}; + for (const char* wsi : {"VK_KHR_surface", "VK_KHR_wayland_surface", + "VK_KHR_xcb_surface", "VK_KHR_xlib_surface"}) { + if (instance_ext_supported(wsi)) + inst_exts.push_back(wsi); + } + + VkInstanceCreateInfo ici{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + ici.pApplicationInfo = &app; + ici.enabledExtensionCount = (uint32_t)inst_exts.size(); + ici.ppEnabledExtensionNames = inst_exts.data(); + if (LogIfFailed(vkCreateInstance(&ici, nullptr, &instance), "vkCreateInstance") != VK_SUCCESS) + return false; + + // ---- physical device ---- + uint32_t dev_count = 0; + vkEnumeratePhysicalDevices(instance, &dev_count, nullptr); + if (dev_count == 0) { + LOG() << "vk: no physical devices"; + return false; + } + std::vector devs(dev_count); + vkEnumeratePhysicalDevices(instance, &dev_count, devs.data()); + + static const char* kRequiredDevExts[] = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME, + VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME, + VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME, + VK_EXT_IMAGE_DRM_FORMAT_MODIFIER_EXTENSION_NAME, + VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME, + VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME, + }; + + for (VkPhysicalDevice candidate : devs) { + uint32_t ext_count = 0; + vkEnumerateDeviceExtensionProperties(candidate, nullptr, &ext_count, nullptr); + std::vector exts(ext_count); + vkEnumerateDeviceExtensionProperties(candidate, nullptr, &ext_count, exts.data()); + bool all = true; + for (const char* need : kRequiredDevExts) { + bool found = false; + for (const auto& e : exts) + if (strcmp(e.extensionName, need) == 0) { found = true; break; } + if (!found) { all = false; break; } + } + if (!all) + continue; + + uint32_t qf_count = 0; + vkGetPhysicalDeviceQueueFamilyProperties(candidate, &qf_count, nullptr); + std::vector qfs(qf_count); + vkGetPhysicalDeviceQueueFamilyProperties(candidate, &qf_count, qfs.data()); + for (uint32_t i = 0; i < qf_count; ++i) { + if (qfs[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { + phys = candidate; + queue_family = i; + break; + } + } + if (phys != VK_NULL_HANDLE) + break; + } + if (phys == VK_NULL_HANDLE) { + LOG() << "vk: no physical device with required extensions"; + return false; + } + + VkPhysicalDeviceProperties props; + vkGetPhysicalDeviceProperties(phys, &props); + LOG() << "vk: using " << props.deviceName; + vkGetPhysicalDeviceMemoryProperties(phys, &mem_props); + + // ---- device ---- + float prio = 1.0f; + VkDeviceQueueCreateInfo qci{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + qci.queueFamilyIndex = queue_family; + qci.queueCount = 1; + qci.pQueuePriorities = &prio; + + VkPhysicalDeviceTimelineSemaphoreFeatures timeline{ + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES}; + timeline.timelineSemaphore = VK_TRUE; + + VkDeviceCreateInfo dci{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + dci.pNext = &timeline; + dci.queueCreateInfoCount = 1; + dci.pQueueCreateInfos = &qci; + dci.enabledExtensionCount = (uint32_t)(sizeof(kRequiredDevExts) / sizeof(kRequiredDevExts[0])); + dci.ppEnabledExtensionNames = kRequiredDevExts; + if (LogIfFailed(vkCreateDevice(phys, &dci, nullptr, &device), "vkCreateDevice") != VK_SUCCESS) + return false; + + vkGetDeviceQueue(device, queue_family, 0, &queue); + return true; +} + +void DeviceCtx::Destroy() +{ + if (device) { + vkDeviceWaitIdle(device); + vkDestroyDevice(device, nullptr); + device = VK_NULL_HANDLE; + } + if (instance) { + vkDestroyInstance(instance, nullptr); + instance = VK_NULL_HANDLE; + } + phys = VK_NULL_HANDLE; + queue = VK_NULL_HANDLE; +} + +uint32_t DeviceCtx::FindMemoryType(uint32_t type_bits, VkMemoryPropertyFlags want) const +{ + for (uint32_t i = 0; i < mem_props.memoryTypeCount; ++i) { + if ((type_bits & (1u << i)) && + (mem_props.memoryTypes[i].propertyFlags & want) == want) + return i; + } + return UINT32_MAX; +} + +void Image2D::Destroy(VkDevice device) +{ + if (view) { vkDestroyImageView(device, view, nullptr); view = VK_NULL_HANDLE; } + if (image) { vkDestroyImage(device, image, nullptr); image = VK_NULL_HANDLE; } + if (memory) { vkFreeMemory(device, memory, nullptr); memory = VK_NULL_HANDLE; } +} + +bool AllocateBindImageView(DeviceCtx& ctx, VkImage image, VkFormat view_format, + VkMemoryPropertyFlags mem_props, const void* alloc_pnext, + uint32_t extra_type_bits, bool make_view, + VkDeviceMemory* out_memory, VkImageView* out_view) +{ + VkMemoryRequirements reqs{}; + vkGetImageMemoryRequirements(ctx.device, image, &reqs); + uint32_t type_bits = reqs.memoryTypeBits; + if (extra_type_bits) + type_bits &= extra_type_bits; + + VkMemoryAllocateInfo alloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + alloc.pNext = alloc_pnext; + alloc.allocationSize = reqs.size; + alloc.memoryTypeIndex = ctx.FindMemoryType(type_bits, mem_props); + if (alloc.memoryTypeIndex == UINT32_MAX) { + LOG() << "vk: no compatible memory type for image allocation"; + return false; + } + VkDeviceMemory memory = VK_NULL_HANDLE; + if (LogIfFailed(vkAllocateMemory(ctx.device, &alloc, nullptr, &memory), + "vkAllocateMemory") != VK_SUCCESS) + return false; + if (LogIfFailed(vkBindImageMemory(ctx.device, image, memory, 0), + "vkBindImageMemory") != VK_SUCCESS) { + vkFreeMemory(ctx.device, memory, nullptr); + return false; + } + + VkImageView view = VK_NULL_HANDLE; + if (make_view) { + VkImageViewCreateInfo vci{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + vci.image = image; + vci.viewType = VK_IMAGE_VIEW_TYPE_2D; + vci.format = view_format; + vci.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + if (LogIfFailed(vkCreateImageView(ctx.device, &vci, nullptr, &view), + "vkCreateImageView") != VK_SUCCESS) { + vkFreeMemory(ctx.device, memory, nullptr); + return false; + } + } + *out_memory = memory; + if (out_view) *out_view = view; + return true; +} + +bool CreateImage2D(DeviceCtx& ctx, uint32_t width, uint32_t height, VkFormat format, + VkImageUsageFlags usage, VkImageTiling tiling, + VkMemoryPropertyFlags mem_props, bool make_view, Image2D* out) +{ + *out = {}; + VkImageCreateInfo ici{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + ici.imageType = VK_IMAGE_TYPE_2D; + ici.format = format; + ici.extent = {width, height, 1}; + ici.mipLevels = 1; + ici.arrayLayers = 1; + ici.samples = VK_SAMPLE_COUNT_1_BIT; + ici.tiling = tiling; + ici.usage = usage; + ici.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + ici.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + if (LogIfFailed(vkCreateImage(ctx.device, &ici, nullptr, &out->image), + "vkCreateImage") != VK_SUCCESS) + return false; + if (!AllocateBindImageView(ctx, out->image, format, mem_props, nullptr, 0, make_view, + &out->memory, &out->view)) { + vkDestroyImage(ctx.device, out->image, nullptr); + out->image = VK_NULL_HANDLE; + return false; + } + return true; +} + +VkShaderModule CreateShaderModule(VkDevice device, const uint32_t* spirv_words, size_t byte_size) +{ + VkShaderModuleCreateInfo ci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + ci.codeSize = byte_size; + ci.pCode = spirv_words; + VkShaderModule mod = VK_NULL_HANDLE; + LogIfFailed(vkCreateShaderModule(device, &ci, nullptr, &mod), "vkCreateShaderModule"); + return mod; +} + +} // namespace vrto3d::vk diff --git a/vrto3d/src/vk/vk_context.h b/vrto3d/src/vk/vk_context.h new file mode 100644 index 00000000..956bc077 --- /dev/null +++ b/vrto3d/src/vk/vk_context.h @@ -0,0 +1,89 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +#include + +#include +#include + +namespace vrto3d::vk { + +// Shared Vulkan device context for the Linux renderer. One per driver. +// +// Thread model: the queue is used from the compositor thread (dmabuf imports) +// and the present thread (frame rendering) — every vkQueueSubmit/vkQueuePresentKHR +// must hold queue_mutex. +struct DeviceCtx { + VkInstance instance = VK_NULL_HANDLE; + VkPhysicalDevice phys = VK_NULL_HANDLE; + VkDevice device = VK_NULL_HANDLE; + uint32_t queue_family = 0; + VkQueue queue = VK_NULL_HANDLE; + std::mutex queue_mutex; + VkPhysicalDeviceMemoryProperties mem_props{}; + + // Creates instance + device with the extensions the Linux port needs: + // instance: VK_KHR_surface, VK_KHR_wayland_surface, VK_KHR_xcb_surface, + // VK_KHR_xlib_surface (each only if supported), + // VK_KHR_get_physical_device_properties2 + // device: VK_KHR_swapchain, VK_KHR_external_memory_fd, + // VK_EXT_external_memory_dma_buf, VK_EXT_image_drm_format_modifier, + // VK_KHR_external_semaphore_fd, VK_KHR_timeline_semaphore + // Picks the first physical device that has the required device extensions + // and a graphics queue with present support. + bool Init(); + void Destroy(); + + // Index into mem_props matching type_bits + required property flags, or + // UINT32_MAX when no type matches. + uint32_t FindMemoryType(uint32_t type_bits, VkMemoryPropertyFlags props) const; +}; + +// A created 2D image with its dedicated allocation + view. +struct Image2D { + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; // VK_NULL_HANDLE if make_view=false + void Destroy(VkDevice device); +}; + +// Allocate device memory for an already-created image, bind it, and optionally +// make a color view. `image_pnext` chains onto the allocation (e.g. +// VkImportMemoryFdInfoKHR / VkExportMemoryAllocateInfo, each with a dedicated +// alloc); `extra_type_bits` further constrains the memory type (e.g. the +// vkGetMemoryFdPropertiesKHR bits for a dmabuf import; 0 = no extra constraint). +// The dmabuf import/export paths use this after building their own VkImage. +bool AllocateBindImageView(DeviceCtx& ctx, VkImage image, VkFormat view_format, + VkMemoryPropertyFlags mem_props, const void* alloc_pnext, + uint32_t extra_type_bits, bool make_view, + VkDeviceMemory* out_memory, VkImageView* out_view); + +// Full create+alloc+bind(+view) for the common device-local image case +// (out_sbs, presenter render targets, scratch images). Returns false on any +// failure (partial state cleaned up). +bool CreateImage2D(DeviceCtx& ctx, uint32_t width, uint32_t height, VkFormat format, + VkImageUsageFlags usage, VkImageTiling tiling, + VkMemoryPropertyFlags mem_props, bool make_view, Image2D* out); + +// Small helpers shared by the renderer / presenters / OSD backend. +VkShaderModule CreateShaderModule(VkDevice device, const uint32_t* spirv_words, size_t byte_size); + +// One-line VkResult logging helper. Returns r for chaining. +VkResult LogIfFailed(VkResult r, const char* what); + +} // namespace vrto3d::vk diff --git a/vrto3d/src/vk/vk_renderer.cpp b/vrto3d/src/vk/vk_renderer.cpp new file mode 100644 index 00000000..ab443b2c --- /dev/null +++ b/vrto3d/src/vk/vk_renderer.cpp @@ -0,0 +1,782 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#include "vk/vk_renderer.h" + +#include +#include +#include +#include + +#include "hmd_device_driver.h" +#include "osd/osd_menu.h" +#include "osd/osd_renderer.h" +#include "platform.h" +#include "presenter/vk_presenter.h" +#include "vrto3dlib/debug_log.hpp" +#include "vrto3dlib/linux_helper.hpp" + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#define STBI_WRITE_NO_STDIO_DISABLE +#include "stb_image_write.h" + +#include "../shaders/generated/fullscreen_vert_spv.h" +#include "../shaders/generated/repack_frag_spv.h" + +namespace { + +// C++ mirror of the push-constant block in shaders/repack.frag. +struct RepackPush { + int32_t out_w, out_h; + int32_t eye_w, eye_h; + int32_t mode; + int32_t eye_swap; + int32_t correction_enabled; + float curve; + float lift[4]; + float gamma[4]; + float gain[4]; + float curve_offsets[4]; +}; +static_assert(sizeof(RepackPush) <= 128, "push constant limit"); + +VkImageMemoryBarrier ImageBarrier(VkImage image, VkAccessFlags src_access, + VkAccessFlags dst_access, VkImageLayout old_layout, + VkImageLayout new_layout, + uint32_t src_family = VK_QUEUE_FAMILY_IGNORED, + uint32_t dst_family = VK_QUEUE_FAMILY_IGNORED) +{ + VkImageMemoryBarrier b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + b.srcAccessMask = src_access; + b.dstAccessMask = dst_access; + b.oldLayout = old_layout; + b.newLayout = new_layout; + b.srcQueueFamilyIndex = src_family; + b.dstQueueFamilyIndex = dst_family; + b.image = image; + b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + return b; +} + +} // namespace + +struct FrameTargetBits { + VkFramebuffer framebuffer; + VkExtent2D extent; +}; + +VkRenderer::VkRenderer() = default; + +VkRenderer::~VkRenderer() +{ + Shutdown(); +} + +bool VkRenderer::Init(const StereoDisplayDriverConfiguration& cfg, + const vrto3d::FocusContext& focus) +{ + cfg_ = cfg; + focus_ = focus; + display_frequency_ = cfg.display_frequency > 1.0f ? cfg.display_frequency : 60.0f; + + if (!ctx_.Init()) { + LOG() << "vk_renderer: device init failed"; + device_dead_.store(true); + return false; + } + + running_.store(true); + present_thread_ = std::thread(&VkRenderer::PresentThread, this); + vsync_thread_ = std::thread(&VkRenderer::VsyncTickThread, this); + return true; +} + +void VkRenderer::Shutdown() +{ + if (!running_.exchange(false)) + return; + frame_cv_.notify_all(); + if (present_thread_.joinable()) + present_thread_.join(); + if (vsync_thread_.joinable()) + vsync_thread_.join(); + // Present thread destroyed its own resources (pipeline, presenter, OSD) + // before exiting; the device goes last. + ctx_.Destroy(); +} + +void VkRenderer::OnDirectModeFrame(const EyeLayer& left, const EyeLayer& right) +{ + if (paused_for_disconnect_.load(std::memory_order_acquire) || + device_dead_.load(std::memory_order_acquire)) + return; + { + std::lock_guard lock(frame_mutex_); + pending_left_ = left; + pending_right_ = right; + ++pending_seq_; + } + frame_cv_.notify_one(); +} + +void VkRenderer::ConfigureOsd(StereoDisplayComponent* component, + vrto3d::osd::MenuCallbacks callbacks, void* /*native_window*/) +{ + osd_component_ = component; + osd_callbacks_ = std::make_unique(std::move(callbacks)); + osd_config_pending_ = true; +} + +vrto3d::osd::OsdRenderer* VkRenderer::Osd() +{ + return osd_renderer_.get(); +} + +void VkRenderer::RequestScreenshot(std::string app_name) +{ + std::lock_guard lock(shot_mutex_); + shot_app_name_ = std::move(app_name); + shot_requested_ = true; +} + +void VkRenderer::OnAppConnect() +{ + paused_for_disconnect_.store(false, std::memory_order_release); +} + +void VkRenderer::OnAppDisconnect() +{ + paused_for_disconnect_.store(true, std::memory_order_release); +} + +void VkRenderer::WaitIdleForTextureRelease() +{ + { + std::lock_guard lock(frame_mutex_); + consumed_seq_ = pending_seq_; // drop any not-yet-consumed snapshot + pending_left_ = {}; + pending_right_ = {}; + } + if (ctx_.device != VK_NULL_HANDLE) { + std::lock_guard qlock(ctx_.queue_mutex); + vkQueueWaitIdle(ctx_.queue); + } +} + +// --------------------------------------------------------------------------- + +bool VkRenderer::EnsureOutputImage(uint32_t eye_w, uint32_t eye_h) +{ + const uint32_t want_w = eye_w * 2; + const uint32_t want_h = eye_h; + if (out_sbs_ != VK_NULL_HANDLE && sbs_width_ == want_w && sbs_height_ == want_h) + return true; + + { + std::lock_guard qlock(ctx_.queue_mutex); + vkQueueWaitIdle(ctx_.queue); + } + if (out_sbs_view_) vkDestroyImageView(ctx_.device, out_sbs_view_, nullptr); + if (out_sbs_) vkDestroyImage(ctx_.device, out_sbs_, nullptr); + if (out_sbs_mem_) vkFreeMemory(ctx_.device, out_sbs_mem_, nullptr); + out_sbs_ = VK_NULL_HANDLE; out_sbs_view_ = VK_NULL_HANDLE; out_sbs_mem_ = VK_NULL_HANDLE; + + out_sbs_format_ = VK_FORMAT_R8G8B8A8_UNORM; + + vrto3d::vk::Image2D img; + if (!vrto3d::vk::CreateImage2D(ctx_, want_w, want_h, out_sbs_format_, + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, + VK_IMAGE_TILING_OPTIMAL, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + /*make_view=*/true, &img)) + return false; + out_sbs_ = img.image; + out_sbs_mem_ = img.memory; + out_sbs_view_ = img.view; + + sbs_width_ = want_w; + sbs_height_ = want_h; + out_sbs_initialized_ = false; + + // Point the repack descriptor at the new view. + if (repack_set_ != VK_NULL_HANDLE) { + VkDescriptorImageInfo img{repack_sampler_, out_sbs_view_, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = repack_set_; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &img; + vkUpdateDescriptorSets(ctx_.device, 1, &write, 0, nullptr); + } + + // OSD render target size follows the per-eye dims. + if (osd_renderer_) + osd_renderer_->OnResize(eye_w, eye_h); + + LOG() << "vk_renderer: out_sbs " << want_w << "x" << want_h; + return true; +} + +bool VkRenderer::EnsureRepackPipeline() +{ + if (repack_pipeline_ != VK_NULL_HANDLE) + return true; + if (!presenter_) + return false; + + repack_render_pass_ = presenter_->RenderPass(); + + VkSamplerCreateInfo sci{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + sci.magFilter = VK_FILTER_LINEAR; + sci.minFilter = VK_FILTER_LINEAR; + sci.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sci.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + vkCreateSampler(ctx_.device, &sci, nullptr, &repack_sampler_); + + VkDescriptorSetLayoutBinding binding{}; + binding.binding = 0; + binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + VkDescriptorSetLayoutCreateInfo dslci{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + dslci.bindingCount = 1; + dslci.pBindings = &binding; + vkCreateDescriptorSetLayout(ctx_.device, &dslci, nullptr, &repack_dsl_); + + VkPushConstantRange pc{VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(RepackPush)}; + VkPipelineLayoutCreateInfo plci{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + plci.setLayoutCount = 1; + plci.pSetLayouts = &repack_dsl_; + plci.pushConstantRangeCount = 1; + plci.pPushConstantRanges = &pc; + vkCreatePipelineLayout(ctx_.device, &plci, nullptr, &repack_layout_); + + VkDescriptorPoolSize pool_size{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1}; + VkDescriptorPoolCreateInfo dpci{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + dpci.maxSets = 1; + dpci.poolSizeCount = 1; + dpci.pPoolSizes = &pool_size; + vkCreateDescriptorPool(ctx_.device, &dpci, nullptr, &repack_pool_); + + VkDescriptorSetAllocateInfo dsai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + dsai.descriptorPool = repack_pool_; + dsai.descriptorSetCount = 1; + dsai.pSetLayouts = &repack_dsl_; + vkAllocateDescriptorSets(ctx_.device, &dsai, &repack_set_); + + if (out_sbs_view_ != VK_NULL_HANDLE) { + VkDescriptorImageInfo img{repack_sampler_, out_sbs_view_, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = repack_set_; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &img; + vkUpdateDescriptorSets(ctx_.device, 1, &write, 0, nullptr); + } + + VkShaderModule vs = vrto3d::vk::CreateShaderModule(ctx_.device, fullscreen_vert_spv, + sizeof(fullscreen_vert_spv)); + VkShaderModule fs = vrto3d::vk::CreateShaderModule(ctx_.device, repack_frag_spv, + sizeof(repack_frag_spv)); + if (!vs || !fs) + return false; + + VkPipelineShaderStageCreateInfo stages[2] = {}; + stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = vs; + stages[0].pName = "main"; + stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = fs; + stages[1].pName = "main"; + + VkPipelineVertexInputStateCreateInfo vin{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo ia{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + VkPipelineViewportStateCreateInfo vp{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + vp.viewportCount = 1; + vp.scissorCount = 1; + VkPipelineRasterizationStateCreateInfo rs{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + rs.polygonMode = VK_POLYGON_MODE_FILL; + rs.cullMode = VK_CULL_MODE_NONE; + rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + rs.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineColorBlendAttachmentState blend_att{}; + blend_att.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + blend.attachmentCount = 1; + blend.pAttachments = &blend_att; + VkDynamicState dyn_states[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo dyn{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + dyn.dynamicStateCount = 2; + dyn.pDynamicStates = dyn_states; + + VkGraphicsPipelineCreateInfo gpci{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + gpci.stageCount = 2; + gpci.pStages = stages; + gpci.pVertexInputState = &vin; + gpci.pInputAssemblyState = &ia; + gpci.pViewportState = &vp; + gpci.pRasterizationState = &rs; + gpci.pMultisampleState = &ms; + gpci.pColorBlendState = &blend; + gpci.pDynamicState = &dyn; + gpci.layout = repack_layout_; + gpci.renderPass = repack_render_pass_; + gpci.subpass = 0; + VkResult r = vkCreateGraphicsPipelines(ctx_.device, VK_NULL_HANDLE, 1, &gpci, nullptr, + &repack_pipeline_); + vkDestroyShaderModule(ctx_.device, vs, nullptr); + vkDestroyShaderModule(ctx_.device, fs, nullptr); + if (vrto3d::vk::LogIfFailed(r, "repack vkCreateGraphicsPipelines") != VK_SUCCESS) + return false; + + LOG() << "vk_renderer: repack pipeline ready"; + return true; +} + +void VkRenderer::RecordFrame(VkCommandBuffer cmd, const EyeLayer& left, const EyeLayer& right, + const FrameTargetBits& target) +{ + // Acquire the compositor's images (written on vrserver's own device; + // implicit dmabuf sync orders those writes against this submission). + VkImageMemoryBarrier acquire[2] = { + ImageBarrier(left.image, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, + VK_QUEUE_FAMILY_EXTERNAL, ctx_.queue_family), + ImageBarrier(right.image, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, + VK_QUEUE_FAMILY_EXTERNAL, ctx_.queue_family), + }; + VkImageMemoryBarrier to_dst = ImageBarrier( + out_sbs_, 0, VK_ACCESS_TRANSFER_WRITE_BIT, + out_sbs_initialized_ ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + out_sbs_initialized_ = true; + VkImageMemoryBarrier pre[3] = {acquire[0], acquire[1], to_dst}; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, nullptr, 0, nullptr, 3, pre); + + // Blit each eye into its half, honoring the submitted UV bounds (swapped + // offsets mirror automatically, which covers vMin>vMax flipped submissions). + const uint32_t eye_w = sbs_width_ / 2; + const uint32_t eye_h = sbs_height_; + for (int i = 0; i < 2; ++i) { + const EyeLayer& src = (i == 0) ? left : right; + VkImageBlit blit{}; + blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + blit.srcOffsets[0] = {(int32_t)std::lround(src.bounds.uMin * src.width), + (int32_t)std::lround(src.bounds.vMin * src.height), 0}; + blit.srcOffsets[1] = {(int32_t)std::lround(src.bounds.uMax * src.width), + (int32_t)std::lround(src.bounds.vMax * src.height), 1}; + blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + blit.dstOffsets[0] = {(int32_t)(i * eye_w), 0, 0}; + blit.dstOffsets[1] = {(int32_t)((i + 1) * eye_w), (int32_t)eye_h, 1}; + vkCmdBlitImage(cmd, src.image, VK_IMAGE_LAYOUT_GENERAL, out_sbs_, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR); + } + + // Screenshot readback happens pre-OSD, matching the Windows behavior. + MaybeSaveScreenshot(cmd); + + // OSD pass (no-op inside when nothing to draw). + VkImageMemoryBarrier to_color = ImageBarrier( + out_sbs_, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr, + 1, &to_color); + if (osd_renderer_) + osd_renderer_->RenderFrame(cmd, out_sbs_, out_sbs_view_, sbs_width_, sbs_height_); + + VkImageMemoryBarrier to_read = ImageBarrier( + out_sbs_, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, + &to_read); + + // Repack into the swapchain image. + VkClearValue clear{}; + VkRenderPassBeginInfo rpbi{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + rpbi.renderPass = repack_render_pass_; + rpbi.framebuffer = target.framebuffer; + rpbi.renderArea = {{0, 0}, target.extent}; + rpbi.clearValueCount = 1; + rpbi.pClearValues = &clear; + vkCmdBeginRenderPass(cmd, &rpbi, VK_SUBPASS_CONTENTS_INLINE); + + VkViewport viewport{0.f, 0.f, (float)target.extent.width, (float)target.extent.height, + 0.f, 1.f}; + VkRect2D scissor{{0, 0}, target.extent}; + vkCmdSetViewport(cmd, 0, 1, &viewport); + vkCmdSetScissor(cmd, 0, 1, &scissor); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, repack_pipeline_); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, repack_layout_, 0, 1, + &repack_set_, 0, nullptr); + + RepackPush push{}; + push.out_w = (int32_t)target.extent.width; + push.out_h = (int32_t)target.extent.height; + push.eye_w = (int32_t)eye_w; + push.eye_h = (int32_t)eye_h; + StereoDisplayDriverConfiguration cfg = osd_component_ ? osd_component_->GetConfig() : cfg_; + push.mode = (int32_t)cfg.output_mode; + push.eye_swap = cfg.eye_swap ? 1 : 0; + push.correction_enabled = cfg.shader_enabled ? 1 : 0; + push.curve = cfg.shader_curve; + for (int i = 0; i < 3; ++i) { + push.lift[i] = cfg.shader_lift[i]; + push.gamma[i] = cfg.shader_gamma[i]; + push.gain[i] = cfg.shader_gain[i]; + } + push.curve_offsets[0] = cfg.shader_curve_off_low; + push.curve_offsets[1] = cfg.shader_curve_off_high; + push.curve_offsets[2] = cfg.shader_curve_off_both; + vkCmdPushConstants(cmd, repack_layout_, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), &push); + vkCmdDraw(cmd, 3, 1, 0, 0); + vkCmdEndRenderPass(cmd); +} + +void VkRenderer::MaybeSaveScreenshot(VkCommandBuffer cmd) +{ + bool want = false; + { + std::lock_guard lock(shot_mutex_); + want = shot_requested_ && !shot_inflight_; + if (want) + shot_requested_ = false; + } + if (!want) + return; + + const VkDeviceSize size = (VkDeviceSize)sbs_width_ * sbs_height_ * 4; + if (shot_buffer_ == VK_NULL_HANDLE || shot_w_ != sbs_width_ || shot_h_ != sbs_height_) { + if (shot_buffer_) vkDestroyBuffer(ctx_.device, shot_buffer_, nullptr); + if (shot_memory_) vkFreeMemory(ctx_.device, shot_memory_, nullptr); + VkBufferCreateInfo bci{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bci.size = size; + bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + if (vkCreateBuffer(ctx_.device, &bci, nullptr, &shot_buffer_) != VK_SUCCESS) + return; + VkMemoryRequirements reqs{}; + vkGetBufferMemoryRequirements(ctx_.device, shot_buffer_, &reqs); + VkMemoryAllocateInfo alloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + alloc.allocationSize = reqs.size; + alloc.memoryTypeIndex = ctx_.FindMemoryType( + reqs.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + if (alloc.memoryTypeIndex == UINT32_MAX || + vkAllocateMemory(ctx_.device, &alloc, nullptr, &shot_memory_) != VK_SUCCESS) + return; + vkBindBufferMemory(ctx_.device, shot_buffer_, shot_memory_, 0); + shot_w_ = sbs_width_; + shot_h_ = sbs_height_; + } + + VkImageMemoryBarrier to_src = ImageBarrier( + out_sbs_, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, + 0, nullptr, 0, nullptr, 1, &to_src); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {sbs_width_, sbs_height_, 1}; + vkCmdCopyImageToBuffer(cmd, out_sbs_, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, shot_buffer_, 1, + ©); + VkImageMemoryBarrier back = ImageBarrier( + out_sbs_, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, + 0, nullptr, 0, nullptr, 1, &back); + shot_inflight_ = true; +} + +void VkRenderer::FinishScreenshot() +{ + if (!shot_inflight_) + return; + shot_inflight_ = false; + + void* mapped = nullptr; + if (vkMapMemory(ctx_.device, shot_memory_, 0, VK_WHOLE_SIZE, 0, &mapped) != VK_SUCCESS) + return; + + namespace fs = std::filesystem; + const std::string steam = GetSteamInstallPath(); + if (steam.empty()) { + vkUnmapMemory(ctx_.device, shot_memory_); + return; + } + fs::path dir = fs::path(steam) / "steamapps/common/SteamVR/screenshots"; + std::error_code ec; + fs::create_directories(dir, ec); + + std::string app; + { + std::lock_guard lock(shot_mutex_); + app = shot_app_name_.empty() ? "vrto3d" : shot_app_name_; + } + for (char& c : app) + if (c == '/' || c == '\\' || c == ':') + c = '_'; + const auto stamp = (long long)(platform::MonotonicSeconds() * 1000.0); + + const uint32_t w = shot_w_, h = shot_h_, eye_w = w / 2; + const uint8_t* src = (const uint8_t*)mapped; + + // Parallel-view (as composited) + cross-view (eyes swapped). + std::vector cross((size_t)w * h * 4); + for (uint32_t y = 0; y < h; ++y) { + const uint8_t* row = src + (size_t)y * w * 4; + uint8_t* out = cross.data() + (size_t)y * w * 4; + memcpy(out, row + (size_t)eye_w * 4, (size_t)eye_w * 4); + memcpy(out + (size_t)eye_w * 4, row, (size_t)eye_w * 4); + } + const std::string para = (dir / (app + "_" + std::to_string(stamp) + "_parallel.png")).string(); + const std::string crss = (dir / (app + "_" + std::to_string(stamp) + "_cross.png")).string(); + stbi_write_png(para.c_str(), (int)w, (int)h, 4, src, (int)(w * 4)); + stbi_write_png(crss.c_str(), (int)w, (int)h, 4, cross.data(), (int)(w * 4)); + vkUnmapMemory(ctx_.device, shot_memory_); + LOG() << "vk_renderer: screenshot saved " << para; +} + +// --------------------------------------------------------------------------- + +void VkRenderer::PresentThread() +{ + presenter_ = vrto3d::MakeVkPresenter(cfg_); + if (!presenter_ || !presenter_->Init(&ctx_, cfg_)) { + LOG() << "vk_renderer: presenter init failed — no output window. Frames will be dropped."; + presenter_.reset(); + // Stay alive: vsync events keep the compositor happy so SteamVR + // doesn't watchdog; there's just nothing on screen. + while (running_.load()) + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + return; + } + // Initial z-order/input state is set by the focus block on the first frame + // (starts lowered + click-through, then the auto-focus path raises on app + // connect) — no unconditional BringToTop here. + + VkCommandPoolCreateInfo cpci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + cpci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + cpci.queueFamilyIndex = ctx_.queue_family; + vkCreateCommandPool(ctx_.device, &cpci, nullptr, &cmd_pool_); + VkCommandBufferAllocateInfo cbai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; + cbai.commandPool = cmd_pool_; + cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + cbai.commandBufferCount = kFramesInFlight; + vkAllocateCommandBuffers(ctx_.device, &cbai, cmd_bufs_); + for (int i = 0; i < kFramesInFlight; ++i) { + VkFenceCreateInfo fci{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + fci.flags = VK_FENCE_CREATE_SIGNALED_BIT; + vkCreateFence(ctx_.device, &fci, nullptr, &frame_fences_[i]); + VkSemaphoreCreateInfo sci{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; + vkCreateSemaphore(ctx_.device, &sci, nullptr, &acquire_sems_[i]); + vkCreateSemaphore(ctx_.device, &sci, nullptr, &render_sems_[i]); + } + + while (running_.load()) { + EyeLayer left, right; + bool have_frame = false; + { + std::unique_lock lock(frame_mutex_); + frame_cv_.wait_for(lock, std::chrono::milliseconds(50), [&] { + return pending_seq_ != consumed_seq_ || !running_.load(); + }); + if (!running_.load()) + break; + if (pending_seq_ != consumed_seq_) { + consumed_seq_ = pending_seq_; + left = pending_left_; + right = pending_right_; + have_frame = left.image != VK_NULL_HANDLE && right.image != VK_NULL_HANDLE; + } + } + + if (!presenter_->PumpEvents()) { + LOG() << "vk_renderer: presenter closed — requesting SteamVR shutdown"; + std::thread([] { RequestSteamVRShutdownWithApp(g_current_app_pid.load()); }).detach(); + break; + } + // Focus/z-order + input capture, edge-tracked on the present thread + // (the only thread that may touch the display connection). This mirrors + // the Windows WindowPresenter::FocusThreadLoop: start lowered; raise + // when an app connects (auto-focus, latched once per app PID); the + // manual Ctrl+F8 / "Always on Top" toggle (man_on_top) overrides; and + // an open OSD forces on-top + input capture so clicks/keys drive the + // GUI rather than the game. + // + // MUST run before the have_frame gate below: when an app disconnects, + // LoadSettings queues the LOWER (clears is_on_top/man_on_top) AND the + // renderer is paused (OnDirectModeFrame drops frames), so have_frame + // stays false. Gating this behind a frame would starve it and leave the + // overlay stuck on top — the queued LOWER would never be applied. + { + const bool menu_open = osd_renderer_ && osd_renderer_->MenuVisible(); + vrto3d::FocusInputs fi; + fi.is_on_top = focus_.is_on_top && focus_.is_on_top->load(); + fi.man_on_top = focus_.man_on_top && focus_.man_on_top->load(); + fi.auto_focus = focus_.auto_focus ? focus_.auto_focus->load() : true; + fi.app_pid = focus_.app_pid ? focus_.app_pid->load() : 0; + fi.app_running = fi.app_pid != 0 && platform::IsProcessRunning(fi.app_pid); + fi.force_on_top = menu_open; // an open OSD must be visible to use + + bool set_is_on_top = false, set_man_on_top = false; + const bool want_on_top = + vrto3d::ComputeWantOnTop(fi, focus_latch_, &set_is_on_top, &set_man_on_top); + if (set_is_on_top && focus_.is_on_top) focus_.is_on_top->store(true); + if (set_man_on_top && focus_.man_on_top) focus_.man_on_top->store(true); + + // Capture input only while the OSD is open (game shielded from menu + // clicks/typing); otherwise click-through so input reaches the game. + const bool want_capture = menu_open; + + if (!focus_state_init_ || want_on_top != last_on_top_) { + LOG() << "focus: " << (want_on_top ? "RAISE" : "LOWER") + << " (is_on_top=" << fi.is_on_top + << " man=" << fi.man_on_top + << " auto=" << fi.auto_focus + << " pid=" << fi.app_pid + << " running=" << fi.app_running + << " menu=" << menu_open << ")"; + presenter_->SetAlwaysOnTop(want_on_top); + last_on_top_ = want_on_top; + } + if (!focus_state_init_ || want_capture != last_capture_) { + presenter_->SetInputCapture(want_capture); + last_capture_ = want_capture; + } + focus_state_init_ = true; + } + + if (!have_frame) + continue; + + if (!EnsureOutputImage(left.width, left.height) || !EnsureRepackPipeline()) + continue; + + // Lazy OSD init once dimensions are known. + if (osd_config_pending_ && osd_component_ && osd_callbacks_) { + osd_config_pending_ = false; + auto osd = std::make_unique(); + if (osd->Init(&ctx_, out_sbs_format_, sbs_width_ / 2, sbs_height_, nullptr, + osd_component_, *osd_callbacks_)) { + osd_renderer_ = std::move(osd); + } else { + LOG() << "vk_renderer: OSD init failed — continuing without OSD"; + } + } + + const int slot = frame_slot_; + frame_slot_ = (frame_slot_ + 1) % kFramesInFlight; + vkWaitForFences(ctx_.device, 1, &frame_fences_[slot], VK_TRUE, UINT64_MAX); + if (shot_inflight_) + FinishScreenshot(); + vkResetFences(ctx_.device, 1, &frame_fences_[slot]); + + vrto3d::IVkPresenter::FrameTarget target{}; + if (!presenter_->AcquireNext(&target, acquire_sems_[slot])) { + // Swapchain went stale (resize etc.) — pipeline must follow the + // new render pass on the next round if the presenter recreated it. + continue; + } + + VkCommandBuffer cmd = cmd_bufs_[slot]; + vkResetCommandBuffer(cmd, 0); + VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; + begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cmd, &begin); + FrameTargetBits bits{target.framebuffer, presenter_->Extent()}; + RecordFrame(cmd, left, right, bits); + vkEndCommandBuffer(cmd); + + VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; + submit.waitSemaphoreCount = 1; + submit.pWaitSemaphores = &acquire_sems_[slot]; + submit.pWaitDstStageMask = &wait_stage; + submit.commandBufferCount = 1; + submit.pCommandBuffers = &cmd; + submit.signalSemaphoreCount = 1; + submit.pSignalSemaphores = &render_sems_[slot]; + { + std::lock_guard qlock(ctx_.queue_mutex); + if (vrto3d::vk::LogIfFailed(vkQueueSubmit(ctx_.queue, 1, &submit, frame_fences_[slot]), + "frame vkQueueSubmit") != VK_SUCCESS) { + device_dead_.store(true, std::memory_order_release); + break; + } + } + presenter_->Present(target.index, render_sems_[slot]); + + frame_counter_.fetch_add(1, std::memory_order_relaxed); + last_vsync_sec_.store(platform::MonotonicSeconds(), std::memory_order_relaxed); + } + + // Teardown on this thread (presenter + OSD are thread-affine). + { + std::lock_guard qlock(ctx_.queue_mutex); + vkQueueWaitIdle(ctx_.queue); + } + if (osd_renderer_) { + osd_renderer_->Shutdown(); + osd_renderer_.reset(); + } + for (int i = 0; i < kFramesInFlight; ++i) { + if (frame_fences_[i]) vkDestroyFence(ctx_.device, frame_fences_[i], nullptr); + if (acquire_sems_[i]) vkDestroySemaphore(ctx_.device, acquire_sems_[i], nullptr); + if (render_sems_[i]) vkDestroySemaphore(ctx_.device, render_sems_[i], nullptr); + } + if (cmd_pool_) vkDestroyCommandPool(ctx_.device, cmd_pool_, nullptr); + if (repack_pipeline_) vkDestroyPipeline(ctx_.device, repack_pipeline_, nullptr); + if (repack_layout_) vkDestroyPipelineLayout(ctx_.device, repack_layout_, nullptr); + if (repack_pool_) vkDestroyDescriptorPool(ctx_.device, repack_pool_, nullptr); + if (repack_dsl_) vkDestroyDescriptorSetLayout(ctx_.device, repack_dsl_, nullptr); + if (repack_sampler_) vkDestroySampler(ctx_.device, repack_sampler_, nullptr); + if (out_sbs_view_) vkDestroyImageView(ctx_.device, out_sbs_view_, nullptr); + if (out_sbs_) vkDestroyImage(ctx_.device, out_sbs_, nullptr); + if (out_sbs_mem_) vkFreeMemory(ctx_.device, out_sbs_mem_, nullptr); + if (shot_buffer_) vkDestroyBuffer(ctx_.device, shot_buffer_, nullptr); + if (shot_memory_) vkFreeMemory(ctx_.device, shot_memory_, nullptr); + if (presenter_) { + presenter_->Shutdown(); + presenter_.reset(); + } +} + +void VkRenderer::VsyncTickThread() +{ + const auto interval = std::chrono::duration_cast( + std::chrono::duration(1.0 / display_frequency_)); + auto next = std::chrono::steady_clock::now(); + while (running_.load()) { + vr::VRServerDriverHost()->VsyncEvent(0.0); + next += interval; + std::this_thread::sleep_until(next); + } +} diff --git a/vrto3d/src/vk/vk_renderer.h b/vrto3d/src/vk/vk_renderer.h new file mode 100644 index 00000000..41619500 --- /dev/null +++ b/vrto3d/src/vk/vk_renderer.h @@ -0,0 +1,185 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +// Linux twin of Dx11Renderer. The Linux compositor pre-composites all apps and +// overlays and submits a single layer pair per frame, so unlike the Windows +// path there is no multi-layer composite: import the compositor's shared +// images once, per frame blit L/R +// into out_sbs_, run the OSD, then repack into the presenter's swapchain. + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "openvr_driver.h" + +#include "focus_context.h" +#include "focus_policy.h" +#include "vk/vk_context.h" +#include "vrto3dlib/stereo_config.h" + +class StereoDisplayComponent; +namespace vrto3d { +class IVkPresenter; +namespace osd { +class OsdRenderer; +struct MenuCallbacks; +} // namespace osd +} // namespace vrto3d + +class VkRenderer { +public: + VkRenderer(); + ~VkRenderer(); + + bool Init(const StereoDisplayDriverConfiguration& cfg, const vrto3d::FocusContext& focus); + void Shutdown(); + + // Per-eye view of an imported compositor swap texture. `image`/`view` are + // owned by DirectModeComponentVk's handle map and stay valid until + // DestroySwapTextureSet / DestroyAllSwapTextureSets. + struct EyeLayer { + VkImage image = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + uint32_t width = 0; + uint32_t height = 0; + vr::VRTextureBounds_t bounds{0.f, 0.f, 1.f, 1.f}; + }; + + // Called from IVRDriverDirectModeComponent::Present (compositor thread). + // Snapshots the pair and wakes the present thread. Cheap — no Vulkan work + // happens on the compositor thread (implicit dmabuf sync orders the + // compositor's rendering against our sampling at submit time). + void OnDirectModeFrame(const EyeLayer& left, const EyeLayer& right); + + // OSD plumbing (mirrors Dx11Renderer). `native_window` is unused on Linux + // (mouse mapping is virtual-cursor based) but kept for signature parity. + void ConfigureOsd(StereoDisplayComponent* component, + vrto3d::osd::MenuCallbacks callbacks, + void* native_window); + void SetOsdHeadsetHwnd(void* native_window) {} + + void RequestScreenshot(std::string app_name); + + vrto3d::osd::OsdRenderer* Osd(); + StereoDisplayComponent* Component() { return osd_component_; } + vrto3d::IVkPresenter* Presenter() { return presenter_.get(); } + + void OnAppConnect(); + void OnAppDisconnect(); + + // Called by DirectModeComponentVk before it destroys imported images: + // drops the pending frame snapshot and waits for in-flight GPU work so no + // submitted command buffer still samples the doomed VkImages. + void WaitIdleForTextureRelease(); + + uint64_t FrameCounter() const { return frame_counter_.load(std::memory_order_relaxed); } + double LastVsyncQpcSec() const { return last_vsync_sec_.load(std::memory_order_relaxed); } + bool IsDeviceDead() const { return device_dead_.load(std::memory_order_acquire); } + + vrto3d::vk::DeviceCtx& Ctx() { return ctx_; } + +private: + void PresentThread(); + void VsyncTickThread(); + bool EnsureOutputImage(uint32_t eye_w, uint32_t eye_h); + bool EnsureRepackPipeline(); + void RecordFrame(VkCommandBuffer cmd, const EyeLayer& left, const EyeLayer& right, + const struct FrameTargetBits& target); + void MaybeSaveScreenshot(VkCommandBuffer cmd); + void FinishScreenshot(); + + vrto3d::vk::DeviceCtx ctx_; + StereoDisplayDriverConfiguration cfg_; + vrto3d::FocusContext focus_; + + std::unique_ptr presenter_; + + // out_sbs_: 2W x H canonical side-by-side target. + VkImage out_sbs_ = VK_NULL_HANDLE; + VkDeviceMemory out_sbs_mem_ = VK_NULL_HANDLE; + VkImageView out_sbs_view_ = VK_NULL_HANDLE; + VkFormat out_sbs_format_ = VK_FORMAT_UNDEFINED; + uint32_t sbs_width_ = 0; + uint32_t sbs_height_ = 0; + bool out_sbs_initialized_ = false; // layout tracking + + // Repack pipeline (fullscreen triangle sampling out_sbs_ into swapchain). + VkDescriptorSetLayout repack_dsl_ = VK_NULL_HANDLE; + VkPipelineLayout repack_layout_ = VK_NULL_HANDLE; + VkPipeline repack_pipeline_ = VK_NULL_HANDLE; + VkDescriptorPool repack_pool_ = VK_NULL_HANDLE; + VkDescriptorSet repack_set_ = VK_NULL_HANDLE; + VkSampler repack_sampler_ = VK_NULL_HANDLE; + VkRenderPass repack_render_pass_ = VK_NULL_HANDLE; // presenter's + + // Present-thread command machinery (double-buffered). + static constexpr int kFramesInFlight = 2; + VkCommandPool cmd_pool_ = VK_NULL_HANDLE; + VkCommandBuffer cmd_bufs_[kFramesInFlight] = {}; + VkFence frame_fences_[kFramesInFlight] = {}; + VkSemaphore acquire_sems_[kFramesInFlight] = {}; + VkSemaphore render_sems_[kFramesInFlight] = {}; + int frame_slot_ = 0; + + // Latest frame snapshot from the compositor thread. + std::mutex frame_mutex_; + std::condition_variable frame_cv_; + EyeLayer pending_left_{}; + EyeLayer pending_right_{}; + uint64_t pending_seq_ = 0; + uint64_t consumed_seq_ = 0; + + // OSD + StereoDisplayComponent* osd_component_ = nullptr; + std::unique_ptr osd_renderer_; + std::unique_ptr osd_callbacks_; + bool osd_config_pending_ = false; + + // Screenshot request (drained on present thread). + std::mutex shot_mutex_; + std::string shot_app_name_; + bool shot_requested_ = false; + VkBuffer shot_buffer_ = VK_NULL_HANDLE; + VkDeviceMemory shot_memory_ = VK_NULL_HANDLE; + uint32_t shot_w_ = 0, shot_h_ = 0; + bool shot_inflight_ = false; + + std::thread present_thread_; + std::thread vsync_thread_; + std::atomic running_{false}; + std::atomic paused_for_disconnect_{false}; + std::atomic device_dead_{false}; + + std::atomic frame_counter_{0}; + std::atomic last_vsync_sec_{0.0}; + float display_frequency_ = 60.0f; + + // Focus/z-order + input-capture edge tracking (present thread only). + // Mirrors WindowPresenter::FocusThreadLoop; starts lowered. + bool focus_state_init_ = false; + bool last_on_top_ = false; + bool last_capture_ = false; + vrto3d::FocusLatchState focus_latch_; +}; diff --git a/vrto3d/src/vr_recenter.cpp b/vrto3d/src/vr_recenter.cpp new file mode 100644 index 00000000..a6235aae --- /dev/null +++ b/vrto3d/src/vr_recenter.cpp @@ -0,0 +1,152 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ + +// This TU intentionally only includes openvr.h (the client API). +// Do NOT include openvr_driver.h here — the two headers redeclare +// types in the vr:: namespace and will not coexist in one TU. +#include "openvr.h" + +#include "vr_recenter.h" + +#include "vrto3dlib/debug_log.hpp" + +#include +#include + +namespace vrto3d { + +// VR_Init / VR_Shutdown is effectively one-shot when called from inside the +// vrserver process. Once VR_Shutdown runs, the global "client version" +// state stays set and every subsequent VR_Init returns +// VRInitError_Init_ClientVersionAlreadyProvided (169). The wedge is +// permanent for the lifetime of vrserver. +// +// So we init a single VRApplication_Background session on first use, cache +// the chaperone pointer, and never call VR_Shutdown. Subsequent recenters +// just reuse the cached interface. The session is benign — Background apps +// are not visible to the user and don't count as scene apps. +namespace { + enum class State { + NotInitialized, // VR_Init has not been attempted + Initialized, // VR_Init succeeded, chaperone may or may not be cached + InitFailed, // VR_Init failed once — never retry (err-169 ratchet) + ShutDown, // ShutdownOpenVRClient ran; no further calls allowed + }; + + std::mutex g_init_mutex; + State g_state = State::NotInitialized; + vr::IVRChaperone* g_chaperone = nullptr; + vr::IVRSystem* g_vrsystem = nullptr; // cached so PumpOpenVRClientEvents can poll + + // Returns a usable chaperone pointer or nullptr. Tries VR_Init only + // once per process (any retry would hit err-169); re-resolves the + // chaperone interface every call until we get one, since it may not + // be available immediately after VR_Init returns. + vr::IVRChaperone* GetChaperone(const char* t) + { + std::lock_guard lock(g_init_mutex); + + if (g_state == State::InitFailed || g_state == State::ShutDown) { + return nullptr; + } + + if (g_state == State::NotInitialized) { + vr::EVRInitError err = vr::VRInitError_None; + vr::IVRSystem* sys = vr::VR_Init(&err, vr::VRApplication_Background); + if (err != vr::VRInitError_None || sys == nullptr) { + LOG() << "TriggerOpenVRRecenter[" << t << "]: VR_Init failed err=" + << static_cast(err) << " (" + << vr::VR_GetVRInitErrorAsSymbol(err) << ")"; + g_state = State::InitFailed; + return nullptr; + } + g_state = State::Initialized; + g_vrsystem = sys; + LOG() << "TriggerOpenVRRecenter[" << t << "]: client session initialized"; + } + + if (!g_chaperone) { + g_chaperone = vr::VRChaperone(); + if (!g_chaperone) { + LOG() << "TriggerOpenVRRecenter[" << t << "]: VRChaperone() still null"; + } + } + return g_chaperone; + } +} + +bool TriggerOpenVRRecenter(const char* tag) +{ + const char* t = tag ? tag : "recenter"; + + vr::IVRChaperone* chap = GetChaperone(t); + if (!chap) return false; + + chap->ResetZeroPose(vr::TrackingUniverseSeated); + chap->ResetZeroPose(vr::TrackingUniverseStanding); + LOG() << "TriggerOpenVRRecenter[" << t << "]: ResetZeroPose issued"; + return true; +} + +void ShutdownOpenVRClient() +{ + std::lock_guard lock(g_init_mutex); + if (g_state == State::Initialized) { + g_chaperone = nullptr; + g_vrsystem = nullptr; + vr::VR_Shutdown(); + LOG() << "ShutdownOpenVRClient: VR_Shutdown issued"; + } + // Always flip to ShutDown — even from InitFailed/NotInitialized — so + // any late TriggerOpenVRRecenter call (e.g. from a detached auto_focus + // thread that slept past the quit event) becomes a clean no-op rather + // than re-attempting VR_Init while vrserver is mid-teardown. + g_state = State::ShutDown; +} + +void PumpOpenVRClientEvents() +{ + // Snapshot the system pointer under the lock so we don't hold the + // mutex across PollNextEvent (cheap call but no reason to serialize + // unrelated TriggerOpenVRRecenter calls behind it). + vr::IVRSystem* sys = nullptr; + { + std::lock_guard lock(g_init_mutex); + if (g_state != State::Initialized) return; + sys = g_vrsystem; + } + if (!sys) return; + + vr::VREvent_t ev{}; + bool saw_quit = false; + while (sys->PollNextEvent(&ev, sizeof(ev))) { + if (ev.eventType == vr::VREvent_Quit) { + saw_quit = true; + // Ack first so vrserver stops waiting on us, then break out + // and let ShutdownOpenVRClient run outside the loop — calling + // it while still polling would invalidate `sys`. + sys->AcknowledgeQuit_Exiting(); + LOG() << "PumpOpenVRClientEvents: VREvent_Quit ack'd"; + break; + } + } + if (saw_quit) { + ShutdownOpenVRClient(); + } +} + +} // namespace vrto3d diff --git a/vrto3d/src/vr_recenter.h b/vrto3d/src/vr_recenter.h new file mode 100644 index 00000000..20c936d7 --- /dev/null +++ b/vrto3d/src/vr_recenter.h @@ -0,0 +1,52 @@ +/* + * This file is part of VRto3D. + * + * VRto3D is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VRto3D is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with VRto3D. If not, see . + */ +#pragma once + +// Thin wrapper around openvr.h (the *client* API). Kept in its own +// translation unit so the rest of the driver can keep including +// openvr_driver.h without hitting redeclaration errors from the two +// headers sharing the vr:: namespace. + +namespace vrto3d { + +// Triggers a SteamVR recenter via the OpenVR client API +// (IVRChaperone::ResetZeroPose). Replaces the old +// PostMessage(vr_window, WM_KEYDOWN, 'Z', 0) hack. +// +// Initializes a transient background-app session, issues the reset for +// both the seated and standing universes, and shuts the session back +// down. Safe to call from any thread; returns false if the runtime +// could not be reached. +// +// `tag` is included in the log line so the same TU can be called from +// several sites (auto_focus retries, hotkey, OSD toggle) and still be +// disambiguated in the driver log. +bool TriggerOpenVRRecenter(const char* tag = nullptr); + +// Tear down the cached background client session created by +// TriggerOpenVRRecenter. Safe to call from RunFrame (poll path) or +// Cleanup (fallback). Idempotent. +void ShutdownOpenVRClient(); + +// Drain pending events on our cached client session and handle +// VREvent_Quit: acknowledge it, then shut the client down. Must be +// called from RunFrame on every tick once VR_Init has succeeded — if +// we don't acknowledge VREvent_Quit, vrserver waits 5s and force-kills +// us. No-op when no client session is open. +void PumpOpenVRClientEvents(); + +} // namespace vrto3d diff --git a/vrto3d/vrto3d.vcxproj b/vrto3d/vrto3d.vcxproj index 31260cf7..7012f823 100644 --- a/vrto3d/vrto3d.vcxproj +++ b/vrto3d/vrto3d.vcxproj @@ -98,21 +98,30 @@ true WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) true - ..\external\VRto3DLib\include;..\external\json\include;..\external\openvr\headers;..\utils\vrmath + src;..\external\VRto3DLib\include;..\external\json\include;..\external\openvr\headers;..\external\SR-lib\SR-SDK-1.34.10\x64\include;..\external\SR-lib\SR-SDK-1.34.10\x64\third_party\OpenCV\include;..\external\nvapi;..\external\adl_sdk\include;..\external\imgui;..\external\imgui\backends;..\external\NV3D-Lib\NV3DLib\include;..\utils\vrmath stdcpp17 4244;4305 Console true - ..\external\openvr\lib\win$(PlatformArchitecture) - openvr_api.lib;%(AdditionalDependencies) + ..\external\openvr\lib\win$(PlatformArchitecture);..\external\SR-lib\SR-SDK-1.34.10\x64\lib;..\external\SR-lib\SR-SDK-1.34.10\x64\third_party\OpenCV\lib\x64;..\external\nvapi\amd64;..\external\NV3D-Lib\NV3DLib\lib\x$(PlatformArchitecture)\$(Configuration)-MD + openvr_api.lib;d3d11.lib;dxgi.lib;dxguid.lib;d3dcompiler.lib;d3d9.lib;ws2_32.lib;simulatedreality.lib;SimulatedRealityCore.lib;SimulatedRealityDirectX.lib;SimulatedRealityDisplays.lib;SimulatedRealityFaceTrackers.lib;DimencoWeaving.lib;opencv_core343.lib;NV3DLib-mdd.lib;nvapi64.lib;delayimp.lib;%(AdditionalDependencies) + simulatedreality.dll;SimulatedRealityCore.dll;SimulatedRealityDirectX.dll;SimulatedRealityDisplays.dll;SimulatedRealityFaceTrackers.dll;DimencoWeaving.dll;opencv_core343.dll;nvapi64.dll;openvr_api.dll;%(DelayLoadDLLs) + + + powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(ProjectDir)..\external\NV3D-Lib\tools\build.ps1" -Configuration $(Configuration)-MD -Platform $(PlatformShortName) -Target NV3DLib + XCOPY "$(ProjectDir)$(ProjectName)" "$(SolutionDir)output\drivers\$(ProjectName)" /S /Y /I + COPY /Y "$(ProjectDir)..\external\openvr\bin\win$(PlatformArchitecture)\openvr_api.dll" "$(SolutionDir)output\drivers\$(ProjectName)\bin\win$(PlatformArchitecture)\openvr_api.dll" XCOPY "$(SolutionDir)output\drivers\" "$(STEAM_PATH)\steamapps\common\SteamVR\drivers\" /S /Y /I - + @@ -123,7 +132,7 @@ true WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true - ..\external\VRto3DLib\include;..\external\json\include;..\external\openvr\headers;..\utils\vrmath + src;..\external\VRto3DLib\include;..\external\json\include;..\external\openvr\headers;..\external\SR-lib\SR-SDK-1.34.10\x64\include;..\external\SR-lib\SR-SDK-1.34.10\x64\third_party\OpenCV\include;..\external\nvapi;..\external\adl_sdk\include;..\external\imgui;..\external\imgui\backends;..\external\NV3D-Lib\NV3DLib\include;..\utils\vrmath stdcpp17 4244;4305 @@ -132,14 +141,23 @@ true true true - ..\external\openvr\lib\win$(PlatformArchitecture) - openvr_api.lib;%(AdditionalDependencies) + ..\external\openvr\lib\win$(PlatformArchitecture);..\external\SR-lib\SR-SDK-1.34.10\x64\lib;..\external\SR-lib\SR-SDK-1.34.10\x64\third_party\OpenCV\lib\x64;..\external\nvapi\amd64;..\external\NV3D-Lib\NV3DLib\lib\x$(PlatformArchitecture)\$(Configuration)-MD + openvr_api.lib;d3d11.lib;dxgi.lib;dxguid.lib;d3dcompiler.lib;d3d9.lib;ws2_32.lib;simulatedreality.lib;SimulatedRealityCore.lib;SimulatedRealityDirectX.lib;SimulatedRealityDisplays.lib;SimulatedRealityFaceTrackers.lib;DimencoWeaving.lib;opencv_core343.lib;NV3DLib-md.lib;nvapi64.lib;delayimp.lib;%(AdditionalDependencies) + simulatedreality.dll;SimulatedRealityCore.dll;SimulatedRealityDirectX.dll;SimulatedRealityDisplays.dll;SimulatedRealityFaceTrackers.dll;DimencoWeaving.dll;opencv_core343.dll;nvapi64.dll;openvr_api.dll;%(DelayLoadDLLs) + + + powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(ProjectDir)..\external\NV3D-Lib\tools\build.ps1" -Configuration $(Configuration)-MD -Platform $(PlatformShortName) -Target NV3DLib + XCOPY "$(ProjectDir)$(ProjectName)" "$(SolutionDir)output\drivers\$(ProjectName)" /S /Y /I + COPY /Y "$(ProjectDir)..\external\openvr\bin\win$(PlatformArchitecture)\openvr_api.dll" "$(SolutionDir)output\drivers\$(ProjectName)\bin\win$(PlatformArchitecture)\openvr_api.dll" XCOPY "$(SolutionDir)output\drivers\" "$(STEAM_PATH)\steamapps\common\SteamVR\drivers\" /S /Y /I - + @@ -148,21 +166,30 @@ true _DEBUG;_CONSOLE;%(PreprocessorDefinitions) true - ..\external\VRto3DLib\include;..\external\json\include;..\external\openvr\headers;..\utils\vrmath + src;..\external\VRto3DLib\include;..\external\json\include;..\external\openvr\headers;..\external\SR-lib\SR-SDK-1.34.10\x64\include;..\external\SR-lib\SR-SDK-1.34.10\x64\third_party\OpenCV\include;..\external\nvapi;..\external\adl_sdk\include;..\external\imgui;..\external\imgui\backends;..\external\NV3D-Lib\NV3DLib\include;..\utils\vrmath stdcpp17 4244;4305 Console true - openvr_api.lib;%(AdditionalDependencies) - ..\external\openvr\lib\win$(PlatformArchitecture) + openvr_api.lib;d3d11.lib;dxgi.lib;dxguid.lib;d3dcompiler.lib;d3d9.lib;ws2_32.lib;simulatedreality.lib;SimulatedRealityCore.lib;SimulatedRealityDirectX.lib;SimulatedRealityDisplays.lib;SimulatedRealityFaceTrackers.lib;DimencoWeaving.lib;opencv_core343.lib;NV3DLib-mdd.lib;nvapi64.lib;delayimp.lib;%(AdditionalDependencies) + simulatedreality.dll;SimulatedRealityCore.dll;SimulatedRealityDirectX.dll;SimulatedRealityDisplays.dll;SimulatedRealityFaceTrackers.dll;DimencoWeaving.dll;opencv_core343.dll;nvapi64.dll;openvr_api.dll;%(DelayLoadDLLs) + ..\external\openvr\lib\win$(PlatformArchitecture);..\external\SR-lib\SR-SDK-1.34.10\x64\lib;..\external\SR-lib\SR-SDK-1.34.10\x64\third_party\OpenCV\lib\x64;..\external\nvapi\amd64;..\external\NV3D-Lib\NV3DLib\lib\x$(PlatformArchitecture)\$(Configuration)-MD + + + powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(ProjectDir)..\external\NV3D-Lib\tools\build.ps1" -Configuration $(Configuration)-MD -Platform $(PlatformShortName) -Target NV3DLib + XCOPY "$(ProjectDir)$(ProjectName)" "$(SolutionDir)output\drivers\$(ProjectName)" /S /Y /I + COPY /Y "$(ProjectDir)..\external\openvr\bin\win$(PlatformArchitecture)\openvr_api.dll" "$(SolutionDir)output\drivers\$(ProjectName)\bin\win$(PlatformArchitecture)\openvr_api.dll" XCOPY "$(SolutionDir)output\drivers\" "$(STEAM_PATH)\steamapps\common\SteamVR\drivers\" /S /Y /I - + @@ -173,7 +200,7 @@ true NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true - ..\external\VRto3DLib\include;..\external\json\include;..\external\openvr\headers;..\utils\vrmath + src;..\external\VRto3DLib\include;..\external\json\include;..\external\openvr\headers;..\external\SR-lib\SR-SDK-1.34.10\x64\include;..\external\SR-lib\SR-SDK-1.34.10\x64\third_party\OpenCV\include;..\external\nvapi;..\external\adl_sdk\include;..\external\imgui;..\external\imgui\backends;..\external\NV3D-Lib\NV3DLib\include;..\utils\vrmath stdcpp17 4244;4305 @@ -182,24 +209,70 @@ true true true - openvr_api.lib;%(AdditionalDependencies) - ..\external\openvr\lib\win$(PlatformArchitecture) + openvr_api.lib;d3d11.lib;dxgi.lib;dxguid.lib;d3dcompiler.lib;d3d9.lib;ws2_32.lib;simulatedreality.lib;SimulatedRealityCore.lib;SimulatedRealityDirectX.lib;SimulatedRealityDisplays.lib;SimulatedRealityFaceTrackers.lib;DimencoWeaving.lib;opencv_core343.lib;NV3DLib-md.lib;nvapi64.lib;delayimp.lib;%(AdditionalDependencies) + simulatedreality.dll;SimulatedRealityCore.dll;SimulatedRealityDirectX.dll;SimulatedRealityDisplays.dll;SimulatedRealityFaceTrackers.dll;DimencoWeaving.dll;opencv_core343.dll;nvapi64.dll;openvr_api.dll;%(DelayLoadDLLs) + ..\external\openvr\lib\win$(PlatformArchitecture);..\external\SR-lib\SR-SDK-1.34.10\x64\lib;..\external\SR-lib\SR-SDK-1.34.10\x64\third_party\OpenCV\lib\x64;..\external\nvapi\amd64;..\external\NV3D-Lib\NV3DLib\lib\x$(PlatformArchitecture)\$(Configuration)-MD + + + powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(ProjectDir)..\external\NV3D-Lib\tools\build.ps1" -Configuration $(Configuration)-MD -Platform $(PlatformShortName) -Target NV3DLib + XCOPY "$(ProjectDir)$(ProjectName)" "$(SolutionDir)output\drivers\$(ProjectName)" /S /Y /I + COPY /Y "$(ProjectDir)..\external\openvr\bin\win$(PlatformArchitecture)\openvr_api.dll" "$(SolutionDir)output\drivers\$(ProjectName)\bin\win$(PlatformArchitecture)\openvr_api.dll" XCOPY "$(SolutionDir)output\drivers\" "$(STEAM_PATH)\steamapps\common\SteamVR\drivers\" /S /Y /I - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vrto3d/vrto3d/driver.vrdrivermanifest b/vrto3d/vrto3d/driver.vrdrivermanifest index 94ae5e9c..a6a26b37 100644 --- a/vrto3d/vrto3d/driver.vrdrivermanifest +++ b/vrto3d/vrto3d/driver.vrdrivermanifest @@ -3,6 +3,7 @@ "name" : "vrto3d", "directory" : "", "resourceOnly" : false, + "redirectsDisplay" : true, "hmd_presence" : [ "*.*" diff --git a/vrto3d/vrto3d/resources/nvtimings.json b/vrto3d/vrto3d/resources/nvtimings.json new file mode 100644 index 00000000..c78b124d --- /dev/null +++ b/vrto3d/vrto3d/resources/nvtimings.json @@ -0,0 +1,11192 @@ +{ + "ACI_23F7_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7489.33, + "x": 3703.25, + "y": 3468.0 + } + }, + "ACI_23F7_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6599.75, + "x": 3200.0, + "y": 3021.25 + } + }, + "ACI_23F7_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5854.58, + "x": 3064.5, + "y": 2375.25 + } + }, + "ACI_24A0_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACI_24A0_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACI_24A4_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACI_24A4_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACI_24AE_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACI_24AE_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACI_24E1_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACI_24E1_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACI_2725_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7333.33 + } + }, + "ACI_2725_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACI_2758_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7489.33, + "x": 3703.25, + "y": 3468.0 + } + }, + "ACI_2758_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6599.75, + "x": 3200.0, + "y": 3021.25 + } + }, + "ACI_2758_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5854.58, + "x": 3064.5, + "y": 2375.25 + } + }, + "ACI_27B1_120": { + "monitor_timings": { + "frequency_10khz": 49938, + "refresh_hz": 119.997, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1530, + "visible": 1440, + "numBlanking": 90, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 82 + } + }, + "glasses_timings": { + "z": 8333.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACI_27B1_100": { + "monitor_timings": { + "frequency_10khz": 41186, + "refresh_hz": 99.947, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1515, + "visible": 1440, + "numBlanking": 75, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 67 + } + }, + "glasses_timings": { + "z": 10005.33, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACI_27E2_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7333.33 + } + }, + "ACI_27E2_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9166.67, + "w": 5096.75, + "x": 100.0, + "y": 8048.25 + } + }, + "ACI_27E2_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACI_27E3_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7333.33 + } + }, + "ACI_27E3_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9166.67, + "w": 5096.75, + "x": 100.0, + "y": 8048.25 + } + }, + "ACI_27E3_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACI_27F3_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5800.25, + "x": 2867.25, + "y": 2649.5 + } + }, + "ACI_27F3_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6599.75, + "x": 3200.0, + "y": 3124.25 + } + }, + "ACI_27F3_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7488.75, + "x": 3703.25, + "y": 3571.0 + } + }, + "ACI_27F8_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7333.33 + } + }, + "ACI_27F8_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9166.67, + "w": 5096.75, + "x": 100.0, + "y": 8048.25 + } + }, + "ACI_27F8_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACI_E300_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 6900.08, + "x": 4000.0, + "y": 3300.25 + } + }, + "ACI_E300_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6000.08, + "x": 3500.25, + "y": 3000.25 + } + }, + "ACI_E300_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5600.0, + "x": 2900.0, + "y": 2599.5 + } + }, + "ACI_E327_120": { + "monitor_timings": { + "frequency_10khz": 28679, + "refresh_hz": 120.0, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4846.58, + "x": 3064.5, + "y": 4166.0 + } + }, + "ACI_E327_110": { + "monitor_timings": { + "frequency_10khz": 26152, + "refresh_hz": 110.001, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9090.83, + "w": 5836.75, + "x": 3200.0, + "y": 4548.25 + } + }, + "ACI_E327_100": { + "monitor_timings": { + "frequency_10khz": 23670, + "refresh_hz": 99.998, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.17, + "w": 6514.33, + "x": 3703.25, + "y": 5000.0 + } + }, + "ACR_0102_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 6900.08, + "x": 4000.0, + "y": 3300.25 + } + }, + "ACR_0102_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6000.08, + "x": 3500.25, + "y": 3000.25 + } + }, + "ACR_0102_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5600.0, + "x": 2900.0, + "y": 2599.5 + } + }, + "ACR_0125_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5800.25, + "x": 2867.25, + "y": 2649.5 + } + }, + "ACR_0125_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6599.75, + "x": 3200.0, + "y": 3124.25 + } + }, + "ACR_0125_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7488.75, + "x": 3703.25, + "y": 3571.0 + } + }, + "ACR_01D1_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7489.33, + "x": 3703.25, + "y": 3468.0 + } + }, + "ACR_01D1_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6599.75, + "x": 3200.0, + "y": 3021.25 + } + }, + "ACR_01D1_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5854.58, + "x": 3064.5, + "y": 2375.25 + } + }, + "ACR_0201_120": { + "monitor_timings": { + "frequency_10khz": 7325, + "refresh_hz": 119.972, + "hor": { + "total": 960, + "visible": 800, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 636, + "visible": 600, + "numBlanking": 36, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 29 + } + }, + "glasses_timings": { + "z": 8335.25, + "w": 4430.42, + "x": 2867.5, + "y": 340.25 + } + }, + "ACR_020D_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 6900.08, + "x": 4000.0, + "y": 3300.25 + } + }, + "ACR_020D_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6000.08, + "x": 3500.25, + "y": 3000.25 + } + }, + "ACR_020D_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5600.0, + "x": 2900.0, + "y": 2599.5 + } + }, + "ACR_024C_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5770.0, + "x": 2867.25, + "y": 2649.5 + } + }, + "ACR_024C_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6499.0, + "x": 3200.0, + "y": 3124.25 + } + }, + "ACR_024C_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7128.0, + "x": 3703.25, + "y": 3571.0 + } + }, + "ACR_0255_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5854.58, + "x": 3064.5, + "y": 2150.0 + } + }, + "ACR_0255_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6599.75, + "x": 3200.0, + "y": 3021.25 + } + }, + "ACR_0255_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7489.33, + "x": 3703.25, + "y": 3050.0 + } + }, + "ACR_028B_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 5925.25, + "x": 2867.25, + "y": 2649.5 + } + }, + "ACR_028B_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9090.83, + "w": 6664.75, + "x": 3200.0, + "y": 3124.25 + } + }, + "ACR_028B_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 7394.75, + "x": 3703.25, + "y": 3571.0 + } + }, + "ACR_02AB_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5249.0, + "x": 3064.5, + "y": 2150.0 + } + }, + "ACR_02AB_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 5999.0, + "x": 3200.0, + "y": 3021.25 + } + }, + "ACR_02AB_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 6889.0, + "x": 3703.25, + "y": 3050.0 + } + }, + "ACR_02B4_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7333.33 + } + }, + "ACR_02B4_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9166.67, + "w": 5096.75, + "x": 100.0, + "y": 8048.25 + } + }, + "ACR_02B4_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 5754.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_02F9_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_02F9_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 4946.75, + "x": 550.0, + "y": 8000.25 + } + }, + "ACR_02F9_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_02FD_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_02FD_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 4946.75, + "x": 550.0, + "y": 8000.25 + } + }, + "ACR_02FD_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_03f5_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7333.33 + } + }, + "ACR_03f5_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_03f7_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7333.33 + } + }, + "ACR_03f7_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_0403_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7333.33 + } + }, + "ACR_0403_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_0405_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7333.33 + } + }, + "ACR_0405_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_0406_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_0406_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_0407_120": { + "monitor_timings": { + "frequency_10khz": 49938, + "refresh_hz": 119.997, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1530, + "visible": 1440, + "numBlanking": 90, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 82 + } + }, + "glasses_timings": { + "z": 8333.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_0407_100": { + "monitor_timings": { + "frequency_10khz": 41186, + "refresh_hz": 99.947, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1515, + "visible": 1440, + "numBlanking": 75, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 67 + } + }, + "glasses_timings": { + "z": 10005.33, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_0419_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_0419_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_041B_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_041B_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_041C_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_041C_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_0463_120": { + "monitor_timings": { + "frequency_10khz": 49938, + "refresh_hz": 119.997, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1530, + "visible": 1440, + "numBlanking": 90, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 82 + } + }, + "glasses_timings": { + "z": 8333.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_0463_100": { + "monitor_timings": { + "frequency_10khz": 41186, + "refresh_hz": 99.947, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1515, + "visible": 1440, + "numBlanking": 75, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 67 + } + }, + "glasses_timings": { + "z": 10005.33, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_0489_120": { + "monitor_timings": { + "frequency_10khz": 49938, + "refresh_hz": 119.997, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1530, + "visible": 1440, + "numBlanking": 90, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 82 + } + }, + "glasses_timings": { + "z": 8333.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_0489_100": { + "monitor_timings": { + "frequency_10khz": 41186, + "refresh_hz": 99.947, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1515, + "visible": 1440, + "numBlanking": 75, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 67 + } + }, + "glasses_timings": { + "z": 10005.33, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_050E_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_050E_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_0510_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_0510_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_0526_120": { + "monitor_timings": { + "frequency_10khz": 49938, + "refresh_hz": 119.997, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1530, + "visible": 1440, + "numBlanking": 90, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 82 + } + }, + "glasses_timings": { + "z": 8333.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_0526_100": { + "monitor_timings": { + "frequency_10khz": 41186, + "refresh_hz": 99.947, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1515, + "visible": 1440, + "numBlanking": 75, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 67 + } + }, + "glasses_timings": { + "z": 10005.33, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_052C_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_052C_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_052F_120": { + "monitor_timings": { + "frequency_10khz": 49938, + "refresh_hz": 119.997, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1530, + "visible": 1440, + "numBlanking": 90, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 82 + } + }, + "glasses_timings": { + "z": 8333.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_052F_100": { + "monitor_timings": { + "frequency_10khz": 41186, + "refresh_hz": 99.947, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1515, + "visible": 1440, + "numBlanking": 75, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 67 + } + }, + "glasses_timings": { + "z": 10005.33, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_0563_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5451.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_0563_144": { + "monitor_timings": { + "frequency_10khz": 34800, + "refresh_hz": 143.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1162, + "visible": 1080, + "numBlanking": 82, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 74 + } + }, + "glasses_timings": { + "z": 6945.25, + "w": 4153.33, + "x": 16.67, + "y": 5945.0 + } + }, + "ACR_0585_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5451.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_0585_144": { + "monitor_timings": { + "frequency_10khz": 34800, + "refresh_hz": 143.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1162, + "visible": 1080, + "numBlanking": 82, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 74 + } + }, + "glasses_timings": { + "z": 6945.25, + "w": 4153.33, + "x": 16.67, + "y": 5945.0 + } + }, + "ACR_0605_120": { + "monitor_timings": { + "frequency_10khz": 11550, + "refresh_hz": 119.989, + "hor": { + "total": 1184, + "visible": 1024, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 813, + "visible": 768, + "numBlanking": 45, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 38 + } + }, + "glasses_timings": { + "z": 8334.17, + "w": 4500.17, + "x": 2867.0, + "y": 290.0 + } + }, + "ACR_0616_120": { + "monitor_timings": { + "frequency_10khz": 49938, + "refresh_hz": 119.997, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1530, + "visible": 1440, + "numBlanking": 90, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 82 + } + }, + "glasses_timings": { + "z": 8333.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_0616_100": { + "monitor_timings": { + "frequency_10khz": 41186, + "refresh_hz": 99.947, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1515, + "visible": 1440, + "numBlanking": 75, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 67 + } + }, + "glasses_timings": { + "z": 10005.33, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_0637_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_0637_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_06D0_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_06D0_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "ACR_06D2_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5451.58, + "x": 0.5, + "y": 7334.0 + } + }, + "ACR_06D2_144": { + "monitor_timings": { + "frequency_10khz": 34800, + "refresh_hz": 143.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1162, + "visible": 1080, + "numBlanking": 82, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 74 + } + }, + "glasses_timings": { + "z": 6945.25, + "w": 4153.33, + "x": 16.67, + "y": 5945.0 + } + }, + "ACR_0701_120": { + "monitor_timings": { + "frequency_10khz": 11550, + "refresh_hz": 119.989, + "hor": { + "total": 1184, + "visible": 1024, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 813, + "visible": 768, + "numBlanking": 45, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 38 + } + }, + "glasses_timings": { + "z": 8333.58, + "w": 4600.25, + "x": 2724.75, + "y": 290.0 + } + }, + "ACR_1131_120": { + "monitor_timings": { + "frequency_10khz": 11550, + "refresh_hz": 119.989, + "hor": { + "total": 1184, + "visible": 1024, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 813, + "visible": 768, + "numBlanking": 45, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 38 + } + }, + "glasses_timings": { + "z": 8334.17, + "w": 4649.67, + "x": 2867.0, + "y": 290.0 + } + }, + "ACR_3005_120": { + "monitor_timings": { + "frequency_10khz": 7325, + "refresh_hz": 119.972, + "hor": { + "total": 960, + "visible": 800, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 636, + "visible": 600, + "numBlanking": 36, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 29 + } + }, + "glasses_timings": { + "z": 8335.25, + "w": 4500.17, + "x": 2866.75, + "y": 379.75 + } + }, + "ACR_D005_120": { + "monitor_timings": { + "frequency_10khz": 11550, + "refresh_hz": 119.989, + "hor": { + "total": 1184, + "visible": 1024, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 813, + "visible": 768, + "numBlanking": 45, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 38 + } + }, + "glasses_timings": { + "z": 8334.17, + "w": 4629.67, + "x": 2868.0, + "y": 379.75 + } + }, + "ACR_F230_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 6900.08, + "x": 4000.0, + "y": 3300.25 + } + }, + "ACR_F230_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6000.08, + "x": 3500.25, + "y": 3000.25 + } + }, + "ACR_F230_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5600.0, + "x": 2900.0, + "y": 2599.5 + } + }, + "ACR_F231_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7100.0, + "x": 3703.25, + "y": 3468.0 + } + }, + "ACR_F231_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6599.75, + "x": 3200.0, + "y": 3021.25 + } + }, + "ACR_F231_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5804.58, + "x": 3064.5, + "y": 2325.25 + } + }, + "AOC_2460_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "AOC_2460_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "AOC_2738_120": { + "monitor_timings": { + "frequency_10khz": 49938, + "refresh_hz": 119.997, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1530, + "visible": 1440, + "numBlanking": 90, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 82 + } + }, + "glasses_timings": { + "z": 8333.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "AOC_2738_100": { + "monitor_timings": { + "frequency_10khz": 41186, + "refresh_hz": 99.947, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1515, + "visible": 1440, + "numBlanking": 75, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 67 + } + }, + "glasses_timings": { + "z": 10005.33, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "AUS_24B1_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "AUS_24B1_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "AUS_25B1_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5451.58, + "x": 0.5, + "y": 7334.0 + } + }, + "AUS_25B1_144": { + "monitor_timings": { + "frequency_10khz": 34800, + "refresh_hz": 143.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1162, + "visible": 1080, + "numBlanking": 82, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 74 + } + }, + "glasses_timings": { + "z": 6945.25, + "w": 4153.33, + "x": 16.67, + "y": 5945.0 + } + }, + "AUS_27B1_120": { + "monitor_timings": { + "frequency_10khz": 49938, + "refresh_hz": 119.997, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1530, + "visible": 1440, + "numBlanking": 90, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 82 + } + }, + "glasses_timings": { + "z": 8333.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "AUS_27B1_100": { + "monitor_timings": { + "frequency_10khz": 41186, + "refresh_hz": 99.947, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1515, + "visible": 1440, + "numBlanking": 75, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 67 + } + }, + "glasses_timings": { + "z": 10005.33, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "AUS_27B4_120": { + "monitor_timings": { + "frequency_10khz": 49938, + "refresh_hz": 119.997, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1530, + "visible": 1440, + "numBlanking": 90, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 82 + } + }, + "glasses_timings": { + "z": 8333.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "AUS_27B4_100": { + "monitor_timings": { + "frequency_10khz": 41186, + "refresh_hz": 99.947, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1515, + "visible": 1440, + "numBlanking": 75, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 67 + } + }, + "glasses_timings": { + "z": 10005.33, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_0A02_120": { + "monitor_timings": { + "frequency_10khz": 16156, + "refresh_hz": 119.997, + "hor": { + "total": 1744, + "visible": 1280, + "numBlanking": 464, + "numSync": 136, + "border": 0, + "frontPorch": 96, + "backPorch": 232 + }, + "ver": { + "total": 772, + "visible": 720, + "numBlanking": 52, + "numSync": 3, + "border": 0, + "frontPorch": 1, + "backPorch": 48 + } + }, + "glasses_timings": { + "z": 8333.58, + "w": 4840.25, + "x": 2867.75, + "y": 290.0 + } + }, + "BNQ_0F02_120": { + "monitor_timings": { + "frequency_10khz": 16156, + "refresh_hz": 119.997, + "hor": { + "total": 1744, + "visible": 1280, + "numBlanking": 464, + "numSync": 136, + "border": 0, + "frontPorch": 96, + "backPorch": 232 + }, + "ver": { + "total": 772, + "visible": 720, + "numBlanking": 52, + "numSync": 3, + "border": 0, + "frontPorch": 1, + "backPorch": 48 + } + }, + "glasses_timings": { + "z": 8333.58, + "w": 5050.0, + "x": 2724.75, + "y": 290.0 + } + }, + "BNQ_4502_120": { + "monitor_timings": { + "frequency_10khz": 14625, + "refresh_hz": 119.909, + "hor": { + "total": 1440, + "visible": 1280, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 847, + "visible": 800, + "numBlanking": 47, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 38 + } + }, + "glasses_timings": { + "z": 8339.67, + "w": 4499.17, + "x": 3090.0, + "y": 500.25 + } + }, + "BNQ_7F02_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 6900.08, + "x": 4000.0, + "y": 3300.25 + } + }, + "BNQ_7F02_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6000.08, + "x": 3500.25, + "y": 3000.25 + } + }, + "BNQ_7F02_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5800.25, + "x": 2900.0, + "y": 2599.5 + } + }, + "BNQ_7F04_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "BNQ_7F04_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7F06_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "BNQ_7F06_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7F08_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "BNQ_7F08_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7F0A_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "BNQ_7F0A_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7F10_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "BNQ_7F10_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7f13_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7333.33 + } + }, + "BNQ_7f13_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9166.67, + "w": 5096.75, + "x": 100.0, + "y": 8048.25 + } + }, + "BNQ_7f13_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7f15_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7333.33 + } + }, + "BNQ_7f15_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9166.67, + "w": 5096.75, + "x": 100.0, + "y": 8048.25 + } + }, + "BNQ_7f15_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7f17_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7333.33 + } + }, + "BNQ_7f17_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9166.67, + "w": 5096.75, + "x": 100.0, + "y": 8048.25 + } + }, + "BNQ_7f17_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7f19_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7333.33 + } + }, + "BNQ_7f19_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9166.67, + "w": 5096.75, + "x": 100.0, + "y": 8048.25 + } + }, + "BNQ_7f19_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7F1E_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "BNQ_7F1E_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7F21_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "BNQ_7F21_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7F23_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "BNQ_7F23_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7F25_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "BNQ_7F25_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7F27_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "BNQ_7F27_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7f29_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7333.33 + } + }, + "BNQ_7f29_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7f2b_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7333.33 + } + }, + "BNQ_7f2b_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7F2D_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "BNQ_7F2D_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7F2F_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "BNQ_7F2F_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7F31_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "BNQ_7F31_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7F33_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "BNQ_7F33_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7F39_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "BNQ_7F39_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_7F3C_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "BNQ_7F3C_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "BNQ_F101_120": { + "monitor_timings": { + "frequency_10khz": 11550, + "refresh_hz": 119.989, + "hor": { + "total": 1184, + "visible": 1024, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 813, + "visible": 768, + "numBlanking": 45, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 38 + } + }, + "glasses_timings": { + "z": 8334.17, + "w": 4527.58, + "x": 2868.0, + "y": 500.0 + } + }, + "BOE_0593_120": { + "monitor_timings": { + "frequency_10khz": 19263, + "refresh_hz": 120.001, + "hor": { + "total": 1470, + "visible": 1366, + "numBlanking": 104, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 64 + }, + "ver": { + "total": 1092, + "visible": 768, + "numBlanking": 324, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 315 + } + }, + "glasses_timings": { + "z": 8333.25, + "w": 5600.0, + "x": 2865.0, + "y": 2475.25 + } + }, + "BOE_0593_110": { + "monitor_timings": { + "frequency_10khz": 19242, + "refresh_hz": 109.998, + "hor": { + "total": 1470, + "visible": 1366, + "numBlanking": 104, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 64 + }, + "ver": { + "total": 1190, + "visible": 768, + "numBlanking": 422, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 413 + } + }, + "glasses_timings": { + "z": 9091.08, + "w": 6300.0, + "x": 3200.0, + "y": 3021.25 + } + }, + "BOE_0593_100": { + "monitor_timings": { + "frequency_10khz": 19228, + "refresh_hz": 100.002, + "hor": { + "total": 1470, + "visible": 1366, + "numBlanking": 104, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 64 + }, + "ver": { + "total": 1308, + "visible": 768, + "numBlanking": 540, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 531 + } + }, + "glasses_timings": { + "z": 9999.83, + "w": 7100.0, + "x": 3403.0, + "y": 3468.0 + } + }, + "CAI_BB15_120": { + "monitor_timings": { + "frequency_10khz": 16156, + "refresh_hz": 119.997, + "hor": { + "total": 1744, + "visible": 1280, + "numBlanking": 464, + "numSync": 136, + "border": 0, + "frontPorch": 96, + "backPorch": 232 + }, + "ver": { + "total": 772, + "visible": 720, + "numBlanking": 52, + "numSync": 3, + "border": 0, + "frontPorch": 1, + "backPorch": 48 + } + }, + "glasses_timings": { + "z": 8333.58, + "w": 4899.58, + "x": 2867.75, + "y": 500.0 + } + }, + "CAI_BB16_120": { + "monitor_timings": { + "frequency_10khz": 14850, + "refresh_hz": 120.0, + "hor": { + "total": 1650, + "visible": 1280, + "numBlanking": 370, + "numSync": 40, + "border": 0, + "frontPorch": 110, + "backPorch": 220 + }, + "ver": { + "total": 750, + "visible": 720, + "numBlanking": 30, + "numSync": 5, + "border": 0, + "frontPorch": 5, + "backPorch": 20 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4960.0, + "x": 2884.5, + "y": 290.0 + } + }, + "CMO_1590_120": { + "monitor_timings": { + "frequency_10khz": 14249, + "refresh_hz": 119.965, + "hor": { + "total": 1470, + "visible": 1366, + "numBlanking": 104, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 64 + }, + "ver": { + "total": 808, + "visible": 768, + "numBlanking": 40, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 31 + } + }, + "glasses_timings": { + "z": 8335.75, + "w": 5600.0, + "x": 2867.75, + "y": 2599.75 + } + }, + "CMO_1590_110": { + "monitor_timings": { + "frequency_10khz": 13057, + "refresh_hz": 109.93, + "hor": { + "total": 1470, + "visible": 1366, + "numBlanking": 104, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 64 + }, + "ver": { + "total": 808, + "visible": 768, + "numBlanking": 40, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 31 + } + }, + "glasses_timings": { + "z": 9096.75, + "w": 6099.58, + "x": 3247.25, + "y": 3500.25 + } + }, + "CMO_1590_100": { + "monitor_timings": { + "frequency_10khz": 11879, + "refresh_hz": 100.012, + "hor": { + "total": 1470, + "visible": 1366, + "numBlanking": 104, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 64 + }, + "ver": { + "total": 808, + "visible": 768, + "numBlanking": 40, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 31 + } + }, + "glasses_timings": { + "z": 9998.83, + "w": 6499.67, + "x": 3000.25, + "y": 5900.25 + } + }, + "CMO_1598_120": { + "monitor_timings": { + "frequency_10khz": 15181, + "refresh_hz": 119.993, + "hor": { + "total": 1560, + "visible": 1366, + "numBlanking": 194, + "numSync": 65, + "border": 0, + "frontPorch": 31, + "backPorch": 98 + }, + "ver": { + "total": 811, + "visible": 768, + "numBlanking": 43, + "numSync": 12, + "border": 0, + "frontPorch": 4, + "backPorch": 27 + } + }, + "glasses_timings": { + "z": 8333.83, + "w": 5700.17, + "x": 2867.75, + "y": 2399.5 + } + }, + "CMO_1598_110": { + "monitor_timings": { + "frequency_10khz": 13915, + "refresh_hz": 109.986, + "hor": { + "total": 1560, + "visible": 1366, + "numBlanking": 194, + "numSync": 65, + "border": 0, + "frontPorch": 31, + "backPorch": 98 + }, + "ver": { + "total": 811, + "visible": 768, + "numBlanking": 43, + "numSync": 12, + "border": 0, + "frontPorch": 4, + "backPorch": 27 + } + }, + "glasses_timings": { + "z": 9092.08, + "w": 6099.58, + "x": 3247.0, + "y": 3149.75 + } + }, + "CMO_1598_100": { + "monitor_timings": { + "frequency_10khz": 12650, + "refresh_hz": 99.987, + "hor": { + "total": 1560, + "visible": 1366, + "numBlanking": 194, + "numSync": 65, + "border": 0, + "frontPorch": 31, + "backPorch": 98 + }, + "ver": { + "total": 811, + "visible": 768, + "numBlanking": 43, + "numSync": 12, + "border": 0, + "frontPorch": 4, + "backPorch": 27 + } + }, + "glasses_timings": { + "z": 10001.25, + "w": 7149.67, + "x": 2999.75, + "y": 4199.75 + } + }, + "CMO_1723_120": { + "monitor_timings": { + "frequency_10khz": 39636, + "refresh_hz": 119.999, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1588, + "visible": 1080, + "numBlanking": 508, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 500 + } + }, + "glasses_timings": { + "z": 8333.42, + "w": 4054.0, + "x": 2465.0, + "y": 5075.0 + } + }, + "CMO_1723_110": { + "monitor_timings": { + "frequency_10khz": 39636, + "refresh_hz": 110.022, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1732, + "visible": 1080, + "numBlanking": 652, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 644 + } + }, + "glasses_timings": { + "z": 9089.08, + "w": 5099.83, + "x": 3200.0, + "y": 4920.5 + } + }, + "CMO_1723_100": { + "monitor_timings": { + "frequency_10khz": 39636, + "refresh_hz": 100.03, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1905, + "visible": 1080, + "numBlanking": 825, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 817 + } + }, + "glasses_timings": { + "z": 9997.0, + "w": 5989.08, + "x": 3403.0, + "y": 5368.25 + } + }, + "CMO_FFFF_100": { + "monitor_timings": { + "frequency_10khz": 35000, + "refresh_hz": 100.0, + "hor": { + "total": 2000, + "visible": 1920, + "numBlanking": 80, + "numSync": 44, + "border": 0, + "frontPorch": 10, + "backPorch": 26 + }, + "ver": { + "total": 1750, + "visible": 1080, + "numBlanking": 670, + "numSync": 5, + "border": 0, + "frontPorch": 4, + "backPorch": 661 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 6802.0, + "x": 3700.0, + "y": 3708.5 + } + }, + "CMO_FFFF_110": { + "monitor_timings": { + "frequency_10khz": 38474, + "refresh_hz": 109.926, + "hor": { + "total": 2000, + "visible": 1920, + "numBlanking": 80, + "numSync": 44, + "border": 0, + "frontPorch": 10, + "backPorch": 26 + }, + "ver": { + "total": 1750, + "visible": 1080, + "numBlanking": 670, + "numSync": 5, + "border": 0, + "frontPorch": 4, + "backPorch": 661 + } + }, + "glasses_timings": { + "z": 9097.08, + "w": 6320.75, + "x": 3248.5, + "y": 3158.75 + } + }, + "CMO_FFFF_120": { + "monitor_timings": { + "frequency_10khz": 38474, + "refresh_hz": 119.931, + "hor": { + "total": 2000, + "visible": 1920, + "numBlanking": 80, + "numSync": 44, + "border": 0, + "frontPorch": 10, + "backPorch": 26 + }, + "ver": { + "total": 1604, + "visible": 1080, + "numBlanking": 524, + "numSync": 5, + "border": 0, + "frontPorch": 4, + "backPorch": 515 + } + }, + "glasses_timings": { + "z": 8338.08, + "w": 5908.33, + "x": 2869.0, + "y": 2437.0 + } + }, + "CPT_14B6_110": { + "monitor_timings": { + "frequency_10khz": 13060, + "refresh_hz": 109.955, + "hor": { + "total": 1470, + "visible": 1366, + "numBlanking": 104, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 64 + }, + "ver": { + "total": 808, + "visible": 768, + "numBlanking": 40, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 31 + } + }, + "glasses_timings": { + "z": 9094.67, + "w": 6250.08, + "x": 3247.25, + "y": 3100.0 + } + }, + "CPT_14B6_120": { + "monitor_timings": { + "frequency_10khz": 14250, + "refresh_hz": 119.974, + "hor": { + "total": 1470, + "visible": 1366, + "numBlanking": 104, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 64 + }, + "ver": { + "total": 808, + "visible": 768, + "numBlanking": 40, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 31 + } + }, + "glasses_timings": { + "z": 8335.17, + "w": 5749.5, + "x": 2867.5, + "y": 2400.0 + } + }, + "CPT_14B6_100": { + "monitor_timings": { + "frequency_10khz": 11880, + "refresh_hz": 100.02, + "hor": { + "total": 1470, + "visible": 1366, + "numBlanking": 104, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 64 + }, + "ver": { + "total": 808, + "visible": 768, + "numBlanking": 40, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 31 + } + }, + "glasses_timings": { + "z": 9998.0, + "w": 6900.08, + "x": 3699.0, + "y": 4199.75 + } + }, + "CPT_14BF_110": { + "monitor_timings": { + "frequency_10khz": 13060, + "refresh_hz": 109.955, + "hor": { + "total": 1470, + "visible": 1366, + "numBlanking": 104, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 64 + }, + "ver": { + "total": 808, + "visible": 768, + "numBlanking": 40, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 31 + } + }, + "glasses_timings": { + "z": 9094.67, + "w": 6250.08, + "x": 3247.25, + "y": 3100.0 + } + }, + "CPT_14BF_120": { + "monitor_timings": { + "frequency_10khz": 14250, + "refresh_hz": 119.974, + "hor": { + "total": 1470, + "visible": 1366, + "numBlanking": 104, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 64 + }, + "ver": { + "total": 808, + "visible": 768, + "numBlanking": 40, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 31 + } + }, + "glasses_timings": { + "z": 8335.17, + "w": 5749.5, + "x": 2867.5, + "y": 2400.0 + } + }, + "CPT_14BF_100": { + "monitor_timings": { + "frequency_10khz": 11880, + "refresh_hz": 100.02, + "hor": { + "total": 1470, + "visible": 1366, + "numBlanking": 104, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 64 + }, + "ver": { + "total": 808, + "visible": 768, + "numBlanking": 40, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 31 + } + }, + "glasses_timings": { + "z": 9998.0, + "w": 6900.08, + "x": 3699.0, + "y": 4199.75 + } + }, + "CPT_157D_120": { + "monitor_timings": { + "frequency_10khz": 19263, + "refresh_hz": 120.001, + "hor": { + "total": 1470, + "visible": 1366, + "numBlanking": 104, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 64 + }, + "ver": { + "total": 1092, + "visible": 768, + "numBlanking": 324, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 315 + } + }, + "glasses_timings": { + "z": 8333.25, + "w": 4249.58, + "x": 2867.5, + "y": 4199.75 + } + }, + "CPT_157D_110": { + "monitor_timings": { + "frequency_10khz": 19242, + "refresh_hz": 109.998, + "hor": { + "total": 1470, + "visible": 1366, + "numBlanking": 104, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 64 + }, + "ver": { + "total": 1190, + "visible": 768, + "numBlanking": 422, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 413 + } + }, + "glasses_timings": { + "z": 9091.08, + "w": 4750.17, + "x": 3246.5, + "y": 5000.25 + } + }, + "CPT_157D_100": { + "monitor_timings": { + "frequency_10khz": 19228, + "refresh_hz": 100.002, + "hor": { + "total": 1470, + "visible": 1366, + "numBlanking": 104, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 64 + }, + "ver": { + "total": 1308, + "visible": 768, + "numBlanking": 540, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 531 + } + }, + "glasses_timings": { + "z": 9999.83, + "w": 5399.83, + "x": 3699.75, + "y": 5700.0 + } + }, + "CPT_2329_120": { + "monitor_timings": { + "frequency_10khz": 39636, + "refresh_hz": 119.999, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1588, + "visible": 1080, + "numBlanking": 508, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 500 + } + }, + "glasses_timings": { + "z": 8333.42, + "w": 2000.0, + "x": 2000.0, + "y": 1000.0 + } + }, + "CPT_2329_110": { + "monitor_timings": { + "frequency_10khz": 39636, + "refresh_hz": 110.022, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1732, + "visible": 1080, + "numBlanking": 652, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 644 + } + }, + "glasses_timings": { + "z": 9089.08, + "w": 2500.0, + "x": 2500.0, + "y": 1000.0 + } + }, + "CPT_2329_100": { + "monitor_timings": { + "frequency_10khz": 39636, + "refresh_hz": 100.03, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1905, + "visible": 1080, + "numBlanking": 825, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 817 + } + }, + "glasses_timings": { + "z": 9997.0, + "w": 3000.0, + "x": 3000.0, + "y": 1000.0 + } + }, + "DEL_4051_120": { + "monitor_timings": { + "frequency_10khz": 38493, + "refresh_hz": 120.0, + "hor": { + "total": 2020, + "visible": 1920, + "numBlanking": 100, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 60 + }, + "ver": { + "total": 1588, + "visible": 1080, + "numBlanking": 508, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 500 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 5900.33, + "x": 2952.5, + "y": 2349.75 + } + }, + "DEL_4051_110": { + "monitor_timings": { + "frequency_10khz": 38485, + "refresh_hz": 110.0, + "hor": { + "total": 2020, + "visible": 1920, + "numBlanking": 100, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 60 + }, + "ver": { + "total": 1732, + "visible": 1080, + "numBlanking": 652, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 644 + } + }, + "glasses_timings": { + "z": 9090.92, + "w": 5908.33, + "x": 4121.0, + "y": 2699.75 + } + }, + "DEL_4051_100": { + "monitor_timings": { + "frequency_10khz": 38501, + "refresh_hz": 99.999, + "hor": { + "total": 2020, + "visible": 1920, + "numBlanking": 100, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 60 + }, + "ver": { + "total": 1906, + "visible": 1080, + "numBlanking": 826, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 818 + } + }, + "glasses_timings": { + "z": 10000.08, + "w": 7200.42, + "x": 3700.0, + "y": 3699.5 + } + }, + "DEL_405C_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7489.33, + "x": 3703.25, + "y": 3468.0 + } + }, + "DEL_405C_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6599.75, + "x": 3200.0, + "y": 3021.25 + } + }, + "DEL_405C_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5854.58, + "x": 2867.25, + "y": 2574.5 + } + }, + "DEL_407A_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 4446.58, + "x": 3064.5, + "y": 4166.0 + } + }, + "DEL_407A_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 5836.75, + "x": 3200.0, + "y": 4548.25 + } + }, + "DEL_407A_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 6514.33, + "x": 3703.25, + "y": 5000.0 + } + }, + "DEL_A0D1_120": { + "monitor_timings": { + "frequency_10khz": 49938, + "refresh_hz": 119.997, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1530, + "visible": 1440, + "numBlanking": 90, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 82 + } + }, + "glasses_timings": { + "z": 8333.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "DEL_A0D1_100": { + "monitor_timings": { + "frequency_10khz": 41186, + "refresh_hz": 99.947, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1515, + "visible": 1440, + "numBlanking": 75, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 67 + } + }, + "glasses_timings": { + "z": 10005.33, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "DEL_A0E7_120": { + "monitor_timings": { + "frequency_10khz": 49938, + "refresh_hz": 119.997, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1530, + "visible": 1440, + "numBlanking": 90, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 82 + } + }, + "glasses_timings": { + "z": 8333.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "DEL_A0E7_100": { + "monitor_timings": { + "frequency_10khz": 41186, + "refresh_hz": 99.947, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1515, + "visible": 1440, + "numBlanking": 75, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 67 + } + }, + "glasses_timings": { + "z": 10005.33, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "DEL_A0F6_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5451.58, + "x": 0.5, + "y": 7334.0 + } + }, + "DEL_A0F6_144": { + "monitor_timings": { + "frequency_10khz": 34800, + "refresh_hz": 143.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1162, + "visible": 1080, + "numBlanking": 82, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 74 + } + }, + "glasses_timings": { + "z": 6945.25, + "w": 4153.33, + "x": 16.67, + "y": 5945.0 + } + }, + "DWE_2230_120": { + "monitor_timings": { + "frequency_10khz": 24660, + "refresh_hz": 119.984, + "hor": { + "total": 1840, + "visible": 1680, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1117, + "visible": 1050, + "numBlanking": 67, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 58 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5800.0, + "x": 3064.5, + "y": 2375.25 + } + }, + "DWE_2230_110": { + "monitor_timings": { + "frequency_10khz": 22476, + "refresh_hz": 109.948, + "hor": { + "total": 1840, + "visible": 1680, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1111, + "visible": 1050, + "numBlanking": 61, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 52 + } + }, + "glasses_timings": { + "z": 9095.25, + "w": 6599.75, + "x": 3200.0, + "y": 3021.25 + } + }, + "DWE_2230_100": { + "monitor_timings": { + "frequency_10khz": 20342, + "refresh_hz": 99.959, + "hor": { + "total": 1840, + "visible": 1680, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1106, + "visible": 1050, + "numBlanking": 56, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 47 + } + }, + "glasses_timings": { + "z": 10004.17, + "w": 7050.0, + "x": 3703.25, + "y": 3468.0 + } + }, + "EDP_4433_100": { + "monitor_timings": { + "frequency_10khz": 23670, + "refresh_hz": 99.998, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10000.17, + "w": 7489.33, + "x": 3703.0, + "y": 3249.75 + } + }, + "EDP_4433_110": { + "monitor_timings": { + "frequency_10khz": 26152, + "refresh_hz": 110.001, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9090.83, + "w": 6599.75, + "x": 3200.0, + "y": 2750.25 + } + }, + "EDP_4433_120": { + "monitor_timings": { + "frequency_10khz": 28679, + "refresh_hz": 120.0, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 5854.58, + "x": 3064.5, + "y": 2100.25 + } + }, + "GSM_5787_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7489.33, + "x": 3703.25, + "y": 3468.0 + } + }, + "GSM_5787_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6599.75, + "x": 3200.0, + "y": 3021.25 + } + }, + "GSM_5787_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5854.58, + "x": 3064.5, + "y": 2375.25 + } + }, + "HPN_3421_120": { + "monitor_timings": { + "frequency_10khz": 49938, + "refresh_hz": 119.997, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1530, + "visible": 1440, + "numBlanking": 90, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 82 + } + }, + "glasses_timings": { + "z": 8333.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "HPN_3421_100": { + "monitor_timings": { + "frequency_10khz": 41186, + "refresh_hz": 99.947, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1515, + "visible": 1440, + "numBlanking": 75, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 67 + } + }, + "glasses_timings": { + "z": 10005.33, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "HPN_3588_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5451.58, + "x": 0.5, + "y": 7334.0 + } + }, + "HPN_3588_144": { + "monitor_timings": { + "frequency_10khz": 34800, + "refresh_hz": 143.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1162, + "visible": 1080, + "numBlanking": 82, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 74 + } + }, + "glasses_timings": { + "z": 6945.25, + "w": 4153.33, + "x": 16.67, + "y": 5945.0 + } + }, + "HSD_12CC_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 6900.08, + "x": 4000.0, + "y": 3300.25 + } + }, + "HSD_12CC_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6000.08, + "x": 3500.25, + "y": 3000.25 + } + }, + "HSD_12CC_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5600.0, + "x": 2900.0, + "y": 2599.5 + } + }, + "HWP_2860_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7489.33, + "x": 3703.25, + "y": 3468.0 + } + }, + "HWP_2860_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6599.75, + "x": 3200.0, + "y": 3021.25 + } + }, + "HWP_2860_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5854.58, + "x": 3064.5, + "y": 2375.25 + } + }, + "IFS_F782_120": { + "monitor_timings": { + "frequency_10khz": 8395, + "refresh_hz": 120.0, + "hor": { + "total": 1088, + "visible": 800, + "numBlanking": 288, + "numSync": 88, + "border": 0, + "frontPorch": 56, + "backPorch": 144 + }, + "ver": { + "total": 643, + "visible": 600, + "numBlanking": 43, + "numSync": 3, + "border": 0, + "frontPorch": 1, + "backPorch": 39 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4166.67, + "x": 4739.75, + "y": 500.0 + } + }, + "IOD_1782_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 6900.08, + "x": 4000.0, + "y": 3300.25 + } + }, + "IOD_1782_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6000.08, + "x": 3500.25, + "y": 3000.25 + } + }, + "IOD_1782_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5460.0, + "x": 2900.0, + "y": 2599.5 + } + }, + "JEN_2456_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7489.33, + "x": 3702.5, + "y": 3570.75 + } + }, + "JEN_2456_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5800.25, + "x": 2866.75, + "y": 2650.0 + } + }, + "JEN_2456_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6599.75, + "x": 3199.5, + "y": 3124.25 + } + }, + "LDG_1611_120": { + "monitor_timings": { + "frequency_10khz": 14896, + "refresh_hz": 119.999, + "hor": { + "total": 1664, + "visible": 1280, + "numBlanking": 384, + "numSync": 136, + "border": 0, + "frontPorch": 56, + "backPorch": 192 + }, + "ver": { + "total": 746, + "visible": 720, + "numBlanking": 26, + "numSync": 3, + "border": 0, + "frontPorch": 1, + "backPorch": 22 + } + }, + "glasses_timings": { + "z": 8333.42, + "w": 4166.67, + "x": 3743.08, + "y": 700.0 + } + }, + "LDG_EC0E_120": { + "monitor_timings": { + "frequency_10khz": 7325, + "refresh_hz": 119.972, + "hor": { + "total": 960, + "visible": 800, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 636, + "visible": 600, + "numBlanking": 36, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 29 + } + }, + "glasses_timings": { + "z": 8335.25, + "w": 4600.25, + "x": 2724.75, + "y": 290.0 + } + }, + "LDG_ED0E_120": { + "monitor_timings": { + "frequency_10khz": 7325, + "refresh_hz": 119.972, + "hor": { + "total": 960, + "visible": 800, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 636, + "visible": 600, + "numBlanking": 36, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 29 + } + }, + "glasses_timings": { + "z": 8335.25, + "w": 4600.25, + "x": 2724.75, + "y": 290.0 + } + }, + "LEN_0100_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 6900.08, + "x": 4000.0, + "y": 3300.25 + } + }, + "LEN_0100_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6000.08, + "x": 3500.25, + "y": 3000.25 + } + }, + "LEN_0100_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5600.0, + "x": 2900.0, + "y": 2599.5 + } + }, + "LEN_0200_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7389.33, + "x": 3703.25, + "y": 3348.0 + } + }, + "LEN_0200_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6599.75, + "x": 3200.0, + "y": 3021.25 + } + }, + "LEN_0200_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5754.58, + "x": 3064.5, + "y": 2325.25 + } + }, + "LEN_11CC_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7100.0, + "x": 3703.25, + "y": 3468.0 + } + }, + "LEN_11CC_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6599.75, + "x": 3200.0, + "y": 3021.25 + } + }, + "LEN_11CC_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5854.58, + "x": 3064.5, + "y": 2375.25 + } + }, + "LGD_02C5_120": { + "monitor_timings": { + "frequency_10khz": 39636, + "refresh_hz": 119.999, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1588, + "visible": 1080, + "numBlanking": 508, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 500 + } + }, + "glasses_timings": { + "z": 8333.42, + "w": 5854.58, + "x": 2865.0, + "y": 2475.25 + } + }, + "LGD_02C5_110": { + "monitor_timings": { + "frequency_10khz": 39636, + "refresh_hz": 110.022, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1732, + "visible": 1080, + "numBlanking": 652, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 644 + } + }, + "glasses_timings": { + "z": 9089.08, + "w": 6599.75, + "x": 3200.0, + "y": 3021.25 + } + }, + "LGD_02C5_100": { + "monitor_timings": { + "frequency_10khz": 39636, + "refresh_hz": 100.03, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1905, + "visible": 1080, + "numBlanking": 825, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 817 + } + }, + "glasses_timings": { + "z": 9997.0, + "w": 7489.33, + "x": 3403.0, + "y": 3468.0 + } + }, + "LGD_9C95_110": { + "monitor_timings": { + "frequency_10khz": 39651, + "refresh_hz": 110.0, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 48, + "border": 0, + "frontPorch": 32, + "backPorch": 80 + }, + "ver": { + "total": 1733, + "visible": 1080, + "numBlanking": 653, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 645 + } + }, + "glasses_timings": { + "z": 9090.92, + "w": 5908.33, + "x": 4121.0, + "y": 2999.5 + } + }, + "LGD_9C95_120": { + "monitor_timings": { + "frequency_10khz": 39636, + "refresh_hz": 119.999, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 48, + "border": 0, + "frontPorch": 32, + "backPorch": 80 + }, + "ver": { + "total": 1588, + "visible": 1080, + "numBlanking": 508, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 500 + } + }, + "glasses_timings": { + "z": 8333.42, + "w": 6149.92, + "x": 2952.5, + "y": 2599.5 + } + }, + "LGD_9C95_100": { + "monitor_timings": { + "frequency_10khz": 39645, + "refresh_hz": 100.001, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 48, + "border": 0, + "frontPorch": 32, + "backPorch": 80 + }, + "ver": { + "total": 1906, + "visible": 1080, + "numBlanking": 826, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 818 + } + }, + "glasses_timings": { + "z": 9999.92, + "w": 7200.0, + "x": 3699.75, + "y": 4199.75 + } + }, + "MEI_D0C7_120": { + "monitor_timings": { + "frequency_10khz": 16156, + "refresh_hz": 119.997, + "hor": { + "total": 1744, + "visible": 1280, + "numBlanking": 464, + "numSync": 136, + "border": 0, + "frontPorch": 96, + "backPorch": 232 + }, + "ver": { + "total": 772, + "visible": 720, + "numBlanking": 52, + "numSync": 3, + "border": 0, + "frontPorch": 1, + "backPorch": 48 + } + }, + "glasses_timings": { + "z": 8333.58, + "w": 4666.67, + "x": 2866.75, + "y": 500.0 + } + }, + "MEI_D0C8_120": { + "monitor_timings": { + "frequency_10khz": 16156, + "refresh_hz": 119.997, + "hor": { + "total": 1744, + "visible": 1280, + "numBlanking": 464, + "numSync": 136, + "border": 0, + "frontPorch": 96, + "backPorch": 232 + }, + "ver": { + "total": 772, + "visible": 720, + "numBlanking": 52, + "numSync": 3, + "border": 0, + "frontPorch": 1, + "backPorch": 48 + } + }, + "glasses_timings": { + "z": 8333.58, + "w": 4666.67, + "x": 2866.75, + "y": 500.0 + } + }, + "MST_0100_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7489.33, + "x": 3703.25, + "y": 3468.0 + } + }, + "MST_0100_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6599.75, + "x": 3200.0, + "y": 3021.25 + } + }, + "MST_0100_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5854.58, + "x": 2867.25, + "y": 2574.5 + } + }, + "MST_0101_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7489.33, + "x": 3703.25, + "y": 3571.0 + } + }, + "MST_0101_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6599.75, + "x": 3200.0, + "y": 3124.25 + } + }, + "MST_0101_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5800.25, + "x": 2867.25, + "y": 2650.0 + } + }, + "MST_0102_119": { + "monitor_timings": { + "frequency_10khz": 28542, + "refresh_hz": 119.427, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8373.33, + "w": 5754.5, + "x": 3064.5, + "y": 2375.25 + } + }, + "MST_0102_109": { + "monitor_timings": { + "frequency_10khz": 25987, + "refresh_hz": 109.307, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9148.58, + "w": 6599.75, + "x": 3200.0, + "y": 3021.25 + } + }, + "MST_0102_100": { + "monitor_timings": { + "frequency_10khz": 23582, + "refresh_hz": 99.627, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10037.5, + "w": 7489.33, + "x": 3702.75, + "y": 3468.0 + } + }, + "MST_0103_119": { + "monitor_timings": { + "frequency_10khz": 28542, + "refresh_hz": 119.427, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8373.33, + "w": 5855.0, + "x": 3264.75, + "y": 2475.25 + } + }, + "MST_0103_109": { + "monitor_timings": { + "frequency_10khz": 25987, + "refresh_hz": 109.307, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9148.58, + "w": 6599.75, + "x": 3200.0, + "y": 3021.25 + } + }, + "MST_0103_100": { + "monitor_timings": { + "frequency_10khz": 23582, + "refresh_hz": 99.627, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10037.5, + "w": 7489.33, + "x": 3702.75, + "y": 3468.0 + } + }, + "MST_0400_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7550.08, + "x": 3703.25, + "y": 3400.25 + } + }, + "MST_0400_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6619.75, + "x": 3200.0, + "y": 2949.5 + } + }, + "MST_0400_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5930.33, + "x": 2867.25, + "y": 2499.75 + } + }, + "NEC_67EF_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5854.58, + "x": 3064.5, + "y": 2375.25 + } + }, + "NEC_67EF_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6599.75, + "x": 3200.0, + "y": 3021.25 + } + }, + "NEC_67EF_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7489.33, + "x": 3703.25, + "y": 3468.0 + } + }, + "NEC_79BB_120": { + "monitor_timings": { + "frequency_10khz": 8395, + "refresh_hz": 120.0, + "hor": { + "total": 1088, + "visible": 800, + "numBlanking": 288, + "numSync": 88, + "border": 0, + "frontPorch": 56, + "backPorch": 144 + }, + "ver": { + "total": 643, + "visible": 600, + "numBlanking": 43, + "numSync": 3, + "border": 0, + "frontPorch": 1, + "backPorch": 39 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4700.42, + "x": 2866.5, + "y": 450.0 + } + }, + "NEC_79D8_120": { + "monitor_timings": { + "frequency_10khz": 14625, + "refresh_hz": 119.909, + "hor": { + "total": 1440, + "visible": 1280, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 847, + "visible": 800, + "numBlanking": 47, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 38 + } + }, + "glasses_timings": { + "z": 8339.67, + "w": 4600.25, + "x": 2724.75, + "y": 290.0 + } + }, + "NEC_79DA_120": { + "monitor_timings": { + "frequency_10khz": 14625, + "refresh_hz": 119.909, + "hor": { + "total": 1440, + "visible": 1280, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 847, + "visible": 800, + "numBlanking": 47, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 38 + } + }, + "glasses_timings": { + "z": 8339.67, + "w": 4600.25, + "x": 2724.75, + "y": 290.0 + } + }, + "NEC_79F7_120": { + "monitor_timings": { + "frequency_10khz": 14625, + "refresh_hz": 119.909, + "hor": { + "total": 1440, + "visible": 1280, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 847, + "visible": 800, + "numBlanking": 47, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 38 + } + }, + "glasses_timings": { + "z": 8339.67, + "w": 4169.83, + "x": 3199.75, + "y": 500.0 + } + }, + "NEC_79F8_120": { + "monitor_timings": { + "frequency_10khz": 13905, + "refresh_hz": 119.996, + "hor": { + "total": 1408, + "visible": 1024, + "numBlanking": 384, + "numSync": 112, + "border": 0, + "frontPorch": 80, + "backPorch": 192 + }, + "ver": { + "total": 823, + "visible": 768, + "numBlanking": 55, + "numSync": 3, + "border": 0, + "frontPorch": 1, + "backPorch": 51 + } + }, + "glasses_timings": { + "z": 8333.58, + "w": 4600.25, + "x": 2724.75, + "y": 290.0 + } + }, + "NEC_7A6F_120": { + "monitor_timings": { + "frequency_10khz": 14625, + "refresh_hz": 119.909, + "hor": { + "total": 1440, + "visible": 1280, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 847, + "visible": 800, + "numBlanking": 47, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 38 + } + }, + "glasses_timings": { + "z": 8339.67, + "w": 4169.83, + "x": 3199.75, + "y": 500.0 + } + }, + "NEC_7A71_120": { + "monitor_timings": { + "frequency_10khz": 14625, + "refresh_hz": 119.909, + "hor": { + "total": 1440, + "visible": 1280, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 847, + "visible": 800, + "numBlanking": 47, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 38 + } + }, + "glasses_timings": { + "z": 8339.67, + "w": 4169.83, + "x": 3199.75, + "y": 500.0 + } + }, + "NEC_7A72_120": { + "monitor_timings": { + "frequency_10khz": 14625, + "refresh_hz": 119.909, + "hor": { + "total": 1440, + "visible": 1280, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 847, + "visible": 800, + "numBlanking": 47, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 38 + } + }, + "glasses_timings": { + "z": 8339.67, + "w": 4600.25, + "x": 2724.75, + "y": 290.0 + } + }, + "NEC_7A74_120": { + "monitor_timings": { + "frequency_10khz": 14625, + "refresh_hz": 119.909, + "hor": { + "total": 1440, + "visible": 1280, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 847, + "visible": 800, + "numBlanking": 47, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 38 + } + }, + "glasses_timings": { + "z": 8339.67, + "w": 4600.25, + "x": 2724.75, + "y": 290.0 + } + }, + "NVD_FFFB_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5451.58, + "x": 0.5, + "y": 7334.0 + } + }, + "NVD_FFFB_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 6975.0, + "x": 203.25, + "y": 8800.0 + } + }, + "NVD_FFFB_144": { + "monitor_timings": { + "frequency_10khz": 34800, + "refresh_hz": 143.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1162, + "visible": 1080, + "numBlanking": 82, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 74 + } + }, + "glasses_timings": { + "z": 6945.25, + "w": 4153.33, + "x": 16.67, + "y": 5945.0 + } + }, + "NVD_FFFD_120": { + "monitor_timings": { + "frequency_10khz": 49938, + "refresh_hz": 119.997, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1530, + "visible": 1440, + "numBlanking": 90, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 82 + } + }, + "glasses_timings": { + "z": 8333.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "NVD_FFFD_100": { + "monitor_timings": { + "frequency_10khz": 41186, + "refresh_hz": 99.947, + "hor": { + "total": 2720, + "visible": 2560, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1515, + "visible": 1440, + "numBlanking": 75, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 67 + } + }, + "glasses_timings": { + "z": 10005.33, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "NVD_FFFE_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "NVD_FFFE_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "OTM_3031_120": { + "monitor_timings": { + "frequency_10khz": 7325, + "refresh_hz": 119.972, + "hor": { + "total": 960, + "visible": 800, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 636, + "visible": 600, + "numBlanking": 36, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 29 + } + }, + "glasses_timings": { + "z": 8335.25, + "w": 4600.25, + "x": 2884.75, + "y": 290.0 + } + }, + "OTM_3032_120": { + "monitor_timings": { + "frequency_10khz": 16156, + "refresh_hz": 119.997, + "hor": { + "total": 1744, + "visible": 1280, + "numBlanking": 464, + "numSync": 136, + "border": 0, + "frontPorch": 96, + "backPorch": 232 + }, + "ver": { + "total": 772, + "visible": 720, + "numBlanking": 52, + "numSync": 3, + "border": 0, + "frontPorch": 1, + "backPorch": 48 + } + }, + "glasses_timings": { + "z": 8333.58, + "w": 4600.25, + "x": 2884.75, + "y": 289.5 + } + }, + "OTM_3033_120": { + "monitor_timings": { + "frequency_10khz": 16156, + "refresh_hz": 119.997, + "hor": { + "total": 1744, + "visible": 1280, + "numBlanking": 464, + "numSync": 136, + "border": 0, + "frontPorch": 96, + "backPorch": 232 + }, + "ver": { + "total": 772, + "visible": 720, + "numBlanking": 52, + "numSync": 3, + "border": 0, + "frontPorch": 1, + "backPorch": 48 + } + }, + "glasses_timings": { + "z": 8333.58, + "w": 4899.58, + "x": 2884.75, + "y": 290.0 + } + }, + "OTM_3034_120": { + "monitor_timings": { + "frequency_10khz": 16156, + "refresh_hz": 119.997, + "hor": { + "total": 1744, + "visible": 1280, + "numBlanking": 464, + "numSync": 136, + "border": 0, + "frontPorch": 96, + "backPorch": 232 + }, + "ver": { + "total": 772, + "visible": 720, + "numBlanking": 52, + "numSync": 3, + "border": 0, + "frontPorch": 1, + "backPorch": 48 + } + }, + "glasses_timings": { + "z": 8333.58, + "w": 4899.58, + "x": 2884.75, + "y": 290.0 + } + }, + "PHL_C0ED_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 4735.58, + "x": 0.5, + "y": 7334.0 + } + }, + "PHL_C0ED_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 5204.33, + "x": 203.25, + "y": 8800.0 + } + }, + "PJD_00E8_60": { + "monitor_timings": { + "frequency_10khz": 6500, + "refresh_hz": 60.004, + "hor": { + "total": 1344, + "visible": 1024, + "numBlanking": 320, + "numSync": 136, + "border": 0, + "frontPorch": 24, + "backPorch": 160 + }, + "ver": { + "total": 806, + "visible": 768, + "numBlanking": 38, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 29 + } + }, + "glasses_timings": { + "z": 8332.83, + "w": 4166.42, + "x": 1749.5, + "y": 700.0 + } + }, + "PJD_00E8_72": { + "monitor_timings": { + "frequency_10khz": 5000, + "refresh_hz": 72.188, + "hor": { + "total": 1040, + "visible": 800, + "numBlanking": 240, + "numSync": 120, + "border": 0, + "frontPorch": 56, + "backPorch": 64 + }, + "ver": { + "total": 666, + "visible": 600, + "numBlanking": 66, + "numSync": 6, + "border": 0, + "frontPorch": 37, + "backPorch": 23 + } + }, + "glasses_timings": { + "z": 6926.42, + "w": 3463.17, + "x": 2299.5, + "y": 700.0 + } + }, + "PJD_00E8_75": { + "monitor_timings": { + "frequency_10khz": 13500, + "refresh_hz": 75.025, + "hor": { + "total": 1688, + "visible": 1280, + "numBlanking": 408, + "numSync": 144, + "border": 0, + "frontPorch": 16, + "backPorch": 248 + }, + "ver": { + "total": 1066, + "visible": 1024, + "numBlanking": 42, + "numSync": 3, + "border": 0, + "frontPorch": 1, + "backPorch": 38 + } + }, + "glasses_timings": { + "z": 6664.5, + "w": 3332.25, + "x": 3330.5, + "y": 718.5 + } + }, + "PJD_00E8_56": { + "monitor_timings": { + "frequency_10khz": 3600, + "refresh_hz": 56.25, + "hor": { + "total": 1024, + "visible": 800, + "numBlanking": 224, + "numSync": 72, + "border": 0, + "frontPorch": 24, + "backPorch": 128 + }, + "ver": { + "total": 625, + "visible": 600, + "numBlanking": 25, + "numSync": 2, + "border": 0, + "frontPorch": 1, + "backPorch": 22 + } + }, + "glasses_timings": { + "z": 8888.92, + "w": 4444.42, + "x": 1784.0, + "y": 700.0 + } + }, + "PLN_2311_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 6900.08, + "x": 4000.0, + "y": 3300.25 + } + }, + "PLN_2311_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6000.08, + "x": 3500.25, + "y": 3000.25 + } + }, + "PLN_2311_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5600.0, + "x": 2900.0, + "y": 2599.5 + } + }, + "SAM_00FF_120": { + "monitor_timings": { + "frequency_10khz": 16488, + "refresh_hz": 120.0, + "hor": { + "total": 1500, + "visible": 1366, + "numBlanking": 134, + "numSync": 10, + "border": 0, + "frontPorch": 114, + "backPorch": 10 + }, + "ver": { + "total": 916, + "visible": 768, + "numBlanking": 148, + "numSync": 28, + "border": 0, + "frontPorch": 60, + "backPorch": 60 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 5667.75, + "x": 2866.5, + "y": 1612.0 + } + }, + "SAM_04EA_110": { + "monitor_timings": { + "frequency_10khz": 32993, + "refresh_hz": 110.002, + "hor": { + "total": 1780, + "visible": 1680, + "numBlanking": 100, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 60 + }, + "ver": { + "total": 1685, + "visible": 1050, + "numBlanking": 635, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 626 + } + }, + "glasses_timings": { + "z": 9090.67, + "w": 6149.92, + "x": 4052.33, + "y": 2801.42 + } + }, + "SAM_04EA_120": { + "monitor_timings": { + "frequency_10khz": 32980, + "refresh_hz": 120.001, + "hor": { + "total": 1780, + "visible": 1680, + "numBlanking": 100, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 60 + }, + "ver": { + "total": 1544, + "visible": 1050, + "numBlanking": 494, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 485 + } + }, + "glasses_timings": { + "z": 8333.25, + "w": 4567.92, + "x": 4774.17, + "y": 2079.92 + } + }, + "SAM_04EA_100": { + "monitor_timings": { + "frequency_10khz": 32965, + "refresh_hz": 99.998, + "hor": { + "total": 1780, + "visible": 1680, + "numBlanking": 100, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 60 + }, + "ver": { + "total": 1852, + "visible": 1050, + "numBlanking": 802, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 793 + } + }, + "glasses_timings": { + "z": 10000.17, + "w": 7399.58, + "x": 3950.25, + "y": 3499.83 + } + }, + "SAM_8524_110": { + "monitor_timings": { + "frequency_10khz": 32993, + "refresh_hz": 110.002, + "hor": { + "total": 1780, + "visible": 1680, + "numBlanking": 100, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 60 + }, + "ver": { + "total": 1685, + "visible": 1050, + "numBlanking": 635, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 626 + } + }, + "glasses_timings": { + "z": 9090.67, + "w": 6149.92, + "x": 4052.33, + "y": 2801.42 + } + }, + "SAM_8524_120": { + "monitor_timings": { + "frequency_10khz": 32980, + "refresh_hz": 120.001, + "hor": { + "total": 1780, + "visible": 1680, + "numBlanking": 100, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 60 + }, + "ver": { + "total": 1544, + "visible": 1050, + "numBlanking": 494, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 485 + } + }, + "glasses_timings": { + "z": 8333.25, + "w": 4567.92, + "x": 4774.17, + "y": 2079.92 + } + }, + "SAM_8524_100": { + "monitor_timings": { + "frequency_10khz": 32965, + "refresh_hz": 99.998, + "hor": { + "total": 1780, + "visible": 1680, + "numBlanking": 100, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 60 + }, + "ver": { + "total": 1852, + "visible": 1050, + "numBlanking": 802, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 793 + } + }, + "glasses_timings": { + "z": 10000.17, + "w": 7399.58, + "x": 3950.25, + "y": 3499.83 + } + }, + "SAN_BB15_120": { + "monitor_timings": { + "frequency_10khz": 16156, + "refresh_hz": 119.997, + "hor": { + "total": 1744, + "visible": 1280, + "numBlanking": 464, + "numSync": 136, + "border": 0, + "frontPorch": 96, + "backPorch": 232 + }, + "ver": { + "total": 772, + "visible": 720, + "numBlanking": 52, + "numSync": 3, + "border": 0, + "frontPorch": 1, + "backPorch": 48 + } + }, + "glasses_timings": { + "z": 8333.58, + "w": 4899.58, + "x": 2867.75, + "y": 500.0 + } + }, + "SAN_BB16_120": { + "monitor_timings": { + "frequency_10khz": 14850, + "refresh_hz": 120.0, + "hor": { + "total": 1650, + "visible": 1280, + "numBlanking": 370, + "numSync": 40, + "border": 0, + "frontPorch": 110, + "backPorch": 220 + }, + "ver": { + "total": 750, + "visible": 720, + "numBlanking": 30, + "numSync": 5, + "border": 0, + "frontPorch": 5, + "backPorch": 20 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4960.0, + "x": 2884.5, + "y": 290.0 + } + }, + "SAN_BE59_120": { + "monitor_timings": { + "frequency_10khz": 16156, + "refresh_hz": 119.997, + "hor": { + "total": 1744, + "visible": 1280, + "numBlanking": 464, + "numSync": 136, + "border": 0, + "frontPorch": 96, + "backPorch": 232 + }, + "ver": { + "total": 772, + "visible": 720, + "numBlanking": 52, + "numSync": 3, + "border": 0, + "frontPorch": 1, + "backPorch": 48 + } + }, + "glasses_timings": { + "z": 8333.58, + "w": 4666.67, + "x": 2866.75, + "y": 500.0 + } + }, + "SAN_BE5A_120": { + "monitor_timings": { + "frequency_10khz": 14850, + "refresh_hz": 120.0, + "hor": { + "total": 1650, + "visible": 1280, + "numBlanking": 370, + "numSync": 40, + "border": 0, + "frontPorch": 110, + "backPorch": 220 + }, + "ver": { + "total": 750, + "visible": 720, + "numBlanking": 30, + "numSync": 5, + "border": 0, + "frontPorch": 5, + "backPorch": 20 + } + }, + "glasses_timings": { + "z": 8333.33, + "w": 4666.67, + "x": 2866.75, + "y": 500.0 + } + }, + "SEC_4433_110": { + "monitor_timings": { + "frequency_10khz": 39628, + "refresh_hz": 110.0, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 120 + }, + "ver": { + "total": 1732, + "visible": 1080, + "numBlanking": 652, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 644 + } + }, + "glasses_timings": { + "z": 9090.92, + "w": 6527.0, + "x": 3245.25, + "y": 3330.5 + } + }, + "SEC_4433_120": { + "monitor_timings": { + "frequency_10khz": 39636, + "refresh_hz": 119.999, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 120 + }, + "ver": { + "total": 1588, + "visible": 1080, + "numBlanking": 508, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 500 + } + }, + "glasses_timings": { + "z": 8333.42, + "w": 5977.08, + "x": 2866.5, + "y": 2815.0 + } + }, + "SEC_4433_100": { + "monitor_timings": { + "frequency_10khz": 39645, + "refresh_hz": 100.001, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 120 + }, + "ver": { + "total": 1906, + "visible": 1080, + "numBlanking": 826, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 818 + } + }, + "glasses_timings": { + "z": 9999.92, + "w": 7626.83, + "x": 3699.75, + "y": 3399.25 + } + }, + "SEC_5044_120": { + "monitor_timings": { + "frequency_10khz": 39636, + "refresh_hz": 119.999, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1588, + "visible": 1080, + "numBlanking": 508, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 500 + } + }, + "glasses_timings": { + "z": 8333.42, + "w": 6000.0, + "x": 2250.0, + "y": 4000.0 + } + }, + "SEC_5044_110": { + "monitor_timings": { + "frequency_10khz": 39636, + "refresh_hz": 110.022, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1732, + "visible": 1080, + "numBlanking": 652, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 644 + } + }, + "glasses_timings": { + "z": 9089.08, + "w": 6000.0, + "x": 3170.0, + "y": 4440.0 + } + }, + "SEC_5044_100": { + "monitor_timings": { + "frequency_10khz": 39636, + "refresh_hz": 100.03, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1905, + "visible": 1080, + "numBlanking": 825, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 817 + } + }, + "glasses_timings": { + "z": 9997.0, + "w": 6000.0, + "x": 4300.0, + "y": 4720.0 + } + }, + "SNH_4135_120": { + "monitor_timings": { + "frequency_10khz": 28054, + "refresh_hz": 120.002, + "hor": { + "total": 1522, + "visible": 1366, + "numBlanking": 156, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 76 + }, + "ver": { + "total": 1536, + "visible": 768, + "numBlanking": 768, + "numSync": 5, + "border": 0, + "frontPorch": 2, + "backPorch": 761 + } + }, + "glasses_timings": { + "z": 8333.17, + "w": 7000.17, + "x": 1199.5, + "y": 5150.25 + } + }, + "SNH_4135_110": { + "monitor_timings": { + "frequency_10khz": 28054, + "refresh_hz": 110.044, + "hor": { + "total": 1522, + "visible": 1366, + "numBlanking": 156, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 76 + }, + "ver": { + "total": 1675, + "visible": 768, + "numBlanking": 907, + "numSync": 5, + "border": 0, + "frontPorch": 2, + "backPorch": 900 + } + }, + "glasses_timings": { + "z": 9087.33, + "w": 7000.17, + "x": 2019.5, + "y": 5669.75 + } + }, + "SNH_4135_100": { + "monitor_timings": { + "frequency_10khz": 28054, + "refresh_hz": 100.013, + "hor": { + "total": 1522, + "visible": 1366, + "numBlanking": 156, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 76 + }, + "ver": { + "total": 1843, + "visible": 768, + "numBlanking": 1075, + "numSync": 5, + "border": 0, + "frontPorch": 2, + "backPorch": 1068 + } + }, + "glasses_timings": { + "z": 9998.75, + "w": 7000.17, + "x": 2999.75, + "y": 6630.0 + } + }, + "VSC_0E23_110": { + "monitor_timings": { + "frequency_10khz": 33363, + "refresh_hz": 110.0, + "hor": { + "total": 1800, + "visible": 1680, + "numBlanking": 120, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 80 + }, + "ver": { + "total": 1685, + "visible": 1050, + "numBlanking": 635, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 626 + } + }, + "glasses_timings": { + "z": 9090.92, + "w": 6000.42, + "x": 4052.25, + "y": 2679.5 + } + }, + "VSC_0E23_120": { + "monitor_timings": { + "frequency_10khz": 33350, + "refresh_hz": 119.999, + "hor": { + "total": 1800, + "visible": 1680, + "numBlanking": 120, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 80 + }, + "ver": { + "total": 1544, + "visible": 1050, + "numBlanking": 494, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 485 + } + }, + "glasses_timings": { + "z": 8333.42, + "w": 4324.33, + "x": 4774.0, + "y": 2109.5 + } + }, + "VSC_0E23_100": { + "monitor_timings": { + "frequency_10khz": 33336, + "refresh_hz": 100.0, + "hor": { + "total": 1800, + "visible": 1680, + "numBlanking": 120, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 80 + }, + "ver": { + "total": 1852, + "visible": 1050, + "numBlanking": 802, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 793 + } + }, + "glasses_timings": { + "z": 10000.0, + "w": 7299.5, + "x": 3950.25, + "y": 3400.25 + } + }, + "VSC_2725_120": { + "monitor_timings": { + "frequency_10khz": 16156, + "refresh_hz": 119.997, + "hor": { + "total": 1744, + "visible": 1280, + "numBlanking": 464, + "numSync": 136, + "border": 0, + "frontPorch": 96, + "backPorch": 232 + }, + "ver": { + "total": 772, + "visible": 720, + "numBlanking": 52, + "numSync": 3, + "border": 0, + "frontPorch": 1, + "backPorch": 48 + } + }, + "glasses_timings": { + "z": 8333.58, + "w": 4600.25, + "x": 2884.75, + "y": 289.5 + } + }, + "VSC_3D23_120": { + "monitor_timings": { + "frequency_10khz": 7325, + "refresh_hz": 119.972, + "hor": { + "total": 960, + "visible": 800, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 636, + "visible": 600, + "numBlanking": 36, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 29 + } + }, + "glasses_timings": { + "z": 8335.25, + "w": 4600.25, + "x": 2884.75, + "y": 289.5 + } + }, + "VSC_591E_110": { + "monitor_timings": { + "frequency_10khz": 32993, + "refresh_hz": 110.002, + "hor": { + "total": 1780, + "visible": 1680, + "numBlanking": 100, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 60 + }, + "ver": { + "total": 1685, + "visible": 1050, + "numBlanking": 635, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 626 + } + }, + "glasses_timings": { + "z": 9090.67, + "w": 6149.92, + "x": 4052.33, + "y": 2801.42 + } + }, + "VSC_591E_120": { + "monitor_timings": { + "frequency_10khz": 32980, + "refresh_hz": 120.001, + "hor": { + "total": 1780, + "visible": 1680, + "numBlanking": 100, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 60 + }, + "ver": { + "total": 1544, + "visible": 1050, + "numBlanking": 494, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 485 + } + }, + "glasses_timings": { + "z": 8333.25, + "w": 4567.92, + "x": 4774.17, + "y": 2079.92 + } + }, + "VSC_591E_100": { + "monitor_timings": { + "frequency_10khz": 32965, + "refresh_hz": 99.998, + "hor": { + "total": 1780, + "visible": 1680, + "numBlanking": 100, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 60 + }, + "ver": { + "total": 1852, + "visible": 1050, + "numBlanking": 802, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 793 + } + }, + "glasses_timings": { + "z": 10000.17, + "w": 7399.58, + "x": 3950.25, + "y": 3499.83 + } + }, + "VSC_5E23_120": { + "monitor_timings": { + "frequency_10khz": 7325, + "refresh_hz": 119.972, + "hor": { + "total": 960, + "visible": 800, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 636, + "visible": 600, + "numBlanking": 36, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 29 + } + }, + "glasses_timings": { + "z": 8335.25, + "w": 4600.25, + "x": 2884.75, + "y": 300.0 + } + }, + "VSC_6D24_120": { + "monitor_timings": { + "frequency_10khz": 7325, + "refresh_hz": 119.972, + "hor": { + "total": 960, + "visible": 800, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 636, + "visible": 600, + "numBlanking": 36, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 29 + } + }, + "glasses_timings": { + "z": 8335.25, + "w": 4602.25, + "x": 2867.5, + "y": 500.25 + } + }, + "VSC_7E22_110": { + "monitor_timings": { + "frequency_10khz": 32993, + "refresh_hz": 110.002, + "hor": { + "total": 1780, + "visible": 1680, + "numBlanking": 100, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 60 + }, + "ver": { + "total": 1685, + "visible": 1050, + "numBlanking": 635, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 626 + } + }, + "glasses_timings": { + "z": 9090.67, + "w": 5949.75, + "x": 4052.25, + "y": 2801.25 + } + }, + "VSC_7E22_120": { + "monitor_timings": { + "frequency_10khz": 32980, + "refresh_hz": 120.001, + "hor": { + "total": 1780, + "visible": 1680, + "numBlanking": 100, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 60 + }, + "ver": { + "total": 1544, + "visible": 1050, + "numBlanking": 494, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 485 + } + }, + "glasses_timings": { + "z": 8333.25, + "w": 4367.67, + "x": 4774.0, + "y": 2079.75 + } + }, + "VSC_7E22_100": { + "monitor_timings": { + "frequency_10khz": 32965, + "refresh_hz": 99.998, + "hor": { + "total": 1780, + "visible": 1680, + "numBlanking": 100, + "numSync": 20, + "border": 0, + "frontPorch": 20, + "backPorch": 60 + }, + "ver": { + "total": 1852, + "visible": 1050, + "numBlanking": 802, + "numSync": 6, + "border": 0, + "frontPorch": 3, + "backPorch": 793 + } + }, + "glasses_timings": { + "z": 10000.17, + "w": 7199.75, + "x": 3950.25, + "y": 3499.75 + } + }, + "VSC_AC22_120": { + "monitor_timings": { + "frequency_10khz": 7325, + "refresh_hz": 119.972, + "hor": { + "total": 960, + "visible": 800, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 636, + "visible": 600, + "numBlanking": 36, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 29 + } + }, + "glasses_timings": { + "z": 8335.25, + "w": 4600.25, + "x": 2884.75, + "y": 290.0 + } + }, + "VSC_B222_120": { + "monitor_timings": { + "frequency_10khz": 7325, + "refresh_hz": 119.972, + "hor": { + "total": 960, + "visible": 800, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 636, + "visible": 600, + "numBlanking": 36, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 29 + } + }, + "glasses_timings": { + "z": 8335.25, + "w": 4600.25, + "x": 2884.75, + "y": 290.0 + } + }, + "VSC_C122_120": { + "monitor_timings": { + "frequency_10khz": 7325, + "refresh_hz": 119.972, + "hor": { + "total": 960, + "visible": 800, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 636, + "visible": 600, + "numBlanking": 36, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 29 + } + }, + "glasses_timings": { + "z": 8335.25, + "w": 4600.25, + "x": 2884.5, + "y": 300.0 + } + }, + "VSC_CC22_120": { + "monitor_timings": { + "frequency_10khz": 7325, + "refresh_hz": 119.972, + "hor": { + "total": 960, + "visible": 800, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 636, + "visible": 600, + "numBlanking": 36, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 29 + } + }, + "glasses_timings": { + "z": 8335.25, + "w": 4600.25, + "x": 2884.75, + "y": 290.0 + } + }, + "VSC_D022_120": { + "monitor_timings": { + "frequency_10khz": 7325, + "refresh_hz": 119.972, + "hor": { + "total": 960, + "visible": 800, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 636, + "visible": 600, + "numBlanking": 36, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 29 + } + }, + "glasses_timings": { + "z": 8335.25, + "w": 4600.25, + "x": 2884.75, + "y": 300.0 + } + }, + "VSC_DC22_120": { + "monitor_timings": { + "frequency_10khz": 7325, + "refresh_hz": 119.972, + "hor": { + "total": 960, + "visible": 800, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 636, + "visible": 600, + "numBlanking": 36, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 29 + } + }, + "glasses_timings": { + "z": 8335.25, + "w": 4600.25, + "x": 2884.75, + "y": 290.0 + } + }, + "VSC_E527_100": { + "monitor_timings": { + "frequency_10khz": 23654, + "refresh_hz": 99.931, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1138, + "visible": 1080, + "numBlanking": 58, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 50 + } + }, + "glasses_timings": { + "z": 10006.92, + "w": 7090.08, + "x": 4000.0, + "y": 3279.0 + } + }, + "VSC_E527_110": { + "monitor_timings": { + "frequency_10khz": 26139, + "refresh_hz": 109.946, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1143, + "visible": 1080, + "numBlanking": 63, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 55 + } + }, + "glasses_timings": { + "z": 9095.33, + "w": 6200.08, + "x": 3500.25, + "y": 3130.25 + } + }, + "VSC_E527_120": { + "monitor_timings": { + "frequency_10khz": 28675, + "refresh_hz": 119.983, + "hor": { + "total": 2080, + "visible": 1920, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 1149, + "visible": 1080, + "numBlanking": 69, + "numSync": 5, + "border": 0, + "frontPorch": 3, + "backPorch": 61 + } + }, + "glasses_timings": { + "z": 8334.5, + "w": 5800.25, + "x": 2900.0, + "y": 2599.5 + } + }, + "VSC_EA22_120": { + "monitor_timings": { + "frequency_10khz": 7325, + "refresh_hz": 119.972, + "hor": { + "total": 960, + "visible": 800, + "numBlanking": 160, + "numSync": 32, + "border": 0, + "frontPorch": 48, + "backPorch": 80 + }, + "ver": { + "total": 636, + "visible": 600, + "numBlanking": 36, + "numSync": 4, + "border": 0, + "frontPorch": 3, + "backPorch": 29 + } + }, + "glasses_timings": { + "z": 8335.25, + "w": 4600.25, + "x": 2884.75, + "y": 290.0 + } + }, + "GSM_C0C8_120": { + "monitor_timings": { + "frequency_10khz": 118800, + "refresh_hz": 120.000, + "hor": { + "total": 4400, + "visible": 3840, + "numBlanking": 560, + "numSync": 88, + "border": 0, + "frontPorch": 176, + "backPorch": 296 + }, + "ver": { + "total": 2250, + "visible": 2160, + "numBlanking": 90, + "numSync": 10, + "border": 0, + "frontPorch": 8, + "backPorch": 72 + } + }, + "glasses_timings": { + "z": 0.0, + "w": 3611.080078125, + "x": 277.75, + "y": 4680.66650390625 + } +} +} \ No newline at end of file diff --git a/vrto3d/vrto3d/resources/settings/default.vrsettings b/vrto3d/vrto3d/resources/settings/default.vrsettings index 35a16237..3b2efd52 100644 --- a/vrto3d/vrto3d/resources/settings/default.vrsettings +++ b/vrto3d/vrto3d/resources/settings/default.vrsettings @@ -3,6 +3,6 @@ "enable": true, "serial_number": "Stereo3D-1234", "model_number": "Stereo3D-1", - "version_number": "V4.1.0" + "version_number": "V5.0.0" } } diff --git a/vrto3d_profiles/AC2-Win64-Shipping.exe_config.json b/vrto3d_profiles/AC2-Win64-Shipping.exe_config.json index 2ecb2ad8..60db2cd1 100644 --- a/vrto3d_profiles/AC2-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/AC2-Win64-Shipping.exe_config.json @@ -1,31 +1,31 @@ -{ - "hmd_height": 1.0, - "fov": 110.0, - "depth": 0.06499993056058884, - "convergence": 0.33499982953071594, - "async_enable": false, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.06499993056058884, - "user_convergence": 0.33499982953071594 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.09999999403953552, - "user_convergence": 0.8499995470046997 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 110.0, + "depth": 0.06499993056058884, + "convergence": 0.33499982953071594, + "async_enable": false, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_Guide", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.06499993056058884, + "user_convergence": 0.33499982953071594 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.09999999403953552, + "user_convergence": 0.8499995470046997 + } + ] +} diff --git a/vrto3d_profiles/ACOdyssey.exe_config.json b/vrto3d_profiles/ACOdyssey.exe_config.json index 1399256d..ea881029 100644 --- a/vrto3d_profiles/ACOdyssey.exe_config.json +++ b/vrto3d_profiles/ACOdyssey.exe_config.json @@ -1,38 +1,38 @@ -{ - "hmd_height": 1.0, - "fov": 80.0, - "depth": 0.1850001960992813, - "convergence": 2.0099992752075195, - "async_enable": false, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.1850001960992813, - "user_convergence": 2.0099992752075195 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.06499995291233063, - "user_convergence": 0.6550004482269287 - }, - { - "user_load_key": "VK_NUMPAD3", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.06499995291233063, - "user_convergence": 0.6550004482269287 - } - ] -} +{ + "hmd_height": 1.0, + "fov": 80.0, + "depth": 0.1850001960992813, + "convergence": 2.0099992752075195, + "async_enable": false, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_RS", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.1850001960992813, + "user_convergence": 2.0099992752075195 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.06499995291233063, + "user_convergence": 0.6550004482269287 + }, + { + "user_load_key": "Numpad3", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.06499995291233063, + "user_convergence": 0.6550004482269287 + } + ] +} diff --git a/vrto3d_profiles/ACValhalla.exe_config.json b/vrto3d_profiles/ACValhalla.exe_config.json index 188be6bb..45b6834d 100644 --- a/vrto3d_profiles/ACValhalla.exe_config.json +++ b/vrto3d_profiles/ACValhalla.exe_config.json @@ -1,38 +1,38 @@ -{ - "hmd_height": 1.0, - "fov": 75.0, - "depth": 0.1850001960992813, - "convergence": 2.0099992752075195, - "async_enable": false, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.1850001960992813, - "user_convergence": 2.0099992752075195 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.06499995291233063, - "user_convergence": 0.7250003814697266 - }, - { - "user_load_key": "VK_NUMPAD3", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.06499995291233063, - "user_convergence": 0.7250003814697266 - } - ] -} +{ + "hmd_height": 1.0, + "fov": 75.0, + "depth": 0.1850001960992813, + "convergence": 2.0099992752075195, + "async_enable": false, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_RS", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.1850001960992813, + "user_convergence": 2.0099992752075195 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.06499995291233063, + "user_convergence": 0.7250003814697266 + }, + { + "user_load_key": "Numpad3", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.06499995291233063, + "user_convergence": 0.7250003814697266 + } + ] +} diff --git a/vrto3d_profiles/ADR1FT-Win64-Shipping.exe_config.json b/vrto3d_profiles/ADR1FT-Win64-Shipping.exe_config.json index c1a998eb..ec4f8ca5 100644 --- a/vrto3d_profiles/ADR1FT-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/ADR1FT-Win64-Shipping.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.04500158876180649, - "convergence": 0.15000000596046448, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.4000000059604645, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.04500158876180649, + "convergence": 0.15000000596046448, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Pad_RS", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.4000000059604645, + "user_convergence": 4.0 + } + ] +} diff --git a/vrto3d_profiles/AMS2AVX.exe_config.json b/vrto3d_profiles/AMS2AVX.exe_config.json index e144004f..430b2934 100644 --- a/vrto3d_profiles/AMS2AVX.exe_config.json +++ b/vrto3d_profiles/AMS2AVX.exe_config.json @@ -1,24 +1,24 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.06499992311000824, - "convergence": 0.4599997103214264, - "async_enable": false, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.06499992311000824, - "user_convergence": 0.4599997103214264 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.06499992311000824, + "convergence": 0.4599997103214264, + "async_enable": false, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_Guide", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.06499992311000824, + "user_convergence": 0.4599997103214264 + } + ] +} diff --git a/vrto3d_profiles/AirCar-Win64-Shipping.exe_config.json b/vrto3d_profiles/AirCar-Win64-Shipping.exe_config.json index e4e733a5..5f23624a 100644 --- a/vrto3d_profiles/AirCar-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/AirCar-Win64-Shipping.exe_config.json @@ -1,24 +1,24 @@ -{ - "hmd_height": 1.0, - "fov": 115.0, - "depth": 0.06499995291233063, - "convergence": 0.2549999952316284, - "async_enable": false, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.06499995291233063, - "user_convergence": 0.2549999952316284 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 115.0, + "depth": 0.06499995291233063, + "convergence": 0.2549999952316284, + "async_enable": false, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_Guide", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.06499995291233063, + "user_convergence": 0.2549999952316284 + } + ] +} diff --git a/vrto3d_profiles/AlanWake2.exe_config.json b/vrto3d_profiles/AlanWake2.exe_config.json index 092bf7dd..c310d17d 100644 --- a/vrto3d_profiles/AlanWake2.exe_config.json +++ b/vrto3d_profiles/AlanWake2.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.15000009536743164, - "convergence": 0.8600029945373535, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.15000009536743164, - "user_convergence": 0.8600029945373535 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.15000009536743164, + "convergence": 0.8600029945373535, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.15000009536743164, + "user_convergence": 0.8600029945373535 + } + ] +} diff --git a/vrto3d_profiles/Arca_OpenVRSteam.exe_config.json b/vrto3d_profiles/Arca_OpenVRSteam.exe_config.json index ee94c650..ab8d6e45 100644 --- a/vrto3d_profiles/Arca_OpenVRSteam.exe_config.json +++ b/vrto3d_profiles/Arca_OpenVRSteam.exe_config.json @@ -1,24 +1,24 @@ -{ - "hmd_height": 1.0, - "fov": 110.0, - "depth": 0.15000009536743164, - "convergence": 0.8600002527236938, - "async_enable": false, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.15000009536743164, - "user_convergence": 0.8600002527236938 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 110.0, + "depth": 0.15000009536743164, + "convergence": 0.8600002527236938, + "async_enable": false, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.15000009536743164, + "user_convergence": 0.8600002527236938 + } + ] +} diff --git a/vrto3d_profiles/Avowed-Win64-Shipping.exe_config.json b/vrto3d_profiles/Avowed-Win64-Shipping.exe_config.json index 98d14b50..eec2f045 100644 --- a/vrto3d_profiles/Avowed-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/Avowed-Win64-Shipping.exe_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.06999994069337845, - "convergence": 0.7999995350837708, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.06999994069337845, - "user_convergence": 0.7999995350837708 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.15000009536743164, - "user_convergence": 2.6699981689453125 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.15000009536743164, - "user_convergence": 2.6699981689453125 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.06999994069337845, + "convergence": 0.7999995350837708, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.06999994069337845, + "user_convergence": 0.7999995350837708 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.15000009536743164, + "user_convergence": 2.6699981689453125 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.15000009536743164, + "user_convergence": 2.6699981689453125 + } + ] +} diff --git a/vrto3d_profiles/Avowed-WinGDK-Shipping.exe_config.json b/vrto3d_profiles/Avowed-WinGDK-Shipping.exe_config.json index 98d14b50..eec2f045 100644 --- a/vrto3d_profiles/Avowed-WinGDK-Shipping.exe_config.json +++ b/vrto3d_profiles/Avowed-WinGDK-Shipping.exe_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.06999994069337845, - "convergence": 0.7999995350837708, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.06999994069337845, - "user_convergence": 0.7999995350837708 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.15000009536743164, - "user_convergence": 2.6699981689453125 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.15000009536743164, - "user_convergence": 2.6699981689453125 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.06999994069337845, + "convergence": 0.7999995350837708, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.06999994069337845, + "user_convergence": 0.7999995350837708 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.15000009536743164, + "user_convergence": 2.6699981689453125 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.15000009536743164, + "user_convergence": 2.6699981689453125 + } + ] +} diff --git a/vrto3d_profiles/Battlezone_DX12.exe_config.json b/vrto3d_profiles/Battlezone_DX12.exe_config.json index b6379b01..6dd8709a 100644 --- a/vrto3d_profiles/Battlezone_DX12.exe_config.json +++ b/vrto3d_profiles/Battlezone_DX12.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 110.0, - "depth": 0.20000183582305908, - "convergence": 0.599999725818634, - "pitch_enable": true, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.20000183582305908, - "user_convergence": 0.599999725818634 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 110.0, + "depth": 0.20000183582305908, + "convergence": 0.599999725818634, + "pitch_enable": true, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.20000183582305908, + "user_convergence": 0.599999725818634 + } + ] +} diff --git a/vrto3d_profiles/Bugsnax.exe_config.json b/vrto3d_profiles/Bugsnax.exe_config.json index ee1832f5..c7ef6c9c 100644 --- a/vrto3d_profiles/Bugsnax.exe_config.json +++ b/vrto3d_profiles/Bugsnax.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.10000000149011612, - "convergence": 1.6300022602081299, - "pitch_enable": true, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "XINPUT_GAMEPAD_LEFT_SHOULDER", - "ctrl_toggle_type": "hold", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 0.5000004768371582, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.10000000149011612, - "user_convergence": 1.6300022602081299 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.10000000149011612, + "convergence": 1.6300022602081299, + "pitch_enable": true, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Pad_LB", + "ctrl_toggle_type": "hold", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 0.5000004768371582, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.10000000149011612, + "user_convergence": 1.6300022602081299 + } + ] +} diff --git a/vrto3d_profiles/Cemu.exe_config.json b/vrto3d_profiles/Cemu.exe_config.json index 5dae9c00..086da3e9 100644 --- a/vrto3d_profiles/Cemu.exe_config.json +++ b/vrto3d_profiles/Cemu.exe_config.json @@ -1,33 +1,33 @@ -{ - "hmd_height": 0.0, - "fov": 50.0, - "depth": 0.12500004470348358, - "convergence": 1.4449995756149292, - "async_enable": false, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.12500004470348358, - "user_convergence": 1.4449995756149292, - "user_fov": 50.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.06499999761581421, - "user_convergence": 1.4449995756149292, - "user_fov": 50.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 0.0, + "fov": 50.0, + "depth": 0.12500004470348358, + "convergence": 1.4449995756149292, + "async_enable": false, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.12500004470348358, + "user_convergence": 1.4449995756149292, + "user_fov": 50.0 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.06499999761581421, + "user_convergence": 1.4449995756149292, + "user_fov": 50.0 + } + ] +} diff --git a/vrto3d_profiles/Chronos-Win64-Shipping.exe_config.json b/vrto3d_profiles/Chronos-Win64-Shipping.exe_config.json index b9f40dd8..fb69b8c1 100644 --- a/vrto3d_profiles/Chronos-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/Chronos-Win64-Shipping.exe_config.json @@ -1,31 +1,31 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.15000009536743164, - "convergence": 1.7499994039535522, - "async_enable": false, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "VK_NUMPAD7", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 0.7400002479553223, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.15000009536743164, - "user_convergence": 1.7499994039535522 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.065, - "user_convergence": 1.7499994039535522 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.15000009536743164, + "convergence": 1.7499994039535522, + "async_enable": false, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Numpad7", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 0.7400002479553223, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.15000009536743164, + "user_convergence": 1.7499994039535522 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.065, + "user_convergence": 1.7499994039535522 + } + ] +} diff --git a/vrto3d_profiles/Cyberpunk2077.exe_config.json b/vrto3d_profiles/Cyberpunk2077.exe_config.json index 9fea3200..72053b81 100644 --- a/vrto3d_profiles/Cyberpunk2077.exe_config.json +++ b/vrto3d_profiles/Cyberpunk2077.exe_config.json @@ -1,38 +1,38 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.02, - "convergence": 0.2, - "async_enable": false, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.02, - "user_convergence": 0.2 - }, - { - "user_load_key": "XINPUT_GAMEPAD_LEFT_TRIGGER", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.01, - "user_convergence": 0.2 - }, - { - "user_load_key": "VK_RMOUSE", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.01, - "user_convergence": 0.2 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.02, + "convergence": 0.2, + "async_enable": false, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.02, + "user_convergence": 0.2 + }, + { + "user_load_key": "Pad_LT", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.01, + "user_convergence": 0.2 + }, + { + "user_load_key": "Mouse_Right", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.01, + "user_convergence": 0.2 + } + ] +} diff --git a/vrto3d_profiles/DD2.exe_config.json b/vrto3d_profiles/DD2.exe_config.json index 81f8815d..c8893d61 100644 --- a/vrto3d_profiles/DD2.exe_config.json +++ b/vrto3d_profiles/DD2.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.15000173449516296, - "convergence": 2.500009775161743, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.15000173449516296, - "user_convergence": 2.500009775161743 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.15000173449516296, + "convergence": 2.500009775161743, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.15000173449516296, + "user_convergence": 2.500009775161743 + } + ] +} diff --git a/vrto3d_profiles/DOOMTheDarkAges.exe_config.json b/vrto3d_profiles/DOOMTheDarkAges.exe_config.json index 64ffb85f..6bd79aa8 100644 --- a/vrto3d_profiles/DOOMTheDarkAges.exe_config.json +++ b/vrto3d_profiles/DOOMTheDarkAges.exe_config.json @@ -1,24 +1,24 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.03999998793005943, - "convergence": 0.3509998321533203, - "async_enable": false, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.03999998793005943, - "user_convergence": 0.3509998321533203 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.03999998793005943, + "convergence": 0.3509998321533203, + "async_enable": false, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.03999998793005943, + "user_convergence": 0.3509998321533203 + } + ] +} diff --git a/vrto3d_profiles/DOOMVFRx64.exe_config.json b/vrto3d_profiles/DOOMVFRx64.exe_config.json index 4f4862e5..ea6a8634 100644 --- a/vrto3d_profiles/DOOMVFRx64.exe_config.json +++ b/vrto3d_profiles/DOOMVFRx64.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.07000157237052917, - "convergence": 2.5, - "pitch_enable": true, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.07000157237052917, - "user_convergence": 2.5 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.07000157237052917, + "convergence": 2.5, + "pitch_enable": true, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.07000157237052917, + "user_convergence": 2.5 + } + ] +} diff --git a/vrto3d_profiles/DeadIsland-Win64-Shipping.exe_config.json b/vrto3d_profiles/DeadIsland-Win64-Shipping.exe_config.json index 09e72ffe..0c82eeab 100644 --- a/vrto3d_profiles/DeadIsland-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/DeadIsland-Win64-Shipping.exe_config.json @@ -1,24 +1,24 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.06499993056058884, - "convergence": 1.4999988079071045, - "async_enable": false, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.06499993056058884, - "user_convergence": 1.4999988079071045 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.06499993056058884, + "convergence": 1.4999988079071045, + "async_enable": false, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.06499993056058884, + "user_convergence": 1.4999988079071045 + } + ] +} diff --git a/vrto3d_profiles/DeadIsland-WinGDK-Shipping.exe_config.json b/vrto3d_profiles/DeadIsland-WinGDK-Shipping.exe_config.json index 09e72ffe..0c82eeab 100644 --- a/vrto3d_profiles/DeadIsland-WinGDK-Shipping.exe_config.json +++ b/vrto3d_profiles/DeadIsland-WinGDK-Shipping.exe_config.json @@ -1,24 +1,24 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.06499993056058884, - "convergence": 1.4999988079071045, - "async_enable": false, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.06499993056058884, - "user_convergence": 1.4999988079071045 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.06499993056058884, + "convergence": 1.4999988079071045, + "async_enable": false, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.06499993056058884, + "user_convergence": 1.4999988079071045 + } + ] +} diff --git a/vrto3d_profiles/DeoVR.exe_config.json b/vrto3d_profiles/DeoVR.exe_config.json index de591616..4ffde6e7 100644 --- a/vrto3d_profiles/DeoVR.exe_config.json +++ b/vrto3d_profiles/DeoVR.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.15000012516975403, - "convergence": 0.589999794960022, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.15000012516975403, - "user_convergence": 0.589999794960022 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.15000012516975403, + "convergence": 0.589999794960022, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.15000012516975403, + "user_convergence": 0.589999794960022 + } + ] +} diff --git a/vrto3d_profiles/EdgeOfNowhere.exe_config.json b/vrto3d_profiles/EdgeOfNowhere.exe_config.json index b9f40dd8..fb69b8c1 100644 --- a/vrto3d_profiles/EdgeOfNowhere.exe_config.json +++ b/vrto3d_profiles/EdgeOfNowhere.exe_config.json @@ -1,31 +1,31 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.15000009536743164, - "convergence": 1.7499994039535522, - "async_enable": false, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "VK_NUMPAD7", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 0.7400002479553223, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.15000009536743164, - "user_convergence": 1.7499994039535522 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.065, - "user_convergence": 1.7499994039535522 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.15000009536743164, + "convergence": 1.7499994039535522, + "async_enable": false, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Numpad7", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 0.7400002479553223, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.15000009536743164, + "user_convergence": 1.7499994039535522 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.065, + "user_convergence": 1.7499994039535522 + } + ] +} diff --git a/vrto3d_profiles/EliteDangerous64.exe_config.json b/vrto3d_profiles/EliteDangerous64.exe_config.json index 92367399..d8a2d998 100644 --- a/vrto3d_profiles/EliteDangerous64.exe_config.json +++ b/vrto3d_profiles/EliteDangerous64.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.07000157237052917, - "convergence": 0.27000004053115845, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.07000157237052917, - "user_convergence": 0.27000004053115845 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.07000157237052917, + "convergence": 0.27000004053115845, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_RS", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.07000157237052917, + "user_convergence": 0.27000004053115845 + } + ] +} diff --git a/vrto3d_profiles/Epic Roller Coasters.exe_config.json b/vrto3d_profiles/Epic Roller Coasters.exe_config.json index 2f9e286a..95273079 100644 --- a/vrto3d_profiles/Epic Roller Coasters.exe_config.json +++ b/vrto3d_profiles/Epic Roller Coasters.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.1290016919374466, - "convergence": 0.6399996876716614, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 0.8, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.1290016919374466, - "user_convergence": 0.6399996876716614 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.1290016919374466, + "convergence": 0.6399996876716614, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 0.8, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.1290016919374466, + "user_convergence": 0.6399996876716614 + } + ] +} diff --git a/vrto3d_profiles/EternalStrands-WinGDK-Shipping.exe_config.json b/vrto3d_profiles/EternalStrands-WinGDK-Shipping.exe_config.json index df1738d2..a504e023 100644 --- a/vrto3d_profiles/EternalStrands-WinGDK-Shipping.exe_config.json +++ b/vrto3d_profiles/EternalStrands-WinGDK-Shipping.exe_config.json @@ -1,30 +1,30 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.4000000059604645, - "convergence": 4.0, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.4000000059604645, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.4000000059604645, + "convergence": 4.0, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.4000000059604645, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 4.0 + } + ] +} diff --git a/vrto3d_profiles/EternalStrandsSteam-Win64-Shipping.exe_config.json b/vrto3d_profiles/EternalStrandsSteam-Win64-Shipping.exe_config.json index df1738d2..a504e023 100644 --- a/vrto3d_profiles/EternalStrandsSteam-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/EternalStrandsSteam-Win64-Shipping.exe_config.json @@ -1,30 +1,30 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.4000000059604645, - "convergence": 4.0, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.4000000059604645, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.4000000059604645, + "convergence": 4.0, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.4000000059604645, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 4.0 + } + ] +} diff --git a/vrto3d_profiles/F1_23.exe_config.json b/vrto3d_profiles/F1_23.exe_config.json index b225c877..4990ae06 100644 --- a/vrto3d_profiles/F1_23.exe_config.json +++ b/vrto3d_profiles/F1_23.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 100.0, - "depth": 0.029999978840351105, - "convergence": 0.20999997854232788, - "pitch_enable": false, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.029999978840351105, - "user_convergence": 0.20999997854232788 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 100.0, + "depth": 0.029999978840351105, + "convergence": 0.20999997854232788, + "pitch_enable": false, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.029999978840351105, + "user_convergence": 0.20999997854232788 + } + ] +} diff --git a/vrto3d_profiles/F1_24.exe_config.json b/vrto3d_profiles/F1_24.exe_config.json index b225c877..4990ae06 100644 --- a/vrto3d_profiles/F1_24.exe_config.json +++ b/vrto3d_profiles/F1_24.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 100.0, - "depth": 0.029999978840351105, - "convergence": 0.20999997854232788, - "pitch_enable": false, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.029999978840351105, - "user_convergence": 0.20999997854232788 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 100.0, + "depth": 0.029999978840351105, + "convergence": 0.20999997854232788, + "pitch_enable": false, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.029999978840351105, + "user_convergence": 0.20999997854232788 + } + ] +} diff --git a/vrto3d_profiles/FacetedFlight.exe_config.json b/vrto3d_profiles/FacetedFlight.exe_config.json index a42bc73d..8b07d5f0 100644 --- a/vrto3d_profiles/FacetedFlight.exe_config.json +++ b/vrto3d_profiles/FacetedFlight.exe_config.json @@ -1,32 +1,30 @@ -{ - "hmd_height": 1.0, - "fov": 110.0, - "depth": 0.07000157237052917, - "convergence": 0.3300034999847412, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.07000157237052917, - "user_convergence": 0.3300034999847412 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.4000000059604645, - "user_convergence": 5.240028381347656 - } - ] -} - - +{ + "hmd_height": 1.0, + "fov": 110.0, + "depth": 0.07000157237052917, + "convergence": 0.3300034999847412, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_RS", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.07000157237052917, + "user_convergence": 0.3300034999847412 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.4000000059604645, + "user_convergence": 5.240028381347656 + } + ] +} diff --git a/vrto3d_profiles/FlightSimulator.exe_config.json b/vrto3d_profiles/FlightSimulator.exe_config.json index 0260bab0..6662e07f 100644 --- a/vrto3d_profiles/FlightSimulator.exe_config.json +++ b/vrto3d_profiles/FlightSimulator.exe_config.json @@ -1,30 +1,30 @@ -{ - "hmd_height": 1.0, - "fov": 110.0, - "depth": 0.39999836683273315, - "convergence": 3.00002121925354, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.39999836683273315, - "user_convergence": 3.00002121925354 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.06499992311000824, - "user_convergence": 0.4099999666213989 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 110.0, + "depth": 0.39999836683273315, + "convergence": 3.00002121925354, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.39999836683273315, + "user_convergence": 3.00002121925354 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.06499992311000824, + "user_convergence": 0.4099999666213989 + } + ] +} diff --git a/vrto3d_profiles/FlightSimulator2024.exe_config.json b/vrto3d_profiles/FlightSimulator2024.exe_config.json index 0260bab0..6662e07f 100644 --- a/vrto3d_profiles/FlightSimulator2024.exe_config.json +++ b/vrto3d_profiles/FlightSimulator2024.exe_config.json @@ -1,30 +1,30 @@ -{ - "hmd_height": 1.0, - "fov": 110.0, - "depth": 0.39999836683273315, - "convergence": 3.00002121925354, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.39999836683273315, - "user_convergence": 3.00002121925354 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.06499992311000824, - "user_convergence": 0.4099999666213989 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 110.0, + "depth": 0.39999836683273315, + "convergence": 3.00002121925354, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.39999836683273315, + "user_convergence": 3.00002121925354 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.06499992311000824, + "user_convergence": 0.4099999666213989 + } + ] +} diff --git a/vrto3d_profiles/FlyDangerous.exe_config.json b/vrto3d_profiles/FlyDangerous.exe_config.json index 7a3538a7..f25dc8b1 100644 --- a/vrto3d_profiles/FlyDangerous.exe_config.json +++ b/vrto3d_profiles/FlyDangerous.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.07500158250331879, - "convergence": 0.32999998331069946, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.07500158250331879, - "user_convergence": 0.32999998331069946 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.07500158250331879, + "convergence": 0.32999998331069946, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.07500158250331879, + "user_convergence": 0.32999998331069946 + } + ] +} diff --git a/vrto3d_profiles/GalGun2-Win64-Shipping.exe_config.json b/vrto3d_profiles/GalGun2-Win64-Shipping.exe_config.json index f14b958b..2254a600 100644 --- a/vrto3d_profiles/GalGun2-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/GalGun2-Win64-Shipping.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.06500156223773956, - "convergence": 0.5099998116493225, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.06500156223773956, - "user_convergence": 0.5099998116493225 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.06500156223773956, + "convergence": 0.5099998116493225, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.06500156223773956, + "user_convergence": 0.5099998116493225 + } + ] +} diff --git a/vrto3d_profiles/GhostOfTsushima.exe_config.json b/vrto3d_profiles/GhostOfTsushima.exe_config.json index 18d784f7..f7afbf99 100644 --- a/vrto3d_profiles/GhostOfTsushima.exe_config.json +++ b/vrto3d_profiles/GhostOfTsushima.exe_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 80.0, - "depth": 0.2500019371509552, - "convergence": 2.5, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.2500019371509552, - "user_convergence": 2.5 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 2.5 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 2.5 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 80.0, + "depth": 0.2500019371509552, + "convergence": 2.5, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.2500019371509552, + "user_convergence": 2.5 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 2.5 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 2.5 + } + ] +} diff --git a/vrto3d_profiles/GnomesVsFairies.exe_config.json b/vrto3d_profiles/GnomesVsFairies.exe_config.json index ede87511..085b914b 100644 --- a/vrto3d_profiles/GnomesVsFairies.exe_config.json +++ b/vrto3d_profiles/GnomesVsFairies.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.07000157237052917, - "convergence": 0.4100034236907959, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.07000157237052917, - "user_convergence": 0.4100034236907959 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.07000157237052917, + "convergence": 0.4100034236907959, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.07000157237052917, + "user_convergence": 0.4100034236907959 + } + ] +} diff --git a/vrto3d_profiles/GoWR.exe_config.json b/vrto3d_profiles/GoWR.exe_config.json new file mode 100644 index 00000000..1ee69808 --- /dev/null +++ b/vrto3d_profiles/GoWR.exe_config.json @@ -0,0 +1,27 @@ +{ + "hmd_height": 1.0, + "fov": 73.0, + "depth": 0.125, + "convergence": 2.0, + "async_enable": false, + "auto_depth_enabled": true, + "auto_depth_target_disparity": 0.004999999888241291, + "auto_depth_smoothing": 0.07999999821186066, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Pad_RS", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_key_type": "switch", + "user_depth": 0.125, + "user_convergence": 2.0, + "user_fov": 73.0 + } + ] +} diff --git a/vrto3d_profiles/Grip-Win64-Shipping.exe_config.json b/vrto3d_profiles/Grip-Win64-Shipping.exe_config.json index 486b3eaa..cb7831eb 100644 --- a/vrto3d_profiles/Grip-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/Grip-Win64-Shipping.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.2050018459558487, - "convergence": 1.1800026893615723, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.2050018459558487, - "user_convergence": 1.0000028610229492 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.2050018459558487, + "convergence": 1.1800026893615723, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.2050018459558487, + "user_convergence": 1.0000028610229492 + } + ] +} diff --git a/vrto3d_profiles/HUMANITY.exe_config.json b/vrto3d_profiles/HUMANITY.exe_config.json index bd5aaef9..bd87bdd0 100644 --- a/vrto3d_profiles/HUMANITY.exe_config.json +++ b/vrto3d_profiles/HUMANITY.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.07000157237052917, - "convergence": 0.34999996423721313, - "pitch_enable": true, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.30000001192092896, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.07000157237052917, - "user_convergence": 0.34999996423721313 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.07000157237052917, + "convergence": 0.34999996423721313, + "pitch_enable": true, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.30000001192092896, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.07000157237052917, + "user_convergence": 0.34999996423721313 + } + ] +} diff --git a/vrto3d_profiles/Habitat-WinGDK-Shipping.exe_config.json b/vrto3d_profiles/Habitat-WinGDK-Shipping.exe_config.json index af2169b3..91a221fc 100644 --- a/vrto3d_profiles/Habitat-WinGDK-Shipping.exe_config.json +++ b/vrto3d_profiles/Habitat-WinGDK-Shipping.exe_config.json @@ -1,30 +1,30 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.065, - "convergence": 2.5, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.065, - "user_convergence": 2.5 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.025, - "user_convergence": 2.5 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.065, + "convergence": 2.5, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.065, + "user_convergence": 2.5 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.025, + "user_convergence": 2.5 + } + ] +} diff --git a/vrto3d_profiles/Hellblade2-Win64-Shipping.exe_config.json b/vrto3d_profiles/Hellblade2-Win64-Shipping.exe_config.json index 0cc4ea4d..b844cd66 100644 --- a/vrto3d_profiles/Hellblade2-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/Hellblade2-Win64-Shipping.exe_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.10000000149011612, - "convergence": 4.0, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.2, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.2, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.10000000149011612, + "convergence": 4.0, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.10000000149011612, + "user_convergence": 4.0 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.2, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.2, + "user_convergence": 4.0 + } + ] +} diff --git a/vrto3d_profiles/Hellblade2-WinGDK-Shipping.exe_config.json b/vrto3d_profiles/Hellblade2-WinGDK-Shipping.exe_config.json index 0cc4ea4d..b844cd66 100644 --- a/vrto3d_profiles/Hellblade2-WinGDK-Shipping.exe_config.json +++ b/vrto3d_profiles/Hellblade2-WinGDK-Shipping.exe_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.10000000149011612, - "convergence": 4.0, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.2, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.2, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.10000000149011612, + "convergence": 4.0, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.10000000149011612, + "user_convergence": 4.0 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.2, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.2, + "user_convergence": 4.0 + } + ] +} diff --git a/vrto3d_profiles/HogwartsLegacy.exe_config.json b/vrto3d_profiles/HogwartsLegacy.exe_config.json index 734d89f7..5d332f22 100644 --- a/vrto3d_profiles/HogwartsLegacy.exe_config.json +++ b/vrto3d_profiles/HogwartsLegacy.exe_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 75.0, - "depth": 0.2500019371509552, - "convergence": 1.854954481124878, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.2500019371509552, - "user_convergence": 1.854954481124878 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 1.854954481124878 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 1.854954481124878 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 75.0, + "depth": 0.2500019371509552, + "convergence": 1.854954481124878, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.2500019371509552, + "user_convergence": 1.854954481124878 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 1.854954481124878 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 1.854954481124878 + } + ] +} diff --git a/vrto3d_profiles/HorizonZeroDawn.exe_config.json b/vrto3d_profiles/HorizonZeroDawn.exe_config.json index 02fb9bef..b4b7a016 100644 --- a/vrto3d_profiles/HorizonZeroDawn.exe_config.json +++ b/vrto3d_profiles/HorizonZeroDawn.exe_config.json @@ -1,51 +1,51 @@ -{ - "hmd_height": 1.0, - "fov": 70.0, - "depth": 0.2500019371509552, - "convergence": 2.9999771118164063, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.2500019371509552, - "user_convergence": 2.9999771118164063 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.065, - "user_convergence": 2.9999771118164063 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.065, - "user_convergence": 2.9999771118164063 - }, - { - "user_load_key": "XINPUT_GAMEPAD_LEFT_TRIGGER", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.1, - "user_convergence": 2.9999771118164063 - }, - { - "user_load_key": "VK_RMOUSE", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.1, - "user_convergence": 2.9999771118164063 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 70.0, + "depth": 0.2500019371509552, + "convergence": 2.9999771118164063, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.2500019371509552, + "user_convergence": 2.9999771118164063 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.065, + "user_convergence": 2.9999771118164063 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.065, + "user_convergence": 2.9999771118164063 + }, + { + "user_load_key": "Pad_LT", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.1, + "user_convergence": 2.9999771118164063 + }, + { + "user_load_key": "Mouse_Right", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.1, + "user_convergence": 2.9999771118164063 + } + ] +} diff --git a/vrto3d_profiles/ImmortalsOfAveum-Win64-Shipping.exe_config.json b/vrto3d_profiles/ImmortalsOfAveum-Win64-Shipping.exe_config.json index e4ed0d6d..4378fb2c 100644 --- a/vrto3d_profiles/ImmortalsOfAveum-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/ImmortalsOfAveum-Win64-Shipping.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.06499993056058884, - "convergence": 0.39999979734420776, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.06499993056058884, - "user_convergence": 0.39999979734420776 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.06499993056058884, + "convergence": 0.39999979734420776, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.06499993056058884, + "user_convergence": 0.39999979734420776 + } + ] +} diff --git a/vrto3d_profiles/JediSurvivor.exe_config.json b/vrto3d_profiles/JediSurvivor.exe_config.json index 24e44fe2..cfae2110 100644 --- a/vrto3d_profiles/JediSurvivor.exe_config.json +++ b/vrto3d_profiles/JediSurvivor.exe_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.4, - "convergence": 4.0, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.4, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.4, + "convergence": 4.0, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.4, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 4.0 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 4.0 + } + ] +} diff --git a/vrto3d_profiles/KIN.exe_config.json b/vrto3d_profiles/KIN.exe_config.json index d91af8de..c8684179 100644 --- a/vrto3d_profiles/KIN.exe_config.json +++ b/vrto3d_profiles/KIN.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.15000173449516296, - "convergence": 0.48000335693359375, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.15000173449516296, - "user_convergence": 0.48000335693359375 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.15000173449516296, + "convergence": 0.48000335693359375, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.15000173449516296, + "user_convergence": 0.48000335693359375 + } + ] +} diff --git a/vrto3d_profiles/Kiborg-Win64-Shipping.exe_config.json b/vrto3d_profiles/Kiborg-Win64-Shipping.exe_config.json index 8c107709..3a7e4067 100644 --- a/vrto3d_profiles/Kiborg-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/Kiborg-Win64-Shipping.exe_config.json @@ -1,30 +1,30 @@ -{ - "hmd_height": 1.0, - "fov": 80.0, - "depth": 0.4000000059604645, - "convergence": 4.0, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.4000000059604645, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 80.0, + "depth": 0.4000000059604645, + "convergence": 4.0, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.4000000059604645, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 4.0 + } + ] +} diff --git a/vrto3d_profiles/KunitsuGami.exe_config.json b/vrto3d_profiles/KunitsuGami.exe_config.json index bd44b479..b2c685ef 100644 --- a/vrto3d_profiles/KunitsuGami.exe_config.json +++ b/vrto3d_profiles/KunitsuGami.exe_config.json @@ -1,44 +1,44 @@ -{ - "hmd_height": 1.0, - "fov": 50.0, - "depth": 0.15, - "convergence": 1.5, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "hold", - "pitch_radius": 1.5, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 0.30000001192092896, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.15000000596046448, - "user_convergence": 1.5 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.4000000059604645, - "user_convergence": 3.0 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.4000000059604645, - "user_convergence": 3.0 - }, - { - "user_load_key": "VK_NUMPAD3", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "toggle", - "user_depth": 0.02, - "user_convergence": 2.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 50.0, + "depth": 0.15, + "convergence": 1.5, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "hold", + "pitch_radius": 1.5, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 0.30000001192092896, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.15000000596046448, + "user_convergence": 1.5 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.4000000059604645, + "user_convergence": 3.0 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.4000000059604645, + "user_convergence": 3.0 + }, + { + "user_load_key": "Numpad3", + "user_store_key": "Numpad6", + "user_key_type": "toggle", + "user_depth": 0.02, + "user_convergence": 2.0 + } + ] +} diff --git a/vrto3d_profiles/LT.exe_config.json b/vrto3d_profiles/LT.exe_config.json index 06eaf816..25249ed3 100644 --- a/vrto3d_profiles/LT.exe_config.json +++ b/vrto3d_profiles/LT.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.07000157237052917, - "convergence": 0.27000004053115845, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 0.8500001430511475, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.07000157237052917, - "user_convergence": 0.27000004053115845 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.07000157237052917, + "convergence": 0.27000004053115845, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 0.8500001430511475, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.07000157237052917, + "user_convergence": 0.27000004053115845 + } + ] +} diff --git a/vrto3d_profiles/LostEmber-Win64-Shipping.exe_config.json b/vrto3d_profiles/LostEmber-Win64-Shipping.exe_config.json index 91644688..93aae5ae 100644 --- a/vrto3d_profiles/LostEmber-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/LostEmber-Win64-Shipping.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.2500019371509552, - "convergence": 1.0400028228759766, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.2500019371509552, - "user_convergence": 1.0400028228759766 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.2500019371509552, + "convergence": 1.0400028228759766, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.2500019371509552, + "user_convergence": 1.0400028228759766 + } + ] +} diff --git a/vrto3d_profiles/Mervils.exe_config.json b/vrto3d_profiles/Mervils.exe_config.json index 6e1398da..364ebbdf 100644 --- a/vrto3d_profiles/Mervils.exe_config.json +++ b/vrto3d_profiles/Mervils.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.20000183582305908, - "convergence": 1.070002794265747, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.20000183582305908, - "user_convergence": 1.070002794265747 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.20000183582305908, + "convergence": 1.070002794265747, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_Guide", + "ctrl_toggle_key": "Pad_RS", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.20000183582305908, + "user_convergence": 1.070002794265747 + } + ] +} diff --git a/vrto3d_profiles/MonsterHunterRise.exe_config.json b/vrto3d_profiles/MonsterHunterRise.exe_config.json index 24e44fe2..cfae2110 100644 --- a/vrto3d_profiles/MonsterHunterRise.exe_config.json +++ b/vrto3d_profiles/MonsterHunterRise.exe_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.4, - "convergence": 4.0, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.4, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.4, + "convergence": 4.0, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.4, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 4.0 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 4.0 + } + ] +} diff --git a/vrto3d_profiles/NINJAGAIDEN2BLACK-Win64-Shipping_config.json b/vrto3d_profiles/NINJAGAIDEN2BLACK-Win64-Shipping_config.json index 9e535207..95c5597c 100644 --- a/vrto3d_profiles/NINJAGAIDEN2BLACK-Win64-Shipping_config.json +++ b/vrto3d_profiles/NINJAGAIDEN2BLACK-Win64-Shipping_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 80.0, - "depth": 0.25000035762786865, - "convergence": 1.4999991655349731, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.25000035762786865, - "user_convergence": 1.4999991655349731 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.1, - "user_convergence": 1.4999991655349731 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.1, - "user_convergence": 1.4999991655349731 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 80.0, + "depth": 0.25000035762786865, + "convergence": 1.4999991655349731, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.25000035762786865, + "user_convergence": 1.4999991655349731 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.1, + "user_convergence": 1.4999991655349731 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.1, + "user_convergence": 1.4999991655349731 + } + ] +} diff --git a/vrto3d_profiles/NINJAGAIDEN2BLACK-WinGDK-Shipping.exe_config.json b/vrto3d_profiles/NINJAGAIDEN2BLACK-WinGDK-Shipping.exe_config.json index 9e535207..95c5597c 100644 --- a/vrto3d_profiles/NINJAGAIDEN2BLACK-WinGDK-Shipping.exe_config.json +++ b/vrto3d_profiles/NINJAGAIDEN2BLACK-WinGDK-Shipping.exe_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 80.0, - "depth": 0.25000035762786865, - "convergence": 1.4999991655349731, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.25000035762786865, - "user_convergence": 1.4999991655349731 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.1, - "user_convergence": 1.4999991655349731 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.1, - "user_convergence": 1.4999991655349731 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 80.0, + "depth": 0.25000035762786865, + "convergence": 1.4999991655349731, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.25000035762786865, + "user_convergence": 1.4999991655349731 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.1, + "user_convergence": 1.4999991655349731 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.1, + "user_convergence": 1.4999991655349731 + } + ] +} diff --git a/vrto3d_profiles/OblivionRemastered-Win64-Shipping.exe_config.json b/vrto3d_profiles/OblivionRemastered-Win64-Shipping.exe_config.json index b4d6a29b..db40430d 100644 --- a/vrto3d_profiles/OblivionRemastered-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/OblivionRemastered-Win64-Shipping.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.091000035405159, - "convergence": 0.8699994683265686, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.091000035405159, - "user_convergence": 0.8699994683265686 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.091000035405159, + "convergence": 0.8699994683265686, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.091000035405159, + "user_convergence": 0.8699994683265686 + } + ] +} diff --git a/vrto3d_profiles/OblivionRemastered-WinGDK-Shipping.exe_config.json b/vrto3d_profiles/OblivionRemastered-WinGDK-Shipping.exe_config.json index b4d6a29b..db40430d 100644 --- a/vrto3d_profiles/OblivionRemastered-WinGDK-Shipping.exe_config.json +++ b/vrto3d_profiles/OblivionRemastered-WinGDK-Shipping.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.091000035405159, - "convergence": 0.8699994683265686, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.091000035405159, - "user_convergence": 0.8699994683265686 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.091000035405159, + "convergence": 0.8699994683265686, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.091000035405159, + "user_convergence": 0.8699994683265686 + } + ] +} diff --git a/vrto3d_profiles/OmegaAgent.exe_config.json b/vrto3d_profiles/OmegaAgent.exe_config.json index 8cf6cb2c..ec515594 100644 --- a/vrto3d_profiles/OmegaAgent.exe_config.json +++ b/vrto3d_profiles/OmegaAgent.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 110.0, - "depth": 0.10000163316726685, - "convergence": 1.06999933719635, - "pitch_enable": true, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.10000163316726685, - "user_convergence": 1.06999933719635 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 110.0, + "depth": 0.10000163316726685, + "convergence": 1.06999933719635, + "pitch_enable": true, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.10000163316726685, + "user_convergence": 1.06999933719635 + } + ] +} diff --git a/vrto3d_profiles/Outlaws.exe_config.json b/vrto3d_profiles/Outlaws.exe_config.json index 045d863e..101036ec 100644 --- a/vrto3d_profiles/Outlaws.exe_config.json +++ b/vrto3d_profiles/Outlaws.exe_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 75.0, - "depth": 0.14999990165233612, - "convergence": 1.75, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.14999990165233612, - "user_convergence": 1.75 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.05999999865889549, - "user_convergence": 1.75 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.05999999865889549, - "user_convergence": 1.75 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 75.0, + "depth": 0.14999990165233612, + "convergence": 1.75, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.14999990165233612, + "user_convergence": 1.75 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.05999999865889549, + "user_convergence": 1.75 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.05999999865889549, + "user_convergence": 1.75 + } + ] +} diff --git a/vrto3d_profiles/P3R.exe_config.json b/vrto3d_profiles/P3R.exe_config.json index 303b04ea..3c86eccf 100644 --- a/vrto3d_profiles/P3R.exe_config.json +++ b/vrto3d_profiles/P3R.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 70.0, - "depth": 0.15000173449516296, - "convergence": 1.3100025653839111, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "hold", - "pitch_radius": 1.5, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 0.30000001192092896, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.15000173449516296, - "user_convergence": 1.3100025653839111 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 70.0, + "depth": 0.15000173449516296, + "convergence": 1.3100025653839111, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "hold", + "pitch_radius": 1.5, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 0.30000001192092896, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.15000173449516296, + "user_convergence": 1.3100025653839111 + } + ] +} diff --git a/vrto3d_profiles/Palworld-Win64-Shipping.exe_config.json b/vrto3d_profiles/Palworld-Win64-Shipping.exe_config.json index 9ebe32d8..aecbd631 100644 --- a/vrto3d_profiles/Palworld-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/Palworld-Win64-Shipping.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.2500019371509552, - "convergence": 1.604954719543457, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.2500019371509552, - "user_convergence": 1.604954719543457 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.2500019371509552, + "convergence": 1.604954719543457, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.2500019371509552, + "user_convergence": 1.604954719543457 + } + ] +} diff --git a/vrto3d_profiles/Palworld-WinGDK-Shipping.exe_config.json b/vrto3d_profiles/Palworld-WinGDK-Shipping.exe_config.json index 9ebe32d8..aecbd631 100644 --- a/vrto3d_profiles/Palworld-WinGDK-Shipping.exe_config.json +++ b/vrto3d_profiles/Palworld-WinGDK-Shipping.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.2500019371509552, - "convergence": 1.604954719543457, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.2500019371509552, - "user_convergence": 1.604954719543457 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.2500019371509552, + "convergence": 1.604954719543457, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.2500019371509552, + "user_convergence": 1.604954719543457 + } + ] +} diff --git a/vrto3d_profiles/Polaris-Win64-Shipping.exe_config.json b/vrto3d_profiles/Polaris-Win64-Shipping.exe_config.json index e60152f1..34e09b67 100644 --- a/vrto3d_profiles/Polaris-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/Polaris-Win64-Shipping.exe_config.json @@ -1,24 +1,24 @@ -{ - "hmd_height": 1.0, - "fov": 70.0, - "depth": 0.15000009536743164, - "convergence": 1.4999996423721313, - "async_enable": false, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.15000009536743164, - "user_convergence": 1.4999996423721313 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 70.0, + "depth": 0.15000009536743164, + "convergence": 1.4999996423721313, + "async_enable": false, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.15000009536743164, + "user_convergence": 1.4999996423721313 + } + ] +} diff --git a/vrto3d_profiles/PolyRunnerVR.exe_config.json b/vrto3d_profiles/PolyRunnerVR.exe_config.json index af1bfafc..75e8a219 100644 --- a/vrto3d_profiles/PolyRunnerVR.exe_config.json +++ b/vrto3d_profiles/PolyRunnerVR.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.4000000059604645, - "convergence": 4.0, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.4000000059604645, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.4000000059604645, + "convergence": 4.0, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_RS", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.4000000059604645, + "user_convergence": 4.0 + } + ] +} diff --git a/vrto3d_profiles/PsychonautsInTheRhombusOfRuin-Win64-Shipping.exe_config.json b/vrto3d_profiles/PsychonautsInTheRhombusOfRuin-Win64-Shipping.exe_config.json index ae0291eb..94797750 100644 --- a/vrto3d_profiles/PsychonautsInTheRhombusOfRuin-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/PsychonautsInTheRhombusOfRuin-Win64-Shipping.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 0.0, - "fov": 90.0, - "depth": 0.15000173449516296, - "convergence": 0.5000033378601074, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.15000173449516296, - "user_convergence": 0.5000033378601074 - } - ] -} \ No newline at end of file +{ + "hmd_height": 0.0, + "fov": 90.0, + "depth": 0.15000173449516296, + "convergence": 0.5000033378601074, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.15000173449516296, + "user_convergence": 0.5000033378601074 + } + ] +} diff --git a/vrto3d_profiles/REZ.exe_config.json b/vrto3d_profiles/REZ.exe_config.json index bc4f4252..c41ad7ab 100644 --- a/vrto3d_profiles/REZ.exe_config.json +++ b/vrto3d_profiles/REZ.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.20000186562538147, - "convergence": 0.770003080368042, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.20000186562538147, - "user_convergence": 0.770003080368042 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.20000186562538147, + "convergence": 0.770003080368042, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.20000186562538147, + "user_convergence": 0.770003080368042 + } + ] +} diff --git a/vrto3d_profiles/RaceTheSun.exe_config.json b/vrto3d_profiles/RaceTheSun.exe_config.json index f841e04b..94c2d498 100644 --- a/vrto3d_profiles/RaceTheSun.exe_config.json +++ b/vrto3d_profiles/RaceTheSun.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.4000000059604645, - "convergence": 2.0200018882751465, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.4000000059604645, - "user_convergence": 2.0200018882751465 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.4000000059604645, + "convergence": 2.0200018882751465, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_RS", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.4000000059604645, + "user_convergence": 2.0200018882751465 + } + ] +} diff --git a/vrto3d_profiles/Returnal-Win64-Shipping.exe_config.json b/vrto3d_profiles/Returnal-Win64-Shipping.exe_config.json index 98b49c99..1c333713 100644 --- a/vrto3d_profiles/Returnal-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/Returnal-Win64-Shipping.exe_config.json @@ -1,51 +1,51 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.4, - "convergence": 4.0, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.4, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.1, - "user_convergence": 4.0 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.1, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_LEFT_TRIGGER", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.1, - "user_convergence": 4.0 - }, - { - "user_load_key": "VK_RMOUSE", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.1, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.4, + "convergence": 4.0, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.4, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.1, + "user_convergence": 4.0 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.1, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_LT", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.1, + "user_convergence": 4.0 + }, + { + "user_load_key": "Mouse_Right", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.1, + "user_convergence": 4.0 + } + ] +} diff --git a/vrto3d_profiles/RoboCop-Win64-Shipping.exe_config.json b/vrto3d_profiles/RoboCop-Win64-Shipping.exe_config.json index 9c4c2533..0a0a2926 100644 --- a/vrto3d_profiles/RoboCop-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/RoboCop-Win64-Shipping.exe_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.07000000029802322, - "convergence": 0.4000000059604645, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.07000000029802322, - "user_convergence": 0.4000000059604645 - }, - { - "user_load_key": "XINPUT_GAMEPAD_LEFT_TRIGGER", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.016001608222723007, - "user_convergence": 0.4000000059604645 - }, - { - "user_load_key": "VK_RMOUSE", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.016001608222723007, - "user_convergence": 0.4000000059604645 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.07000000029802322, + "convergence": 0.4000000059604645, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.07000000029802322, + "user_convergence": 0.4000000059604645 + }, + { + "user_load_key": "Pad_LT", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.016001608222723007, + "user_convergence": 0.4000000059604645 + }, + { + "user_load_key": "Mouse_Right", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.016001608222723007, + "user_convergence": 0.4000000059604645 + } + ] +} diff --git a/vrto3d_profiles/SB-Win64-Shipping.exe_config.json b/vrto3d_profiles/SB-Win64-Shipping.exe_config.json index 16375c0d..b2084c08 100644 --- a/vrto3d_profiles/SB-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/SB-Win64-Shipping.exe_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.2999996542930603, - "convergence": 1.9999991655349731, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.2999996542930603, - "user_convergence": 1.9999991655349731 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.09999999403953552, - "user_convergence": 1.9999991655349731 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.09999999403953552, - "user_convergence": 1.9999991655349731 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.2999996542930603, + "convergence": 1.9999991655349731, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.2999996542930603, + "user_convergence": 1.9999991655349731 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.09999999403953552, + "user_convergence": 1.9999991655349731 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.09999999403953552, + "user_convergence": 1.9999991655349731 + } + ] +} diff --git a/vrto3d_profiles/SandFall-Win64-Shipping.exe_config.json b/vrto3d_profiles/SandFall-Win64-Shipping.exe_config.json index 838eb25e..98b0ac96 100644 --- a/vrto3d_profiles/SandFall-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/SandFall-Win64-Shipping.exe_config.json @@ -1,30 +1,30 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.2500002980232239, - "convergence": 1.800002098083496, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.2500002980232239, - "user_convergence": 1.800002098083496 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 1.800002098083496 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.2500002980232239, + "convergence": 1.800002098083496, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.2500002980232239, + "user_convergence": 1.800002098083496 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 1.800002098083496 + } + ] +} diff --git a/vrto3d_profiles/SandFall-WinGDK-Shipping.exe_config.json b/vrto3d_profiles/SandFall-WinGDK-Shipping.exe_config.json index 838eb25e..98b0ac96 100644 --- a/vrto3d_profiles/SandFall-WinGDK-Shipping.exe_config.json +++ b/vrto3d_profiles/SandFall-WinGDK-Shipping.exe_config.json @@ -1,30 +1,30 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.2500002980232239, - "convergence": 1.800002098083496, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.2500002980232239, - "user_convergence": 1.800002098083496 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 1.800002098083496 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.2500002980232239, + "convergence": 1.800002098083496, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.2500002980232239, + "user_convergence": 1.800002098083496 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 1.800002098083496 + } + ] +} diff --git a/vrto3d_profiles/SenzaPeso-Win64-Test.exe_config.json b/vrto3d_profiles/SenzaPeso-Win64-Test.exe_config.json index e8df6770..8d7daac8 100644 --- a/vrto3d_profiles/SenzaPeso-Win64-Test.exe_config.json +++ b/vrto3d_profiles/SenzaPeso-Win64-Test.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.35, - "convergence": 3.0, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.35, - "user_convergence": 3.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.35, + "convergence": 3.0, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.35, + "user_convergence": 3.0 + } + ] +} diff --git a/vrto3d_profiles/SkySquadron.exe_config.json b/vrto3d_profiles/SkySquadron.exe_config.json index 6521842c..df0b72de 100644 --- a/vrto3d_profiles/SkySquadron.exe_config.json +++ b/vrto3d_profiles/SkySquadron.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.10000163316726685, - "convergence": 0.5300033092498779, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.10000163316726685, - "user_convergence": 0.5300033092498779 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.10000163316726685, + "convergence": 0.5300033092498779, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_RS", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.10000163316726685, + "user_convergence": 0.5300033092498779 + } + ] +} diff --git a/vrto3d_profiles/SonicRacingCrossWorldsSteam.exe_config.json b/vrto3d_profiles/SonicRacingCrossWorldsSteam.exe_config.json index d271e102..e9094271 100644 --- a/vrto3d_profiles/SonicRacingCrossWorldsSteam.exe_config.json +++ b/vrto3d_profiles/SonicRacingCrossWorldsSteam.exe_config.json @@ -1,24 +1,24 @@ -{ - "hmd_height": 1.0, - "fov": 75.0, - "depth": 0.15000015497207642, - "convergence": 1.4999996423721313, - "async_enable": false, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.15000015497207642, - "user_convergence": 1.4999996423721313 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 75.0, + "depth": 0.15000015497207642, + "convergence": 1.4999996423721313, + "async_enable": false, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.15000015497207642, + "user_convergence": 1.4999996423721313 + } + ] +} diff --git a/vrto3d_profiles/Spider-Man.exe_config.json b/vrto3d_profiles/Spider-Man.exe_config.json index 90f26ece..08d8448f 100644 --- a/vrto3d_profiles/Spider-Man.exe_config.json +++ b/vrto3d_profiles/Spider-Man.exe_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 65.0, - "depth": 0.2500019371509552, - "convergence": 2.0, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.2500019371509552, - "user_convergence": 2.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 2.0 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 2.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 65.0, + "depth": 0.2500019371509552, + "convergence": 2.0, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.2500019371509552, + "user_convergence": 2.0 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 2.0 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 2.0 + } + ] +} diff --git a/vrto3d_profiles/Starfield.exe_config.json b/vrto3d_profiles/Starfield.exe_config.json index 0d10e95f..3abfd0f9 100644 --- a/vrto3d_profiles/Starfield.exe_config.json +++ b/vrto3d_profiles/Starfield.exe_config.json @@ -1,41 +1,41 @@ -{ - "hmd_height": 1.0, - "fov": 75.0, - "depth": 0.035000041127204895, - "convergence": 0.4600003957748413, - "async_enable": false, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "hold", - "pitch_radius": 1.5, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 0.30000001192092896, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.035000041127204895, - "user_convergence": 0.4600003957748413, - "user_fov": 75.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_LEFT_TRIGGER", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.0030016060918569565, - "user_convergence": 0.4600003957748413, - "user_fov": 75.0 - }, - { - "user_load_key": "VK_RMOUSE", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.0030016060918569565, - "user_convergence": 0.4600003957748413, - "user_fov": 75.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 75.0, + "depth": 0.035000041127204895, + "convergence": 0.4600003957748413, + "async_enable": false, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "hold", + "pitch_radius": 1.5, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 0.30000001192092896, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.035000041127204895, + "user_convergence": 0.4600003957748413, + "user_fov": 75.0 + }, + { + "user_load_key": "Pad_LT", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.0030016060918569565, + "user_convergence": 0.4600003957748413, + "user_fov": 75.0 + }, + { + "user_load_key": "Mouse_Right", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.0030016060918569565, + "user_convergence": 0.4600003957748413, + "user_fov": 75.0 + } + ] +} diff --git a/vrto3d_profiles/StillWakesTheDeep.exe_config.json b/vrto3d_profiles/StillWakesTheDeep.exe_config.json index af2169b3..91a221fc 100644 --- a/vrto3d_profiles/StillWakesTheDeep.exe_config.json +++ b/vrto3d_profiles/StillWakesTheDeep.exe_config.json @@ -1,30 +1,30 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.065, - "convergence": 2.5, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.065, - "user_convergence": 2.5 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.025, - "user_convergence": 2.5 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.065, + "convergence": 2.5, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.065, + "user_convergence": 2.5 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.025, + "user_convergence": 2.5 + } + ] +} diff --git a/vrto3d_profiles/Subnautica.exe_config.json b/vrto3d_profiles/Subnautica.exe_config.json index 2e50ea92..038a85f1 100644 --- a/vrto3d_profiles/Subnautica.exe_config.json +++ b/vrto3d_profiles/Subnautica.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 0.0, - "fov": 90.0, - "depth": 0.06500156223773956, - "convergence": 0.5599997639656067, - "pitch_enable": true, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.06500156223773956, - "user_convergence": 0.5599997639656067 - } - ] -} \ No newline at end of file +{ + "hmd_height": 0.0, + "fov": 90.0, + "depth": 0.06500156223773956, + "convergence": 0.5599997639656067, + "pitch_enable": true, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.06500156223773956, + "user_convergence": 0.5599997639656067 + } + ] +} diff --git a/vrto3d_profiles/THUMPER_win8.exe_config.json b/vrto3d_profiles/THUMPER_win8.exe_config.json index 86e08b32..f0672448 100644 --- a/vrto3d_profiles/THUMPER_win8.exe_config.json +++ b/vrto3d_profiles/THUMPER_win8.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.15000173449516296, - "convergence": 0.8700029850006104, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.15000173449516296, - "user_convergence": 0.8700029850006104 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.15000173449516296, + "convergence": 0.8700029850006104, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.15000173449516296, + "user_convergence": 0.8700029850006104 + } + ] +} diff --git a/vrto3d_profiles/TetrisEffect-Win64-Shipping.exe_config.json b/vrto3d_profiles/TetrisEffect-Win64-Shipping.exe_config.json index 5134093c..046cfb80 100644 --- a/vrto3d_profiles/TetrisEffect-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/TetrisEffect-Win64-Shipping.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 80.0, - "depth": 0.06500156223773956, - "convergence": 0.590003252029419, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.06500156223773956, - "user_convergence": 0.590003252029419 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 80.0, + "depth": 0.06500156223773956, + "convergence": 0.590003252029419, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.06500156223773956, + "user_convergence": 0.590003252029419 + } + ] +} diff --git a/vrto3d_profiles/TheFalconeerVR.exe_config.json b/vrto3d_profiles/TheFalconeerVR.exe_config.json index f329f150..e22cd533 100644 --- a/vrto3d_profiles/TheFalconeerVR.exe_config.json +++ b/vrto3d_profiles/TheFalconeerVR.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.20000183582305908, - "convergence": 0.8500030040740967, - "pitch_enable": true, - "yaw_enable": false, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.20000183582305908, - "user_convergence": 0.8500030040740967 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.20000183582305908, + "convergence": 0.8500030040740967, + "pitch_enable": true, + "yaw_enable": false, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.20000183582305908, + "user_convergence": 0.8500030040740967 + } + ] +} diff --git a/vrto3d_profiles/TheGreatCircle.exe_config.json b/vrto3d_profiles/TheGreatCircle.exe_config.json index 3ea18a17..fbc48ef9 100644 --- a/vrto3d_profiles/TheGreatCircle.exe_config.json +++ b/vrto3d_profiles/TheGreatCircle.exe_config.json @@ -1,24 +1,24 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.039999764412641525, - "convergence": 0.3500000238418579, - "async_enable": false, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.039999764412641525, - "user_convergence": 0.3500000238418579 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.039999764412641525, + "convergence": 0.3500000238418579, + "async_enable": false, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.039999764412641525, + "user_convergence": 0.3500000238418579 + } + ] +} diff --git a/vrto3d_profiles/Theseus-Win64-Shipping.exe_config.json b/vrto3d_profiles/Theseus-Win64-Shipping.exe_config.json index 80e8f7c4..5000ee8b 100644 --- a/vrto3d_profiles/Theseus-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/Theseus-Win64-Shipping.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.2500019371509552, - "convergence": 1.1200027465820313, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.2500019371509552, - "user_convergence": 1.1200027465820313 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.2500019371509552, + "convergence": 1.1200027465820313, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.2500019371509552, + "user_convergence": 1.1200027465820313 + } + ] +} diff --git a/vrto3d_profiles/Touring Karts.exe_config.json b/vrto3d_profiles/Touring Karts.exe_config.json index ace760cc..31bc2cdd 100644 --- a/vrto3d_profiles/Touring Karts.exe_config.json +++ b/vrto3d_profiles/Touring Karts.exe_config.json @@ -1,30 +1,30 @@ -{ - "hmd_height": 1.0, - "fov": 110.0, - "depth": 0.10000000149011612, - "convergence": 0.330000638961792, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.4000000059604645, - "user_convergence": 4.0 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 110.0, + "depth": 0.10000000149011612, + "convergence": 0.330000638961792, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Pad_Guide", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.4000000059604645, + "user_convergence": 4.0 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 4.0 + } + ] +} diff --git a/vrto3d_profiles/UmiByeVR.exe_config.json b/vrto3d_profiles/UmiByeVR.exe_config.json index ad951ff0..871f8810 100644 --- a/vrto3d_profiles/UmiByeVR.exe_config.json +++ b/vrto3d_profiles/UmiByeVR.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 0.0, - "fov": 90.0, - "depth": 0.07000160217285156, - "convergence": 0.29000353813171387, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.07000160217285156, - "user_convergence": 0.29000353813171387 - } - ] -} \ No newline at end of file +{ + "hmd_height": 0.0, + "fov": 90.0, + "depth": 0.07000160217285156, + "convergence": 0.29000353813171387, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_RS", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.07000160217285156, + "user_convergence": 0.29000353813171387 + } + ] +} diff --git a/vrto3d_profiles/WRC.exe_config.json b/vrto3d_profiles/WRC.exe_config.json index 116cbf9d..a121db45 100644 --- a/vrto3d_profiles/WRC.exe_config.json +++ b/vrto3d_profiles/WRC.exe_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.06500156223773956, - "convergence": 0.6900031566619873, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.06500156223773956, - "user_convergence": 0.6900031566619873 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.3000012934207916, - "user_convergence": 2.23000168800354 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.3000012934207916, - "user_convergence": 2.23000168800354 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.06500156223773956, + "convergence": 0.6900031566619873, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.06500156223773956, + "user_convergence": 0.6900031566619873 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.3000012934207916, + "user_convergence": 2.23000168800354 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.3000012934207916, + "user_convergence": 2.23000168800354 + } + ] +} diff --git a/vrto3d_profiles/WW.exe_config.json b/vrto3d_profiles/WW.exe_config.json index af1bfafc..75e8a219 100644 --- a/vrto3d_profiles/WW.exe_config.json +++ b/vrto3d_profiles/WW.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.4000000059604645, - "convergence": 4.0, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.4000000059604645, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.4000000059604645, + "convergence": 4.0, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_RS", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.4000000059604645, + "user_convergence": 4.0 + } + ] +} diff --git a/vrto3d_profiles/Windlands_Win_x64.exe_config.json b/vrto3d_profiles/Windlands_Win_x64.exe_config.json index 376ce32a..0f8fe16e 100644 --- a/vrto3d_profiles/Windlands_Win_x64.exe_config.json +++ b/vrto3d_profiles/Windlands_Win_x64.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 0.5, - "fov": 90.0, - "depth": 0.20000019669532776, - "convergence": 2.6199984550476074, - "pitch_enable": true, - "yaw_enable": false, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.20000019669532776, - "user_convergence": 2.6199984550476074 - } - ] -} \ No newline at end of file +{ + "hmd_height": 0.5, + "fov": 90.0, + "depth": 0.20000019669532776, + "convergence": 2.6199984550476074, + "pitch_enable": true, + "yaw_enable": false, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.20000019669532776, + "user_convergence": 2.6199984550476074 + } + ] +} diff --git a/vrto3d_profiles/Zoe2Mars.exe_config.json b/vrto3d_profiles/Zoe2Mars.exe_config.json index 255f37ad..b461c779 100644 --- a/vrto3d_profiles/Zoe2Mars.exe_config.json +++ b/vrto3d_profiles/Zoe2Mars.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 110.0, - "depth": 0.06999996304512024, - "convergence": 0.5699999332427979, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.06999996304512024, - "user_convergence": 0.5699999332427979 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 110.0, + "depth": 0.06999996304512024, + "convergence": 0.5699999332427979, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.06999996304512024, + "user_convergence": 0.5699999332427979 + } + ] +} diff --git a/vrto3d_profiles/atmmhg.exe_config.json b/vrto3d_profiles/atmmhg.exe_config.json index 2d365adb..b1c2f8df 100644 --- a/vrto3d_profiles/atmmhg.exe_config.json +++ b/vrto3d_profiles/atmmhg.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.10000163316726685, - "convergence": 0.7200031280517578, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.10000163316726685, - "user_convergence": 0.7200031280517578 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.10000163316726685, + "convergence": 0.7200031280517578, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Pad_RS", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.10000163316726685, + "user_convergence": 0.7200031280517578 + } + ] +} diff --git a/vrto3d_profiles/b1-Win64-Shipping.exe_config.json b/vrto3d_profiles/b1-Win64-Shipping.exe_config.json index b2eadbe0..4283a20a 100644 --- a/vrto3d_profiles/b1-Win64-Shipping.exe_config.json +++ b/vrto3d_profiles/b1-Win64-Shipping.exe_config.json @@ -1,37 +1,41 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.4000000059604645, - "convergence": 4.0, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.4000000059604645, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 65.0, + "depth": 0.24999991059303284, + "convergence": 1.999954342842102, + "async_enable": false, + "auto_depth_enabled": true, + "auto_depth_target_disparity": 0.004999999888241291, + "auto_depth_smoothing": 0.07999999821186066, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_key_type": "switch", + "user_depth": 0.24999991059303284, + "user_convergence": 1.999954342842102, + "user_fov": 0.0 + }, + { + "user_load_key": "Pad_Guide", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 1.999954342842102, + "user_fov": 0.0 + }, + { + "user_load_key": "Numpad2", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 1.999954342842102, + "user_fov": 0.0 + } + ] +} diff --git a/vrto3d_profiles/ds.exe_config.json b/vrto3d_profiles/ds.exe_config.json index 2ebe9b1b..ceeff80d 100644 --- a/vrto3d_profiles/ds.exe_config.json +++ b/vrto3d_profiles/ds.exe_config.json @@ -1,44 +1,44 @@ -{ - "hmd_height": 1.0, - "fov": 75.0, - "depth": 0.15000000596046448, - "convergence": 1.75, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.15000000596046448, - "user_convergence": 1.75 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.06499962508678436, - "user_convergence": 1.75 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.06499962508678436, - "user_convergence": 1.75 - }, - { - "user_load_key": "VK_NUMPAD3", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "toggle", - "user_depth": 0.015, - "user_convergence": 1.75 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 75.0, + "depth": 0.15000000596046448, + "convergence": 1.75, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.15000000596046448, + "user_convergence": 1.75 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.06499962508678436, + "user_convergence": 1.75 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.06499962508678436, + "user_convergence": 1.75 + }, + { + "user_load_key": "Numpad3", + "user_store_key": "Numpad6", + "user_key_type": "toggle", + "user_depth": 0.015, + "user_convergence": 1.75 + } + ] +} diff --git a/vrto3d_profiles/dyingsun.exe_config.json b/vrto3d_profiles/dyingsun.exe_config.json index f035f424..3dab5864 100644 --- a/vrto3d_profiles/dyingsun.exe_config.json +++ b/vrto3d_profiles/dyingsun.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 110.0, - "depth": 0.07000157237052917, - "convergence": 0.5600032806396484, - "pitch_enable": true, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.07000157237052917, - "user_convergence": 0.5600032806396484 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 110.0, + "depth": 0.07000157237052917, + "convergence": 0.5600032806396484, + "pitch_enable": true, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.07000157237052917, + "user_convergence": 0.5600032806396484 + } + ] +} diff --git a/vrto3d_profiles/eldenring.exe_config.json b/vrto3d_profiles/eldenring.exe_config.json index 2cfa1817..def4ccef 100644 --- a/vrto3d_profiles/eldenring.exe_config.json +++ b/vrto3d_profiles/eldenring.exe_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 75.0, - "depth": 0.2500019371509552, - "convergence": 1.7499985694885254, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.2500019371509552, - "user_convergence": 1.7499985694885254 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 1.7499985694885254 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.10000000149011612, - "user_convergence": 1.7499985694885254 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 75.0, + "depth": 0.2500019371509552, + "convergence": 1.7499985694885254, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.2500019371509552, + "user_convergence": 1.7499985694885254 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 1.7499985694885254 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.10000000149011612, + "user_convergence": 1.7499985694885254 + } + ] +} diff --git a/vrto3d_profiles/ff7rebirth_.exe_config.json b/vrto3d_profiles/ff7rebirth_.exe_config.json index 428fc03b..e9bec69e 100644 --- a/vrto3d_profiles/ff7rebirth_.exe_config.json +++ b/vrto3d_profiles/ff7rebirth_.exe_config.json @@ -1,38 +1,38 @@ -{ - "hmd_height": 1.0, - "fov": 62.0, - "depth": 0.2, - "convergence": 3.0, - "async_enable": false, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.2, - "user_convergence": 3.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.1, - "user_convergence": 3.0 - }, - { - "user_load_key": "VK_NUMPAD3", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.1, - "user_convergence": 3.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 62.0, + "depth": 0.2, + "convergence": 3.0, + "async_enable": false, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.2, + "user_convergence": 3.0 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.1, + "user_convergence": 3.0 + }, + { + "user_load_key": "Numpad3", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.1, + "user_convergence": 3.0 + } + ] +} diff --git a/vrto3d_profiles/gzdoomvr.exe_config.json b/vrto3d_profiles/gzdoomvr.exe_config.json index a8b5ae62..6dc7b8fe 100644 --- a/vrto3d_profiles/gzdoomvr.exe_config.json +++ b/vrto3d_profiles/gzdoomvr.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.07000157237052917, - "convergence": 0.3399999737739563, - "pitch_enable": true, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.07000157237052917, - "user_convergence": 0.3399999737739563 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.07000157237052917, + "convergence": 0.3399999737739563, + "pitch_enable": true, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.07000157237052917, + "user_convergence": 0.3399999737739563 + } + ] +} diff --git a/vrto3d_profiles/makaimura_GG_RE.exe_config.json b/vrto3d_profiles/makaimura_GG_RE.exe_config.json index dc42e8bb..e25151f0 100644 --- a/vrto3d_profiles/makaimura_GG_RE.exe_config.json +++ b/vrto3d_profiles/makaimura_GG_RE.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 130.0, - "depth": 0.9999906420707703, - "convergence": 2.5050106048583984, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.9999906420707703, - "user_convergence": 2.5050106048583984 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 130.0, + "depth": 0.9999906420707703, + "convergence": 2.5050106048583984, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_RS", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.9999906420707703, + "user_convergence": 2.5050106048583984 + } + ] +} diff --git a/vrto3d_profiles/pCARS2AVX.exe_config.json b/vrto3d_profiles/pCARS2AVX.exe_config.json index cf4d880a..d23afcdc 100644 --- a/vrto3d_profiles/pCARS2AVX.exe_config.json +++ b/vrto3d_profiles/pCARS2AVX.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.06500156223773956, - "convergence": 0.9299994111061096, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.06500156223773956, - "user_convergence": 0.9299994111061096 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.06500156223773956, + "convergence": 0.9299994111061096, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_RS", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.06500156223773956, + "user_convergence": 0.9299994111061096 + } + ] +} diff --git a/vrto3d_profiles/pCARS64.exe_config.json b/vrto3d_profiles/pCARS64.exe_config.json index 9eff937e..8ff33f93 100644 --- a/vrto3d_profiles/pCARS64.exe_config.json +++ b/vrto3d_profiles/pCARS64.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.15000173449516296, - "convergence": 0.45000338554382324, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.15000173449516296, - "user_convergence": 0.45000338554382324 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.15000173449516296, + "convergence": 0.45000338554382324, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_RS", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.15000173449516296, + "user_convergence": 0.45000338554382324 + } + ] +} diff --git a/vrto3d_profiles/re4.exe_config.json b/vrto3d_profiles/re4.exe_config.json index 86a1e77a..3cd977f4 100644 --- a/vrto3d_profiles/re4.exe_config.json +++ b/vrto3d_profiles/re4.exe_config.json @@ -1,51 +1,51 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.10000000149011612, - "convergence": 4.0, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.25, - "user_convergence": 4.0 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.25, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_LEFT_TRIGGER", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - }, - { - "user_load_key": "VK_RMOUSE", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.10000000149011612, + "convergence": 4.0, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.10000000149011612, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.25, + "user_convergence": 4.0 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.25, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_LT", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.10000000149011612, + "user_convergence": 4.0 + }, + { + "user_load_key": "Mouse_Right", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.10000000149011612, + "user_convergence": 4.0 + } + ] +} diff --git a/vrto3d_profiles/re4demo.exe_config.json b/vrto3d_profiles/re4demo.exe_config.json index 86a1e77a..3cd977f4 100644 --- a/vrto3d_profiles/re4demo.exe_config.json +++ b/vrto3d_profiles/re4demo.exe_config.json @@ -1,51 +1,51 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.10000000149011612, - "convergence": 4.0, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.25, - "user_convergence": 4.0 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.25, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_LEFT_TRIGGER", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - }, - { - "user_load_key": "VK_RMOUSE", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.10000000149011612, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.10000000149011612, + "convergence": 4.0, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.10000000149011612, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.25, + "user_convergence": 4.0 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.25, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_LT", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.10000000149011612, + "user_convergence": 4.0 + }, + { + "user_load_key": "Mouse_Right", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.10000000149011612, + "user_convergence": 4.0 + } + ] +} diff --git a/vrto3d_profiles/re7.exe_config.json b/vrto3d_profiles/re7.exe_config.json index 878ad565..d0bb884a 100644 --- a/vrto3d_profiles/re7.exe_config.json +++ b/vrto3d_profiles/re7.exe_config.json @@ -1,51 +1,51 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.07, - "convergence": 4.0, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.07, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.1, - "user_convergence": 4.0 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.1, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_LEFT_TRIGGER", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.05, - "user_convergence": 4.0 - }, - { - "user_load_key": "VK_RMOUSE", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.05, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.07, + "convergence": 4.0, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.07, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.1, + "user_convergence": 4.0 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.1, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_LT", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.05, + "user_convergence": 4.0 + }, + { + "user_load_key": "Mouse_Right", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.05, + "user_convergence": 4.0 + } + ] +} diff --git a/vrto3d_profiles/re8.exe_config.json b/vrto3d_profiles/re8.exe_config.json index 878ad565..d0bb884a 100644 --- a/vrto3d_profiles/re8.exe_config.json +++ b/vrto3d_profiles/re8.exe_config.json @@ -1,51 +1,51 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.07, - "convergence": 4.0, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.07, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.1, - "user_convergence": 4.0 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.1, - "user_convergence": 4.0 - }, - { - "user_load_key": "XINPUT_GAMEPAD_LEFT_TRIGGER", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.05, - "user_convergence": 4.0 - }, - { - "user_load_key": "VK_RMOUSE", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.05, - "user_convergence": 4.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.07, + "convergence": 4.0, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.07, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.1, + "user_convergence": 4.0 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.1, + "user_convergence": 4.0 + }, + { + "user_load_key": "Pad_LT", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.05, + "user_convergence": 4.0 + }, + { + "user_load_key": "Mouse_Right", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.05, + "user_convergence": 4.0 + } + ] +} diff --git a/vrto3d_profiles/starwarssquadrons.exe_config.json b/vrto3d_profiles/starwarssquadrons.exe_config.json index 76ae014a..5a8de2e6 100644 --- a/vrto3d_profiles/starwarssquadrons.exe_config.json +++ b/vrto3d_profiles/starwarssquadrons.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 110.0, - "depth": 0.10000000149011612, - "convergence": 1.0, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.10000000149011612, - "user_convergence": 1.0 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 110.0, + "depth": 0.10000000149011612, + "convergence": 1.0, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.10000000149011612, + "user_convergence": 1.0 + } + ] +} diff --git a/vrto3d_profiles/steamtours.exe_config.json b/vrto3d_profiles/steamtours.exe_config.json index 7900ed94..9f41ce05 100644 --- a/vrto3d_profiles/steamtours.exe_config.json +++ b/vrto3d_profiles/steamtours.exe_config.json @@ -1,23 +1,23 @@ -{ - "hmd_height": 1.0, - "fov": 90.0, - "depth": 0.07499995082616806, - "convergence": 0.8600001335144043, - "pitch_enable": true, - "yaw_enable": true, - "pose_reset_key": "XINPUT_GAMEPAD_RIGHT_THUMB", - "ctrl_toggle_key": "XINPUT_GAMEPAD_GUIDE", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 0.7000002861022949, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.07499995082616806, - "user_convergence": 0.8600001335144043 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 90.0, + "depth": 0.07499995082616806, + "convergence": 0.8600001335144043, + "pitch_enable": true, + "yaw_enable": true, + "pose_reset_key": "Pad_RS", + "ctrl_toggle_key": "Pad_Guide", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 0.7000002861022949, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.07499995082616806, + "user_convergence": 0.8600001335144043 + } + ] +} diff --git a/vrto3d_profiles/tll.exe_config.json b/vrto3d_profiles/tll.exe_config.json index ad6d7ced..4ce06314 100644 --- a/vrto3d_profiles/tll.exe_config.json +++ b/vrto3d_profiles/tll.exe_config.json @@ -1,51 +1,51 @@ -{ - "hmd_height": 1.0, - "fov": 70.0, - "depth": 0.25, - "convergence": 2.5, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.25, - "user_convergence": 2.5 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.075, - "user_convergence": 2.5 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.075, - "user_convergence": 2.5 - }, - { - "user_load_key": "XINPUT_GAMEPAD_LEFT_TRIGGER", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.15, - "user_convergence": 2.5 - }, - { - "user_load_key": "VK_RMOUSE", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.15, - "user_convergence": 2.5 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 70.0, + "depth": 0.25, + "convergence": 2.5, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.25, + "user_convergence": 2.5 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.075, + "user_convergence": 2.5 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.075, + "user_convergence": 2.5 + }, + { + "user_load_key": "Pad_LT", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.15, + "user_convergence": 2.5 + }, + { + "user_load_key": "Mouse_Right", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.15, + "user_convergence": 2.5 + } + ] +} diff --git a/vrto3d_profiles/tlou-i.exe_config.json b/vrto3d_profiles/tlou-i.exe_config.json index b8b3f314..d1b482c2 100644 --- a/vrto3d_profiles/tlou-i.exe_config.json +++ b/vrto3d_profiles/tlou-i.exe_config.json @@ -1,37 +1,37 @@ -{ - "hmd_height": 1.0, - "fov": 75.0, - "depth": 0.06499995291233063, - "convergence": 0.8499995470046997, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.06499995291233063, - "user_convergence": 0.8499995470046997 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.032, - "user_convergence": 0.8499995470046997 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.032, - "user_convergence": 0.8499995470046997 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 75.0, + "depth": 0.06499995291233063, + "convergence": 0.8499995470046997, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.06499995291233063, + "user_convergence": 0.8499995470046997 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.032, + "user_convergence": 0.8499995470046997 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.032, + "user_convergence": 0.8499995470046997 + } + ] +} diff --git a/vrto3d_profiles/u4.exe_config.json b/vrto3d_profiles/u4.exe_config.json index ad6d7ced..4ce06314 100644 --- a/vrto3d_profiles/u4.exe_config.json +++ b/vrto3d_profiles/u4.exe_config.json @@ -1,51 +1,51 @@ -{ - "hmd_height": 1.0, - "fov": 70.0, - "depth": 0.25, - "convergence": 2.5, - "pitch_enable": false, - "yaw_enable": false, - "pose_reset_key": "VK_NUMPAD7", - "ctrl_toggle_key": "VK_NUMPAD8", - "ctrl_toggle_type": "toggle", - "pitch_radius": 0.0, - "ctrl_deadzone": 0.05000000074505806, - "ctrl_sensitivity": 1.0, - "user_settings": [ - { - "user_load_key": "VK_NUMPAD1", - "user_store_key": "VK_NUMPAD4", - "user_key_type": "switch", - "user_depth": 0.25, - "user_convergence": 2.5 - }, - { - "user_load_key": "XINPUT_GAMEPAD_GUIDE", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.075, - "user_convergence": 2.5 - }, - { - "user_load_key": "VK_NUMPAD2", - "user_store_key": "VK_NUMPAD5", - "user_key_type": "toggle", - "user_depth": 0.075, - "user_convergence": 2.5 - }, - { - "user_load_key": "XINPUT_GAMEPAD_LEFT_TRIGGER", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.15, - "user_convergence": 2.5 - }, - { - "user_load_key": "VK_RMOUSE", - "user_store_key": "VK_NUMPAD6", - "user_key_type": "hold", - "user_depth": 0.15, - "user_convergence": 2.5 - } - ] -} \ No newline at end of file +{ + "hmd_height": 1.0, + "fov": 70.0, + "depth": 0.25, + "convergence": 2.5, + "pitch_enable": false, + "yaw_enable": false, + "pose_reset_key": "Numpad7", + "ctrl_toggle_key": "Numpad8", + "ctrl_toggle_type": "toggle", + "pitch_radius": 0.0, + "ctrl_deadzone": 0.05000000074505806, + "ctrl_sensitivity": 1.0, + "user_settings": [ + { + "user_load_key": "Numpad1", + "user_store_key": "Numpad4", + "user_key_type": "switch", + "user_depth": 0.25, + "user_convergence": 2.5 + }, + { + "user_load_key": "Pad_Guide", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.075, + "user_convergence": 2.5 + }, + { + "user_load_key": "Numpad2", + "user_store_key": "Numpad5", + "user_key_type": "toggle", + "user_depth": 0.075, + "user_convergence": 2.5 + }, + { + "user_load_key": "Pad_LT", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.15, + "user_convergence": 2.5 + }, + { + "user_load_key": "Mouse_Right", + "user_store_key": "Numpad6", + "user_key_type": "hold", + "user_depth": 0.15, + "user_convergence": 2.5 + } + ] +}