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 fbf23749..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
@@ -107,6 +113,11 @@ jobs:
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:
@@ -116,6 +127,7 @@ 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
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 c84046b8..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/
diff --git a/README.md b/README.md
index cf7bc0ad..98e2ee05 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
- Supports User Profiles for individual games
- Provides HMD Pitch and Yaw emulation for games that require it
- Currently targeting OpenVR 2.15.6.
-- Windows-only solution, but there are other solutions on Linux like Monado XR.
+- Runs on Windows and natively on Linux — see the [Linux](#linux) section for platform-specific setup
## Compatible 3D Displays & Output Modes
@@ -38,6 +38,10 @@ Find your display type and use the listed Output Mode(s):
- 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
@@ -48,6 +52,7 @@ Find your display type and use the listed Output Mode(s):
- `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
@@ -66,6 +71,7 @@ Find your display type and use the listed Output Mode(s):
- 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
@@ -75,6 +81,7 @@ Find your display type and use the listed Output Mode(s):
- **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
@@ -86,6 +93,7 @@ Find your display type and use the listed Output Mode(s):
- 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
@@ -97,6 +105,7 @@ Find your display type and use the listed Output Mode(s):
- `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.
- 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)
@@ -105,6 +114,7 @@ Find your display type and use the listed Output Mode(s):
- **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
@@ -115,6 +125,7 @@ Find your display type and use the listed Output Mode(s):
- 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
@@ -125,6 +136,7 @@ Find your display type and use the listed Output Mode(s):
- 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
@@ -133,6 +145,7 @@ Find your display type and use the listed Output Mode(s):
- **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
@@ -184,13 +197,54 @@ User-defined preset hotkeys (configured under [User Presets](#user-presets-via-o
- 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 (`hide_cursor`/`lock_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 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 |
@@ -235,6 +289,7 @@ User-defined preset hotkeys (configured under [User Presets](#user-presets-via-o
| `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` |
@@ -302,7 +357,7 @@ User-defined preset hotkeys (configured under [User Presets](#user-presets-via-o
- 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_mappings.h directly
+ - `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
@@ -317,7 +372,7 @@ User-defined preset hotkeys (configured under [User Presets](#user-presets-via-o
- 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
+ - 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
@@ -360,11 +415,32 @@ User-defined preset hotkeys (configured under [User Presets](#user-presets-via-o
- 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/VRto3DLib b/external/VRto3DLib
index 05ee3879..01ff76fa 160000
--- a/external/VRto3DLib
+++ b/external/VRto3DLib
@@ -1 +1 @@
-Subproject commit 05ee38790c2c2a3c5f9812d49bc152cb5585f858
+Subproject commit 01ff76fa028e955f5be0729aa0eb8b04ef1ba3ce
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/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("
+
+
+ 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
index cd707aaf..eb4a99c9 100644
--- a/vrto3d/src/auto_depth.cpp
+++ b/vrto3d/src/auto_depth.cpp
@@ -30,7 +30,9 @@
#include "hmd_device_driver.h" // StereoDisplayComponent::FeedAutoDepthSample
-// Disparity histogram — 128 buckets covering up to 512px (stride=4).
+// 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
@@ -95,8 +97,11 @@ void CSMain(uint3 DTid : SV_DispatchThreadID, uint GI : SV_GroupIndex)
[unroll] for (int i = -BLOCK_HALF; i <= BLOCK_HALF; ++i)
Lb[i + BLOCK_HALF] = LoadL(x + i, y);
- // Search positive disparity: right-eye match is to the LEFT of
- // the left-eye column. d=0 == infinity, growing d == closer.
+ // 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;
@@ -111,8 +116,21 @@ void CSMain(uint3 DTid : SV_DispatchThreadID, uint GI : SV_GroupIndex)
float3 dC = abs(Lb[j + BLOCK_HALF] - R);
cost += dC.r + dC.g + dC.b;
}
- if (cost < bestCost) { secondBest = bestCost; bestCost = cost; bestD = d; }
- else if (cost < secondBest){ secondBest = cost; }
+ // 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
@@ -280,9 +298,15 @@ void AutoDepthAnalyzer::Run(ID3D11DeviceContext* ctx,
if (sbs_w == 0 || sbs_h == 0) return;
if (!EnsureResources(sbs)) return;
- constexpr uint32_t kStride = 4; // quarter-res
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.
{
@@ -349,21 +373,26 @@ void AutoDepthAnalyzer::Run(ID3D11DeviceContext* ctx,
for (int b = NUM_BUCKETS - 12; b < NUM_BUCKETS; ++b) tail_sum += hist[b];
const uint32_t tail_floor = tail_sum / 12u;
- // 3-bucket window must clear several thresholds simultaneously:
- // (a) absolute floor (150 hits)
- // (b) ~3.3% of total
+ // 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)({
- 150u,
- match_count / 30u,
- 3u * 5u * mean_per_bucket,
- 4u * 3u * tail_floor});
- for (int b = NUM_BUCKETS - 2; b >= 1; --b) {
- const uint32_t window = hist[b - 1] + hist[b] + hist[b + 1];
+ 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;
@@ -371,16 +400,23 @@ void AutoDepthAnalyzer::Run(ID3D11DeviceContext* ctx,
}
}
- component->FeedAutoDepthSample(max_disp, eye_w);
+ // 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:";
diff --git a/vrto3d/src/device_provider.cpp b/vrto3d/src/device_provider.cpp
index 0185e761..e3771f35 100644
--- a/vrto3d/src/device_provider.cpp
+++ b/vrto3d/src/device_provider.cpp
@@ -18,6 +18,7 @@
#define WIN32_LEAN_AND_MEAN
#include
#include
+#ifdef _WIN32
#include
#include
#include
@@ -26,6 +27,17 @@
#include "device_provider.h"
#include "dx11_renderer.h"
#include "direct_mode_component.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 {
@@ -57,6 +69,7 @@ void ScheduleAutoExitCheck(uint32_t pid)
//-----------------------------------------------------------------------------
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
@@ -70,6 +83,18 @@ vr::EVRInitError MyDeviceProvider::Init( vr::IVRDriverContext *pDriverContext )
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.
@@ -142,18 +167,38 @@ void MyDeviceProvider::RunFrame()
auto lowerAppName = appName;
std::transform(lowerAppName.begin(), lowerAppName.end(), lowerAppName.begin(), ::tolower);
- if (Skip_Processes.find(lowerAppName) == 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;
g_current_app_pid.store(app_pid_);
- LOG() << "AppName = " << app_name_.c_str();
+ 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 ||
@@ -225,10 +270,17 @@ 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;
diff --git a/vrto3d/src/focus_policy.h b/vrto3d/src/focus_policy.h
new file mode 100644
index 00000000..9b06ba6b
--- /dev/null
+++ b/vrto3d/src/focus_policy.h
@@ -0,0 +1,77 @@
+/*
+ * 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 both the Windows
+// WindowPresenter::FocusThreadLoop and the Linux VkRenderer present-thread
+// focus block so the two 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 ca90baba..ee372797 100644
--- a/vrto3d/src/hmd_device_driver.cpp
+++ b/vrto3d/src/hmd_device_driver.cpp
@@ -17,21 +17,33 @@
#define WIN32_LEAN_AND_MEAN
#include "hmd_device_driver.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/key_mappings.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"
+#ifdef _WIN32
#include "vrto3dlib/win32_helper.hpp"
+#else
+#include "vrto3dlib/linux_helper.hpp"
+#endif
#include "vrmath.h"
#include
@@ -41,11 +53,60 @@
#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
@@ -111,7 +172,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")
@@ -135,6 +202,9 @@ vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId )
// needs activating.
is_active_.store(true);
device_index_ = unObjectId;
+ // 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;
app_updated_ = false;
@@ -218,9 +288,13 @@ vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId )
// 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)
@@ -236,7 +310,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"(
@@ -335,7 +413,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";
@@ -362,6 +444,7 @@ vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId )
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
@@ -369,19 +452,37 @@ 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 DX11 renderer + selected presenter on the same
- // adapter LUID we advertised on the HMD's property container, then stand
- // up the IVRDriverDirectModeComponent that hands per-eye game textures
- // through to the renderer.
+ // 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();
- renderer_ = std::make_unique();
- if (!renderer_->Init(luid, stereo_display_component_->GetConfig(), GetFocusContext())) {
- LOG() << "Dx11Renderer::Init failed; direct-mode path will be inactive";
+ 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());
+ 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;
@@ -436,6 +537,34 @@ vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId )
}
}
};
+ 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);
+ if (renderer_ && renderer_->Osd()) renderer_->Osd()->SetText(toast);
+ };
cb.reset_projection = [this]() {
stereo_display_component_->ResetProjection();
};
@@ -464,11 +593,13 @@ vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId )
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
@@ -487,6 +618,11 @@ vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId )
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
@@ -507,7 +643,9 @@ vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId )
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;
@@ -521,6 +659,23 @@ vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId )
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.set_async = [this](bool on) {
SetAsync(on);
};
@@ -561,6 +716,29 @@ vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId )
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 =
@@ -621,6 +799,7 @@ vr::EVRInitError MockControllerDeviceDriver::Activate( uint32_t unObjectId )
} else {
toast("Profiles installed to config\\vrto3d");
}
+#endif
}).detach();
};
@@ -672,7 +851,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;
@@ -684,6 +867,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) {
@@ -715,6 +899,26 @@ 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
@@ -745,7 +949,9 @@ void MockControllerDeviceDriver::OpenTrackThread()
else std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
closesocket(socket_s);
+#ifdef _WIN32
WSACleanup();
+#endif
}
@@ -1050,6 +1256,16 @@ 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();
@@ -1085,23 +1301,25 @@ void MockControllerDeviceDriver::PollHotkeysThread() {
// 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 (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
@@ -1162,8 +1380,16 @@ void MockControllerDeviceDriver::PollHotkeysThread() {
--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;
@@ -1205,6 +1431,19 @@ void MockControllerDeviceDriver::PollHotkeysThread() {
no_profile_ = false;
}
+ // 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
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
}
@@ -1327,6 +1566,13 @@ void MockControllerDeviceDriver::MonitorModeThread() {
//-----------------------------------------------------------------------------
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;
while (is_active_.load(std::memory_order_relaxed)) {
@@ -1395,6 +1641,7 @@ void MockControllerDeviceDriver::CursorControlThread()
if (clip_active) {
ClipCursor(nullptr);
}
+#endif
}
@@ -1788,9 +2035,11 @@ bool StereoDisplayComponent::IsAutoDepthEnabled() const
void StereoDisplayComponent::SetAutoDepthEnabled(bool enabled)
{
auto_depth_enabled_.store(enabled, std::memory_order_relaxed);
- // Reset the input-side disparity EMA so the next enable starts fresh
- // rather than carrying stale samples from a previous session.
+ // 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));
@@ -1844,16 +2093,33 @@ void StereoDisplayComponent::SetAutoDepthLoggingEnabled(bool enabled)
}
-void StereoDisplayComponent::FeedAutoDepthSample(uint32_t max_disp_px, uint32_t eye_w_px)
+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
@@ -1861,18 +2127,20 @@ void StereoDisplayComponent::FeedAutoDepthSample(uint32_t max_disp_px, uint32_t
// 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: ignore changes under 0.5% of eye width (~10 source pixels at
- // 1920 wide). Frame-to-frame bucket jitter at the integer-disparity
- // resolution (4px = 0.2% of eye width) sits well inside this band.
- constexpr float kDeadband = 0.005f;
+ // 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 = raw_frac; // first sample primes the filter
+ new_ema = med_frac; // first sample primes the filter
} else {
- const float diff = raw_frac - prev_ema;
- if (std::fabs(diff) < kDeadband) {
+ 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;
@@ -1881,25 +2149,35 @@ void StereoDisplayComponent::FeedAutoDepthSample(uint32_t max_disp_px, uint32_t
}
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 < 0.0f) target_depth = 0.0f;
+ 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;
- ApplyDepth(smoothed);
+ // 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) {
@@ -1909,7 +2187,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();
}
diff --git a/vrto3d/src/hmd_device_driver.h b/vrto3d/src/hmd_device_driver.h
index c234a9e5..cc1bb14e 100644
--- a/vrto3d/src/hmd_device_driver.h
+++ b/vrto3d/src/hmd_device_driver.h
@@ -31,8 +31,20 @@
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
@@ -55,7 +67,7 @@ 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();
@@ -77,7 +89,10 @@ class StereoDisplayComponent : public vr::IVRDisplayComponent
// disparity logs. Off by default — only useful when tuning thresholds.
bool IsAutoDepthLoggingEnabled() const;
void SetAutoDepthLoggingEnabled(bool enabled);
- void FeedAutoDepthSample(uint32_t max_disp_px, uint32_t eye_w_px);
+ // 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);
@@ -126,6 +141,15 @@ class StereoDisplayComponent : public vr::IVRDisplayComponent
// -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 };
};
@@ -182,8 +206,8 @@ class MockControllerDeviceDriver : public vr::ITrackedDeviceServerDriver
// 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.
- Dx11Renderer* GetRenderer() { return renderer_.get(); }
- DirectModeComponent* GetDirectModeComponent() { return direct_mode_component_.get(); }
+ StereoRenderer* GetRenderer() { return renderer_.get(); }
+ StereoDirectMode* GetDirectModeComponent() { return direct_mode_component_.get(); }
private:
std::unique_ptr< StereoDisplayComponent > stereo_display_component_;
@@ -200,8 +224,8 @@ class MockControllerDeviceDriver : public vr::ITrackedDeviceServerDriver
std::atomic< bool > is_active_;
std::atomic< uint32_t > device_index_; // HMD object id (from Activate)
- std::unique_ptr< Dx11Renderer > renderer_;
- std::unique_ptr< DirectModeComponent > direct_mode_component_;
+ std::unique_ptr< StereoRenderer > renderer_;
+ std::unique_ptr< StereoDirectMode > direct_mode_component_;
std::atomic< bool > is_on_top_;
std::atomic< bool > man_on_top_;
// Snapshot of man_on_top_ at the moment of ProcessDisconnected, so a
diff --git a/vrto3d/src/hmd_driver_factory.cpp b/vrto3d/src/hmd_driver_factory.cpp
index 2df21e4a..6538918a 100644
--- a/vrto3d/src/hmd_driver_factory.cpp
+++ b/vrto3d/src/hmd_driver_factory.cpp
@@ -18,8 +18,13 @@
#include "openvr_driver.h"
#include
+#ifdef _WIN32
#define HMD_DLL_EXPORT extern "C" __declspec( dllexport )
#define HMD_DLL_IMPORT extern "C" __declspec( dllimport )
+#else
+#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
index 556dd5bd..9d06d2da 100644
--- a/vrto3d/src/osd/osd_input.cpp
+++ b/vrto3d/src/osd/osd_input.cpp
@@ -17,7 +17,6 @@
#include "osd/osd_input.h"
#include
-#include
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
@@ -26,7 +25,7 @@
#include
#include "imgui.h"
-#include "vrto3dlib/key_mappings.h"
+#include "vrto3dlib/key_names.h"
namespace vrto3d::osd {
@@ -135,30 +134,15 @@ ImGuiKey VkToImGuiKey(int vk) {
return ImGuiKey_None;
}
-// Reverse lookup: VK_*/XINPUT_GAMEPAD_* numeric code → its name string in
-// VRto3DLib's VirtualKeyMappings / XInputMappings tables. Used by the
-// click-to-capture key picker to round-trip a captured key into the same
-// string format that JsonManager parses.
-const std::unordered_map& KeyCodeToName() {
- static const auto map = []{
- std::unordered_map m;
- for (auto& kv : VirtualKeyMappings) m.emplace(kv.second, kv.first);
- for (auto& kv : XInputMappings) m.emplace(kv.second, kv.first);
- return m;
- }();
- return map;
-}
-
+// 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) {
- auto& map = KeyCodeToName();
- auto it = map.find(vk);
- return it != map.end() ? it->second : std::string{};
+ return vrto3d::keys::NameFromKeyCode(vk);
}
std::string NameForPadMask(uint32_t mask) {
- auto& map = KeyCodeToName();
- auto it = map.find(static_cast(mask));
- return it != map.end() ? it->second : std::string{};
+ return vrto3d::keys::NameFromPadBits(static_cast(mask));
}
} // namespace
diff --git a/vrto3d/src/osd/osd_input.h b/vrto3d/src/osd/osd_input.h
index 19696ae6..da01d42b 100644
--- a/vrto3d/src/osd/osd_input.h
+++ b/vrto3d/src/osd/osd_input.h
@@ -47,8 +47,8 @@ struct OsdSurface {
};
// One-shot capture result returned by OsdInput::PollCapture().
-// `key_name` is a raw VK_*/XINPUT_GAMEPAD_* string (or '+'-joined combo) that
-// VRto3DLib's VirtualKeyMappings / XInputMappings can resolve back to a code.
+// `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;
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
index a555a8c1..01641628 100644
--- a/vrto3d/src/osd/osd_menu.cpp
+++ b/vrto3d/src/osd/osd_menu.cpp
@@ -27,9 +27,8 @@
#include "hmd_device_driver.h"
#include "platform.h"
#include "vrto3dlib/stereo_config.h"
-#include "vrto3dlib/key_mappings.h"
-
-#include
+#include "vrto3dlib/key_codes.h"
+#include "vrto3dlib/key_names.h"
namespace vrto3d::osd {
@@ -56,6 +55,11 @@ struct OsdMenu::Impl {
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;
@@ -362,7 +366,7 @@ void OsdMenu::Impl::DrawStereoTab() {
bool eye_swap = cfg.eye_swap;
if (ImGui::Checkbox("Swap Eyes", &eye_swap)) {
cfg.eye_swap = eye_swap;
- component->LoadSettings(cfg);
+ ApplyConfig(cfg);
}
}
@@ -385,23 +389,17 @@ std::string FloatsToCsv(const std::vector& v, int prec = 3) {
// 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) {
- code = 0;
- if (xinput) *xinput = false;
- if (s.empty()) return;
- if (VirtualKeyMappings.find(s) != VirtualKeyMappings.end()) {
- code = VirtualKeyMappings[s];
- if (xinput) *xinput = false;
- return;
- }
- if (XInputMappings.find(s) != XInputMappings.end() || s.find('+') != std::string::npos) {
- std::stringstream ss(s);
- std::string tok;
- while (std::getline(ss, tok, '+')) {
- auto it = XInputMappings.find(tok);
- if (it != XInputMappings.end()) code |= it->second;
- }
- if (xinput) *xinput = true;
- }
+ // 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) {
@@ -421,6 +419,27 @@ std::vector CsvToFloats(const char* s) {
} // 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.
// ---------------------------------------------------------------------------
@@ -636,7 +655,7 @@ void OsdMenu::Impl::DrawUserHotkeysTab(OsdInput& input) {
ImGui::TextDisabled("Save profile from the footer to persist changes.");
if (dirty) {
- component->LoadSettings(cfg);
+ ApplyConfig(cfg);
}
}
@@ -775,8 +794,16 @@ void OsdMenu::Impl::DrawTrackingTab(OsdInput& input) {
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();
@@ -807,10 +834,12 @@ void OsdMenu::Impl::DrawTrackingTab(OsdInput& input) {
cfg.sr_track_mode = track_modes[sel];
dirty = true;
}
+ ImGui::EndDisabled();
}
+#endif // _WIN32 (LeiaSR head tracking)
if (dirty) {
- component->LoadSettings(cfg);
+ ApplyConfig(cfg);
}
}
@@ -868,7 +897,7 @@ void OsdMenu::Impl::DrawShaderTab() {
}
if (dirty) {
- component->LoadSettings(cfg);
+ ApplyConfig(cfg);
}
}
@@ -899,21 +928,51 @@ void OsdMenu::Impl::DrawSystemTab() {
ImGui::SameLine();
if (ImGui::SmallButton("Refresh##displays")) RefreshMonitors();
- // Mirror the OutputMode enum order. Keep in sync with stereo_config.h.
- static const char* modes[] = {
- "SbS", "TaB", "RowInterlaced", "ColInterlaced", "Checkerboard",
- "LeiaSR", "NvidiaDX9", "WibbleWobble", "VirtualDesktop",
- "FramePacked720p60", "FramePacked1080p24", "FramePacked1080p60", "FramePacked1080p60CVT",
- "DualDisplay", "DualDisplayFlip",
- "AnaglyphRedCyan", "AnaglyphRedCyanDubois", "AnaglyphRedCyanDeghosted", "AnaglyphRedCyanCompromise",
- "AnaglyphGreenMagenta", "AnaglyphGreenMagentaDubois", "AnaglyphGreenMagentaDeghosted",
- "AnaglyphBlueAmber",
- "Mono",
+ // 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},
};
- int mode_sel = static_cast(cfg.output_mode);
- if (mode_sel < 0 || mode_sel >= IM_ARRAYSIZE(modes)) mode_sel = 0;
- if (ImGui::Combo("Output Mode", &mode_sel, modes, IM_ARRAYSIZE(modes))) {
- cfg.output_mode = static_cast(mode_sel);
+ 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;
}
@@ -930,11 +989,18 @@ void OsdMenu::Impl::DrawSystemTab() {
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);
@@ -947,16 +1013,27 @@ void OsdMenu::Impl::DrawSystemTab() {
}
ImGui::SameLine();
ImGui::TextDisabled("(clip cursor to game window while focused)");
+#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 (Ctrl+Home / Start+DPad-Down still toggle menu)",
+ 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();
@@ -986,20 +1063,7 @@ void OsdMenu::Impl::DrawSystemTab() {
}
if (dirty) {
- // Re-derive load/store keys from the (possibly edited) symbolic
- // strings so changes take effect immediately, not just on reload.
- 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;
- auto kt = KeyBindTypes.find(cfg.user_type_str[i]);
- if (kt != KeyBindTypes.end()) cfg.user_key_type[i] = kt->second;
- }
- ReparseHotkey(cfg.pose_reset_str, cfg.pose_reset_key, &cfg.reset_xinput);
- ReparseHotkey(cfg.ctrl_toggle_str, cfg.ctrl_toggle_key, &cfg.ctrl_xinput);
- auto kt = KeyBindTypes.find(cfg.ctrl_type_str);
- if (kt != KeyBindTypes.end()) cfg.ctrl_type = kt->second;
- component->LoadSettings(cfg);
+ ApplyConfig(cfg);
}
}
diff --git a/vrto3d/src/osd/osd_menu.h b/vrto3d/src/osd/osd_menu.h
index 75a04ee6..fce3ac86 100644
--- a/vrto3d/src/osd/osd_menu.h
+++ b/vrto3d/src/osd/osd_menu.h
@@ -40,6 +40,11 @@ struct MenuCallbacks {
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
diff --git a/vrto3d/src/osd/osd_renderer.h b/vrto3d/src/osd/osd_renderer.h
index 10b50185..92bbf5dc 100644
--- a/vrto3d/src/osd/osd_renderer.h
+++ b/vrto3d/src/osd/osd_renderer.h
@@ -17,11 +17,17 @@
#pragma once
#include
+#include
#include
#include
+#ifdef _WIN32
#include
#include
+#else
+#include
+namespace vrto3d::vk { struct DeviceCtx; }
+#endif
class StereoDisplayComponent;
@@ -45,6 +51,7 @@ class OsdRenderer {
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.
@@ -59,6 +66,19 @@ class OsdRenderer {
// 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();
@@ -91,7 +111,15 @@ class OsdRenderer {
// 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);
diff --git a/vrto3d/src/osd/osd_renderer_vk.cpp b/vrto3d/src/osd/osd_renderer_vk.cpp
new file mode 100644
index 00000000..a8c6f49a
--- /dev/null
+++ b/vrto3d/src/osd/osd_renderer_vk.cpp
@@ -0,0 +1,608 @@
+/*
+ * 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::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
index 093cbfe9..a380b465 100644
--- a/vrto3d/src/platform.h
+++ b/vrto3d/src/platform.h
@@ -21,9 +21,11 @@
#include
#include
+#ifdef _WIN32
#include
#include
#include
+#endif
#include "openvr_driver.h"
@@ -54,6 +56,13 @@ bool ResolveTargetMonitors(int32_t display_index,
// 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();
@@ -107,6 +116,7 @@ std::unique_ptr CreatePresentWindow(const MonitorInfo& primary,
// 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).
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/presenter/leiasr_presenter.cpp b/vrto3d/src/presenter/leiasr_presenter.cpp
index 9eb5e2c7..5c2fe704 100644
--- a/vrto3d/src/presenter/leiasr_presenter.cpp
+++ b/vrto3d/src/presenter/leiasr_presenter.cpp
@@ -257,11 +257,11 @@ class LeiaSrTrackPipeline {
// 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 (yaw + roll
- // are pre-inverted to match the OpenTrack receiver).
+ // 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);
+ const float roll = orient_z_rad * to_deg;
yaw_offset_ = -yaw;
pitch_offset_ = -pitch;
roll_offset_ = -roll;
@@ -286,10 +286,11 @@ class LeiaSrTrackPipeline {
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 here so
- // the round-trip matches an external bridge running in OpenTrack mode.
+ // ::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;
- roll_f = -roll_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_);
@@ -813,12 +814,14 @@ void LeiaSrPresenter::HeadTrackingThreadLoop()
// 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.
+ // 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;
+ use_ot = live_cfg.use_open_track && live_cfg.sr_tracking_enabled;
}
if (use_ot && !prev_use_ot && track_pipeline_) {
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