A first build on a Steam Frame left 8.3 GB in its work folder, the
extracted disc and Dawn's build tree included, so asking for 20 GB plus
the disc nearly refused a card with 29 GB free. Ask for 10 GB (3 once
Dawn is built) plus the disc's extraction.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
The first start-to-finish run (on the Frame itself, --frame local, the
work folder on the SD card, a WBFS copied over) built and installed the
game. Its log showed what to tidy:
- The build step said "hours under emulation" on a native build; it now
says which applies.
- curl's progress bars filled a log written to a file with junk lines;
they show only on a terminal now.
- .NET's first-run banner, telemetry notice and developer certificate
are switched off in the build container.
- The run reports how long it took, after the build and at the end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
Drop the details that only described one Frame (its SteamOS version,
memory, sizes, a disc left by an earlier install) and keep what any owner
needs: podman comes with SteamOS, --disc is a path on the Frame, and the
work folder can go on the SD card.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
--disc names a path on the machine the script runs on, so with --frame
local the image has to be copied to the Frame first, or --disc can point
at a disc an earlier install extracted there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
Checked on a Frame with ldd: the executable needs only glibc, libstdc++,
libgcc, libpng and zlib from the system, and no symbol version newer than
SteamOS 0.4.3's glibc 2.39 offers, although the build container has 2.41.
So it runs both from Steam, inside Steam Linux Runtime 4, and straight
from a Desktop Mode terminal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
A survey of a Steam Frame (SteamOS 0.4.3, build 20260930) found podman
5.5.2 set up for rootless use (subuid/subgid for steamos, crun, overlay
storage in the home folder) running aarch64 containers natively, plus
every tool the installer uses there (bash, python3, rsync, curl, tar,
bsdtar, unzip, sftp-server; no 7-Zip, which bsdtar covers). It also found
a nearly full home partition, which is what would break a build first.
- The installer checks the work folder has room before starting: about
20 GB for a first build, 5 once Dawn is built, plus the disc's
extraction. Short of it, it points at --work-dir on the SD card.
- Before copying the disc to the Frame it checks the destination has room
for it, pointing at --frame-disc on the card otherwise.
- $USER may be unset (cron, some containers); the message falls back to
id -un rather than tripping set -u.
- README: building on the Frame is confirmed to work with SteamOS's own
podman, with the work folder on the SD card when home is short; the
sizes involved are given where they matter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
SteamOS's system is read-only and replaced by every update, so the
installer must never send anyone to pacman on the Frame, and everything
it puts there has to live in the home folder.
- On SteamOS, a missing podman/docker or qemu now gets its own message:
nothing can be installed into the system (and should not be, with the
read-only mode switched off), so build on a PC and install over SSH,
or on the Frame itself where podman is present.
- --frame local refuses to run on anything but an ARM64 machine, so it
cannot install onto the PC it was meant to build on.
- The disc folder is checked for being writable before a few GB are
copied; a path in the read-only system gets a clear message instead of
a failed copy.
- Config.toml goes where the game reads it, honouring XDG_DATA_HOME.
- Other image-based systems (Bazzite, Silverblue) get the rpm-ostree hint.
- README: what the installer puts on the Frame (all in the home folder,
surviving updates) and how to remove it; building on the Frame needs
podman to come with SteamOS, without claiming it always does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
Checked every claim that names a setting, default, log line, binding or
script against the source, and brought the docs up to date with the
Steam Frame build:
- README: the recommended settings show their Frame defaults (render
scale 0.8, resolution multiplier 1) and add eye-tracked foveation and
the refresh rate; the settings panel's gamepad-mode button (both
sticks); by-hand release tags moved to frame-beta-2; the AI usage note
points at a section that exists.
- OPENXR.md: the intro and requirements cover all three backends; the
Linux Vulkan backend, its patched Dawn and time conversion; Frame
defaults for render scale and culling; the Frame controller profile in
the binding list and limitations; foveation on the Frame and its
per-map memory; scope of passthrough and tracked hands; Linux log path.
- quest-port.md: Known gaps rewritten (it still said the app had not run
on a Quest, foveation was off by default, haptics unused and the pack
not downloadable); intro names every product sharing the spec.
- DISTRIBUTION.md: how this fork's source-only frame-* releases are made.
- THIRD-PARTY-NOTICES.md: libco's aarch64 backend, Dawn built from source
for the Quest and the Frame, and the tools the Frame installer fetches.
- CONTRIBUTING, RELEASE_VALIDATION, android/README, translator/README,
building-macos: fork context, broken pointers, list formatting, typos.
- aurora-main/README: drop a screenshot that was not vendored.
All relative links and anchors resolve.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
The second beta brings the one-command installer and the README quick
start; the game and Dawn are unchanged since frame-beta-1. The release
workflow's manual run now defaults to this tag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
An archive is recognised by its first bytes, unpacked into the work dir
with whichever unpacker the machine has (7-Zip, bsdtar, unzip, Python's
zipfile, or bsdtar in a container of the machine's own architecture), and
every file in it is offered to nodtool, largest first. The RMCP01 image
wins; another region's disc is passed on so the ID check names it. The
unpacked files are removed once the disc is extracted, or when no disc is
found.
Tested each unpacker with zip and 7z archives holding an RVZ in a
subfolder, an archive holding NTSC and PAL images, an NTSC-only one, one
with no disc, a password-protected zip and truncated archives.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
Tested with a synthetic RMCP01 Wii disc built with nod and converted to
encrypted ISO, WBFS and RVZ: all three extract, and main.dol and
StaticR.rel come out byte-identical. nodtool reads the format from the
file, so a misnamed image works too.
- Stop before building unless boot.bin says RMCP01, naming the release
(RMCE01 and the like) when it is another region; a wrong extraction in
the work dir is removed so the next run starts clean.
- Accept extracted folders whose partition sits in a subfolder (DATA/).
- Explain unreadable files (a .zip, say) instead of nodtool's bare error.
- Extract quietly: the real disc has thousands of files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
Launcher/steam-frame-install.sh does the whole quick start in one command:
it downloads the newest release (or builds the tree it is in), extracts the
disc with nodtool, builds in a long-lived Debian ARM64 container (podman or
docker, under qemu on x86_64), copies the game and disc to the Frame over
SSH (or locally with --frame local), points Config.toml at the disc, and
registers the game with Steam through Valve's devkit tools when present.
Running it again updates: only changed source files are replaced, so the
build recompiles just those and Dawn is reused.
The README's quick start now runs the script; the manual steps move to a
"Building by hand" section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
The quick start, the on-Frame build and updates now download a release's
source tarball. Updates copy only the files whose content changed (rsync -c),
which stamps them with the current time: unpacking over the source would
restore commit dates that can be older than the last build, and the build
would skip the changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
docs/steam-frame.md is gone. The README now opens with a quick start that
goes from the disc to playing (emulation setup, disc extraction, the
container build, installing with Frame Control, updating), and carries what
the guide held that a player or contributor needs: recommended settings, the
Frame controller map, known issues, troubleshooting (log lines, the pacing
line, crash backtraces), building on the Frame or with Docker (including
Unraid's qemu registration), how the native build works and why the Android
flavour cannot show a picture. Links to the old guide point at the README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
ax_effects.cpp calls the guest function func_801284B4 directly, since
upstream commit e7b670b, but the synthetic DOL test never generated a
stand-in for it, so its Windows runtime link failed with an undefined
symbol. This was hidden on openxr-work, where the workflow had not run.
The list now covers every guest function the runtime calls directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
The checkbox is drawn on every platform, but its variable was declared
only under MKW_VR_STANDALONE, so the Windows runtime failed to compile
(settings_overlay.cpp: use of undeclared identifier 'g_vrRepeatFrames').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
Tags cannot always be pushed from where a release is prepared. Run from the
Actions tab, the workflow creates the tag on the run's commit through
gh release create --target instead of requiring a pushed tag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
The README and docs/steam-frame.md now describe the Frame build as a beta
that runs on the headset: what works, the recommended settings (render_scale
1.25 is the panels' native 2160x2160), the known issues (doubled images,
right-eye foveation), building with Docker on another machine (including
registering qemu on hosts whose binfmt_misc is per container), and
installing with Frame Control. A frame-* tag now publishes a source-only
pre-release with the notes in docs/releases/<tag>.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
The first repeat_frames waited a millisecond for the eyes before spending a
refresh on the retained layer, so nearly every packet also cost a needless
repeat, the next packet missed the game's next frame, and the Steam Frame
showed about 35 new frames a second. The pacing thread now waits until
1.5 ms before the next wake, a period after the last xrWaitFrame returned,
which OpenXRRuntime records.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
SteamVR on the Steam Frame ran the game at half the 120 Hz display rate,
since render-first pacing submits only when a game frame is sealed, and
filled every other refresh itself even with Motion Smoothing off, which
doubled the HUD and the menu screen while the head turned. With the new
[vr] repeat_frames (default on for the Frame, off elsewhere, live), the
pacing thread waits a millisecond for the eyes and otherwise spends the
refresh on a keep-alive cycle that resubmits the retained layer with its
rendered poses, paced by xrWaitFrame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
On the Steam Frame the full-density region was too small and lagged the
eyes. Gaze-centred maps now widen both of the level's rings by 8 degrees,
covering a saccade until the next frame's map is bound, the tracker's error
and Turnip's per-bin density, and each eye keeps 128 maps instead of 32 so
fewer glances wait for an upload. The fixed, forward maps are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
On the Steam Frame the game crashed in Turnip while Dawn recorded an eye
render pass, reading an unmapped address. Turnip reads a density map through
a host mapping of its memory taken at bind time, and the maps were
sub-allocated from blocks that Dawn's buffer upload fast path maps and
unmaps. A dedicated allocation is never mapped or unmapped by Dawn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
A SIGSEGV on the Steam Frame logged only the guest state and 'Host stack
trace unavailable on this platform'. With glibc the handler now names the
faulting thread, its pc and lr, and a backtrace, each as module + offset for
addr2line. The Dawn provider also says when a local package (--dawn-package)
replaces the download instead of printing the URL it does not fetch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
The README now introduces this repository as the Steam Frame fork of
WiiCompiled OpenXR VR: what the fork adds, its untested status, the
requirements and build steps, where to report problems, and what it keeps
from upstream. docs/steam-frame.md gains the emulated ARM64 container route
for building on an x86_64 Linux PC, disc extraction with nodtool and the
copy to the Frame, and clones the default branch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
Launcher/build-dawn-linux.sh builds the pinned Dawn with Aurora's Vulkan hook
and density map patches into a package local-build.sh takes through the new
--dawn-package option, together with --openxr, --headset steam_frame and
--cpu. docs/steam-frame.md now leads with the native SteamOS build (building
in a podman container on the Frame, running it, the log lines to expect);
OPENXR.md's backend table records the Linux Vulkan backend and why the Lepton
flavour cannot present.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
MKW_VR_STANDALONE (runtime_config.h) names the standalone headsets: the
Quest, and the Steam Frame in its Android flavour and its native SteamOS
build. They share render scale 0.8, foveation medium, the GX thread,
the game's object culling, and the headset panel's foveation and
tracked-hands rows; passthrough stays Quest-only.
The Frame's native build also sets AuroraConfig::xrHeadsetOnly, so
Aurora neither presents the desktop window nobody watches nor renders
it past the last pass the eyes sample, which Android does always (4 to
6 ms of a 12 ms GPU frame on a Quest 3).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
Lepton's Vulkan driver has no external memory or sync fd extensions,
so the Steam Frame gets a native SteamOS build instead. Its backend is
the PC's: OpenXR creates Dawn's own Vulkan instance and device, and the
eyes are copied into the swapchain on Dawn's queue, with nothing shared
between devices.
- openxr_vulkan_win32 and Aurora's vulkan_win32_interop now compile on
desktop Linux. Dawn links statically there, so the bridge calls the
patched package's hooks directly, and exists only when the package's
aurora-dawn.json declares the Vulkan hook ABI (AuroraDawnProvider
turns AuroraVulkanAbi into AURORA_DAWN_VULKAN_HOOKS); a stock Dawn
keeps the stubs and VR falls back to the desktop.
- openxr_integration.cpp gains a Linux branch: XR_KHR_vulkan_enable2,
the timespec clock, refresh rate, performance level, the Frame
controllers and eye gaze; fragment density maps are requested on
Linux as on the Quest (fdm.cpp and gpu.cpp, AURORA_FDM=0/1 overrides).
- Controller motion uses XR_KHR_convert_timespec_time on any Linux.
- CMake: Vulkan headers are fetched for Linux OpenXR builds, the
headset option is MKW_HEADSET on Android and Linux, and Linux aarch64
builds take MKW_LINUX_CPU (cortex-x4 for the Steam Frame, else native).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
The Capability Viewer inside Lepton shows no AHardwareBuffer, external
memory fd, semaphore fd or fence fd extension, so the Quest backend's
two-device eye handoff cannot present there; the eyes need to be copied
on Dawn's own device bound to the session, as on Windows Vulkan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
Read from Walkabout Mini Golf's Unity log on the Frame: the Frame
controller profile, display refresh rate, eye gaze and performance
settings are all there; XR_KHR_convert_timespec_time is not, so VR
frame interpolation is unavailable on the Frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
Lepton ships /vendor/etc/openxr/1/active_runtime.json and Valve's
implicit fdm_injection layer instead of a runtime broker; the statically
linked Khronos loader falls back to that file, so the app needs no
change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
The Frame's kernel reports sve, sve2 and the SVE2 extensions in its
HWCAP, on SteamOS and inside Lepton alike, unlike phones with the same
Snapdragon 8 Gen 3. The steamFrame flavour now builds with
-mcpu=cortex-x4 instead of cortex-x4+nosve.
docs/steam-frame.md records what the headset reported: 4 KB pages,
Android 11 (API 30) in a Waydroid-based Lepton, Mesa's Turnip as its
Vulkan driver, and SteamVR's native linuxarm64 OpenXR runtime on the
host, with the questions still open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
The adb commands named the device through a PowerShell variable, which
fish and bash do not read; each line now names it, and two grep lines
pull the driver and the relevant extensions out of the vkjson dump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
docs/steam-frame.md covers the steamFrame flavour: the CPU target and
why it excludes SVE, the Lepton entry activity and manifest, the Frame
controller map, the refresh rate and eye-tracked foveation, building and
installing, the device checklist (what to collect from the headset, the
log lines a first session should show, the open questions), and what a
native SteamOS build would need. OPENXR.md documents refresh_rate and
eye_tracked_foveation and the Frame controller profile; the Quest doc,
the Android README and the README point to it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
With [vr] eye_tracked_foveation (on by default on the Steam Frame, off
elsewhere) the runtime asks for XR_EXT_eye_gaze_interaction. When the
system reports an eye tracker, OpenXRInput binds the gaze pose and
locates it for each packet's display time, in the space the eye views
are located in; vr/eye_gaze.h turns it into tangents of each eye's own
view, which AuroraStereoFrame now carries (appended, after the existing
prefix).
Aurora centres the eye's fragment density map on the gaze snapped to a
cell of two map texels (about 3 degrees). Each eye keeps up to 32 maps,
one per cell looked at, so a glance back reuses its map; a new map is
bound once its upload completes, and until then the eye keeps the map
it had. Without a tracked gaze (a blink, no tracker, the setting off)
foveation centres on the forward direction exactly as before: the
forward maps are byte-identical.
Also logs every extension the OpenXR runtime offers at startup, so the
first Steam Frame session shows what SteamVR's Android runtime has.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
SteamVR has no XR_FB_passthrough, so the Steam Frame build neither asks
for it nor offers the setting: [vr] passthrough defaults to false there,
and the F10/headset panel and the launcher hide the switch. The Frame
keeps the Android defaults that suit it (render scale 0.8, foveation
medium, the GX thread, object culling, performance level boost) and
starts at 120 Hz (previous commit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
[vr] refresh_rate (Hz, 0 = the headset's own) is requested through
XR_FB_display_refresh_rate each time the session starts running and
whenever the setting changes, matched against the runtime's own list
within half a hertz. Setting it back to 0 restores the rate the session
started at. A rate that is not offered, or one the runtime declines, is
logged and changes nothing.
The game renders 60 frames a second, so at 120 Hz every frame shows for
exactly two refreshes, where 72 or 90 Hz hold some frames longer than
others. The Steam Frame build defaults to 120; every other build keeps
the headset's own rate unless the player picks one.
The F10/headset panel and the Android launcher's Headset section offer
the choice; the interpolation tooltip points at it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
The Steam Frame runs Android apps through Lepton with SteamVR's OpenXR
runtime. A third headset flavour, steamFrame, targets it:
- -mcpu=cortex-x4+nosve for the Snapdragon 8 Gen 3 (its firmware does
not expose SVE, which clang otherwise auto-vectorises with), from one
flavour-to-CPU map that the kit export now reads instead of guessing
from the variant name.
- MKW_ANDROID_HEADSET=steam_frame defines MKW_HEADSET_STEAM_FRAME for
the runtime's own targets, for Frame-specific defaults.
- A manifest without the Horizon OS entries, and FrameEntryActivity as
the single real MAIN/LAUNCHER activity with the Khronos and Oculus VR
categories, which Lepton needs to start an app in VR. It opens the
setup panel and, when the selected game can start, the game on top.
- XR_VALVE_frame_controller_interaction: the Frame controller profile
with its left D-pad (new dpad_* actions, the Wii Remote's D-pad or the
gamepad's), View as menu and the left shoulder as the panel button.
Also requested on Windows for SteamVR streaming to a Frame.
- Build-Quest.ps1, Build-QuestGame.ps1 and Run-Quest.ps1 take
-Headset frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
- Updated stereo_frame_worker_smoke.cpp to allow dynamic headset rates and prediction lead time.
- Improved logging to include motion diagnostics and adjusted frame submission logic based on headset frequency.
- Enhanced stereo_interpolation_test.cpp with additional tests for camera motion separation and playback cadence.
- Introduced MkwVRReadSceneView function to read the camera view matrix for improved scene rendering.
- Modified VR first-person logic to support scene view reading and validation.
- Added scene_camera.hpp to encapsulate camera motion handling and inverse view calculations.
- Ensured that the VR integration layer correctly logs motion diagnostics and handles scene playback accurately.
- Introduced `vrCockpitItemThrow` configuration option to enable throwing items in VR.
- Implemented `ThrowDetector` and `ThrowSequence` classes to manage item throwing mechanics based on hand movements.
- Updated `OpenXRInput` to handle item throwing based on hand swings, integrating with existing VR input systems.
- Enhanced settings overlay to allow users to toggle item throwing functionality.
- Added tests for item throwing mechanics, ensuring correct behavior for various hand movements and conditions.
- Implemented DVDReadVrAsset function to read mapped disc paths for VR assets.
- Created HeldItem structure and ReadHeldItem function for managing held items in the game.
- Developed unit tests for ReadHeldItem to ensure correct functionality and edge case handling.
- Added cockpit item data tests to validate model indexing and parsing of archives.
- Introduced a new configuration option for object culling in VR, allowing the game to hide objects outside the camera's view.
- Implemented native replacements for the Mario Kart functions responsible for scene culling, ensuring accurate behavior in VR.
- Added a new header file `mkw_vr_culling.h` to define the culling logic and structures.
- Created `mkw_vr_culling.cpp` to implement the culling logic, including frustum intersection checks and screen info updates.
- Updated runtime configuration to include the new object culling option, with appropriate getters and setters.
- Enhanced the settings overlay to allow users to toggle object culling in VR.
- Added tests in `vr_culling_tests.cpp` to validate the frustum intersection logic and ensure compliance with original behavior.
- Updated CMake files to include new source files and tests.