Document the Steam Frame build

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
This commit is contained in:
Claude committed 2026-10-04 08:46:49 +00:00
1 parent b1a8b034d9
commit 6815b10122
5 files changed
+282 -7

No files matched your search

+29 -2
View File
@@ -40,6 +40,7 @@ required = false
mirror_view = "normal"
controller_mode = "wii_remote"
frame_interpolation_fps = 0
refresh_rate = 0
render_scale = 1.0
world_units_per_meter = 500.0
hud_distance_meters = 2.0
@@ -217,11 +218,19 @@ its CPU and GPU domains: `boost`, `sustained_high`, `sustained_low`, `power_savi
request (see `docs/quest-port.md`); desktop runtimes rarely offer the extension, and the setting
then does nothing. It is read at launch, and the session log records whether the runtime accepted
it and any later performance notification (a thermal or rendering warning).
`foveation` (Quest only, default `medium`) shades the edges of the immersive race view more coarsely:
`foveation` (Quest and Steam Frame only, default `medium`) shades the edges of the immersive race view more coarsely:
`off`, `low`, `medium` or `high`, see [Foveated rendering](#foveated-rendering). A session launched
with it off runs without fragment density maps, so going from `off` to a level takes a restart;
between levels, and back to `off`, it is live from the headset panel's VR tab. The launcher's
Settings page has it too.
Settings page has it too. `eye_tracked_foveation` (default on for the Steam Frame, off elsewhere)
centres it on the player's gaze where the runtime offers `XR_EXT_eye_gaze_interaction` with an eye
tracker; see [Eye-tracked foveation](#eye-tracked-foveation).
`refresh_rate` is the display rate in Hz asked of the runtime through `XR_FB_display_refresh_rate`
each time the session starts and whenever it changes, or `0` (the default, `120` on the Steam
Frame) to leave the headset's own. The game renders 60 frames a second, so 120 Hz shows each frame
for exactly two refreshes. A rate the runtime does not list, or declines, is logged and leaves its
own; setting `0` again restores the rate the session started at. It is live from F10 / the headset
panel (*Headset refresh rate*) and the Quest launcher; runtimes without the extension ignore it.
## Controllers
@@ -269,6 +278,12 @@ because hand steering holds a grip down for a whole corner, and C is the game's
game's Wii Remote rumble vibrates both controllers, subject to the ordinary controller-vibration
switch.
The Steam Frame's controllers get their own profile where the runtime offers it
(`XR_VALVE_frame_controller_interaction`, `/interaction_profiles/valve/frame_controller_valve`):
right A, B, trigger and stick as above, left View as the left menu (+), the left shoulder as left Y
(the settings panel), and the left D-pad as the Wii Remote's D-pad (the gamepad's D-pad in
`"gamepad"` mode). The table is in `docs/steam-frame.md`.
**Motion.** Each XR frame the aim and grip poses are located at the measured current time
(`XR_KHR_win32_convert_performance_counter_time`, `XR_KHR_convert_timespec_time` on Android), not
the predicted display time, whose extrapolation sprays fast wrist motion. The grip's linear
@@ -1068,6 +1083,17 @@ ripples give way to a smoother look. It is no fix for a heavy track: on Retro Re
2 at 1.0, GPU-bound at about 40 FPS, no level raised the frame rate, while merging the eye passes
did (39 to 41.5 FPS).
### Eye-tracked foveation
With `eye_tracked_foveation`, a runtime that offers `XR_EXT_eye_gaze_interaction` and reports an eye
tracker (the Steam Frame's SteamVR) has its gaze pose located for each packet's display time and
turned into tangents of each eye's view (`vr/eye_gaze.h`), which `AuroraStereoFrame` carries as
`gaze`/`gazeValid`. Aurora centres the level's rings on the gaze snapped to a cell of two map texels
(about 3 degrees), keeping up to 32 maps per eye, one per cell looked at, and binds a new one once
its upload completes, the previous map staying bound meanwhile. Without a tracked gaze (a blink, no
tracker, the setting off) the map is the forward one above, unchanged. Details and the Steam Frame
checks are in `docs/steam-frame.md`.
## Diagnostics
**F10 > Diagnostics** holds three bug-report aids.
@@ -1250,6 +1276,7 @@ custom DLL's ABI through three borrowed-image copy/readback cycles; run it with
| Windows D3D12 | Implemented: same-adapter, same-device asynchronous OpenXR submission. |
| Windows Vulkan | Implemented, opt-in (`video.graphics_api = "vulkan"`): the runtime creates Dawn's Vulkan instance and device through `XR_KHR_vulkan_enable2`, eyes are copied on the same queue, and Dawn's device guard is held around the four queue-touching OpenXR calls. Needs the custom Dawn from `Launcher/Build-DawnVulkan.ps1`. Raced on SteamVR/PSVR2 at the headset's full rate; other runtimes unexercised. See [Windows Vulkan](#windows-vulkan). |
| Android Vulkan (Meta Quest) | Implemented and running on a Quest 3: the OpenXR side owns its own Vulkan device (`XR_KHR_vulkan_enable2`, `XR_KHR_vulkan_enable` fallback) and shares eyes with Dawn through `AHardwareBuffer`s ordered by sync-fd fences. Controllers arrive through OpenXR actions as a virtual SDL gamepad. See `docs/quest-port.md`. |
| Android Vulkan (Steam Frame) | The same backend in the `steamFrame` flavour, for SteamVR's Android runtime under Lepton: Frame controller profile, 120 Hz request, eye-tracked foveation. Built and unit-tested, not yet run on the headset. See `docs/steam-frame.md`. |
| Linux Vulkan | Not wired. The pinned Dawn package does not expose a native Vulkan device, and the AHardwareBuffer bridge is Android-only; a dma-buf/opaque-fd variant of the same design would cover desktop Linux. |
| Other platforms | Not wired yet. |
+3
View File
@@ -57,6 +57,9 @@ to immersive stereo rendering. VR is opt-in and falls back to the normal desktop
runtime or headset is unavailable. In first person you sit in the cockpit, where the steering wheel
or handlebar turns with your steering, and hand steering by heurazy lets you grab it with the
tracked controllers and turn it. On a Quest the hands can follow the headset's own hand tracking.
A Steam Frame build of the Android app (not yet tested on the headset) adds the Frame controllers'
D-pad, a 120 Hz display for the game's 60 FPS, and foveation that follows your eyes; see
[`docs/steam-frame.md`](docs/steam-frame.md).
See [`OPENXR.md`](OPENXR.md) for setup, configuration, and the current limitations.
**Music ducking.**
+4 -2
View File
@@ -1,8 +1,9 @@
# WiiCompiled VR for Meta Quest (Android)
Standalone Android/OpenXR build of the Mario Kart Wii recompilation for Quest 1,
Quest 2, Quest 3, Quest 3S and Quest Pro. The full design, build walkthrough and current
status live in [docs/quest-port.md](../docs/quest-port.md); this directory only
Quest 2, Quest 3, Quest 3S and Quest Pro, and, as the `steamFrame` flavour, for Valve's Steam
Frame under Lepton ([docs/steam-frame.md](../docs/steam-frame.md)). The full design, build
walkthrough and current status live in [docs/quest-port.md](../docs/quest-port.md); this directory only
holds the Gradle project, its helper scripts, the game kit tooling
(`QuestGameKit.psm1`, `Build-QuestGame.ps1`), the on-headset build toolchain
(`Prepare-QuestToolchain.ps1`, `toolchain/`) and `nod-jni`.
@@ -10,6 +11,7 @@ holds the Gradle project, its helper scripts, the game kit tooling
```powershell
powershell -ExecutionPolicy Bypass -File android/Prepare-QuestDependencies.ps1 # stages the SDL3 3.4.4 AAR once
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Install # the app, debug-signed, installs over adb
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Headset frame # Steam Frame flavour
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Headset quest1 -Install # Quest 1 flavour
```
+6 -3
View File
@@ -819,9 +819,11 @@ Android facts this design rests on, all measured on a Quest 3:
`generate-data-init` and `translate-mod`, for pipelines that generate on
another host.
- `android/`: the Gradle project, with `modernQuest` (the default script
target) and `quest1` headset flavours. They share the application ID and
storage, but select the appropriate CPU baseline, supported-device manifest,
and launcher behavior.
target), `quest1` and `steamFrame` headset flavours. They share the
application ID and storage, but select the appropriate CPU baseline (one
`headsetCpus` map in `app/build.gradle.kts`), manifest and launcher
behavior. `steamFrame` is Valve's Steam Frame under Lepton, SteamOS's
Android layer; see `docs/steam-frame.md`.
`app/src/main/cpp/CMakeLists.txt` adds the repository's `runtime/` as a
subdirectory with those Android choices and builds both game kit probes
(the Retro Rewind one only when the translation includes the mod), which
@@ -850,6 +852,7 @@ powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Headset quest1
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Install # your game, against that kit, into Import (or WheelWizard VR's Build for Quest)
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Product retro_rewind -Mod <RetroRewind6> -Install # the mod and its pack (needs translate-mod output with --retro-wfc-payload)
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Headset quest1 -Install # game package from the Quest 1 kit
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Headset frame # Steam Frame flavour (docs/steam-frame.md)
adb push MarioKart.iso /sdcard/Download/ # then Select disc image in the launcher
```
+240
View File
@@ -0,0 +1,240 @@
# WiiCompiled VR on the Steam Frame
Valve's Steam Frame runs SteamOS on a Snapdragon 8 Gen 3 (Cortex-X4, A720 and A520 cores, Adreno 750),
with 2160x2160 panels per eye at 72 to 144 Hz, eye tracking, and SteamVR as its OpenXR runtime. It runs
Android apps through Lepton, SteamOS's Android layer, where SteamVR provides an Android OpenXR runtime
(OpenXR 1.0, through the Khronos loader's runtime broker). The Steam Frame build is therefore a third
flavour of the Quest app, `steamFrame`: everything in `docs/quest-port.md` below the app shell (the
Vulkan backend, the game kit, `.wcgame` packages, the on-headset build) applies unchanged, and this
document covers what differs.
A native SteamOS ARM64 build is a separate, later piece of work: Linux has no OpenXR graphics backend
yet (see [A native SteamOS build](#a-native-steamos-build)).
**Status: not yet run on a Steam Frame.** Everything below compiles and is unit-tested, but the
device checks at the end are still to do.
## What the flavour changes
| | Quest flavours | `steamFrame` |
| --- | --- | --- |
| CPU target (`kit.json` `androidCpu`) | `cortex-a77` (`kryo` on Quest 1) | `cortex-x4+nosve` |
| `MKW_ANDROID_HEADSET` | `quest` | `steam_frame` (defines `MKW_HEADSET_STEAM_FRAME`) |
| Library entry | `LauncherActivity` (Quest 1: `QuestActivity`) | `FrameEntryActivity` |
| Horizon OS manifest entries | present | removed |
| `[vr] refresh_rate` default | `0` (the headset's own) | `120` |
| `[vr] passthrough` | default on (`XR_FB_passthrough`) | not asked for, default off, setting hidden |
| `[vr] eye_tracked_foveation` default | off | on |
The application ID stays `org.wiicompiled.quest`, so the storage paths in `docs/quest-port.md` hold
as they are. The kit's CPU string differs from the Quest ones, which gives the Frame its own kit
fingerprint: a game built for a Quest is refused on the Frame and the other way round, by the same
checks that keep Quest 1 and modern Quest games apart.
**CPU.** Every core of the 8 Gen 3 implements ARMv9.2, so the products are tuned for the Cortex-X4.
`+nosve` matters: clang auto-vectorises with SVE for a `cortex-x4` (a simple loop compiled with
`-O3` used SVE registers ten times), and Qualcomm's firmware does not expose SVE on this chip, so
those instructions would end the game with `SIGILL`. With `+nosve` the target features read
`-sve -sve2 -sve2-bitperm` and the same loop uses NEON only. The flavour-to-CPU map lives once in
`android/app/build.gradle.kts` (`headsetCpus`), which the kit export also reads now instead of
guessing from the variant name.
**Launch under Lepton.** Lepton starts the one real activity that is both `MAIN` and `LAUNCHER`, and
runs the app in VR when that activity carries a VR category; it ignores `activity-alias` entries.
Quest builds put `LAUNCHER` on the 2D panel (or, on Quest 1, add an alias), so neither works there.
`src/steamFrame/AndroidManifest.xml` makes `FrameEntryActivity` the only `MAIN`/`LAUNCHER` activity,
with `org.khronos.openxr.intent.category.IMMERSIVE_HMD` and `com.oculus.intent.category.VR`. It
shows nothing: it always opens the setup panel (`LauncherActivity`), and when the selected game can
start as it is (game files, a game built for this kit, Retro Rewind's pack, and no enabled mods
still to copy into the pack), it opens `QuestActivity` on top of it. The headset therefore goes
straight into VR, and quitting the game returns to the panel for setup, imports and mods.
`adb logcat -s WiiCompiledLauncher` shows which way it went.
The manifest also removes Horizon OS's own entries (`com.oculus.supportedDevices`, `focusaware`,
`trade_cpu_for_gpu_amount`, the passthrough feature and the hand tracking permissions and feature)
and keeps the Khronos broker queries and the `OPENXR_SYSTEM` permission, which any Android OpenXR
runtime needs.
## Controllers
With `XR_VALVE_frame_controller_interaction` the runtime offers the Frame controller's own profile,
`/interaction_profiles/valve/frame_controller_valve`. Without it SteamVR presents the controllers as
Touch controllers, which loses the left D-pad. `openxr_input.cpp` suggests it after Touch and the
simple controller. Each hand has a thumbstick, trigger, grip and shoulder button. The right hand has
A, B, X, Y and a menu button; the left hand has a D-pad and a View button. Binding paths follow
DolphinXR's port (iChris4/dolphinXR#9). Windows asks for the same extension, so a Frame streaming
from a PC through SteamVR gets the D-pad too.
| Frame controller | Wii Remote mode | Gamepad mode |
| --- | --- | --- |
| Right A | A | South (A) |
| Right B | C (look behind) | East (B) |
| Right trigger | B | Right trigger |
| Right stick up / down | 1 / 2 | Right stick |
| Left View | + (pause) | Start |
| Left shoulder | Settings panel (Touch's left Y) | North (Y) |
| Left D-pad | Wii Remote D-pad | D-pad |
| Left stick, left trigger | Nunchuk stick, Z | Left stick, left trigger |
| Grips, stick clicks, motion, aim | as on Touch (`OPENXR.md`, Controllers) | as on Touch |
| Right X, Y, menu and shoulder | unbound | unbound |
The four D-pad actions are new and also reach the virtual gamepad's D-pad; Touch leaves them unbound,
so nothing changes on a Quest.
## Refresh rate
`[vr] refresh_rate` (Hz, `0` = the headset's own rate) is asked of the runtime through
`XR_FB_display_refresh_rate` each time the session starts running and whenever the setting changes.
The request uses the runtime's own value within half a hertz of the setting (runtimes report 119.98
for 120). Setting it back to `0` restores the rate the session started at. The game renders 60 frames
a second, so at 120 Hz each frame shows for exactly two refreshes. At 72 or 90 Hz some frames show
for one refresh and others for two, which judders. The Frame starts at 120. Render-first pacing
(`docs/quest-port.md`) already waits for each sealed game frame, so on the Frame the pacing summary
should read about 60 `skipped-slots` a second with no `late` cycles.
Lepton may decline the request (frame-control found SteamVR keeping its own rate there). The session
log then says `display refresh rate 120 Hz refused` with the rates it offers, and nothing else
changes. The setting is in the headset panel and in the launcher (VR → Headset); other headsets
offer it as well, at their own default of `0`.
## Eye-tracked foveation
`[vr] eye_tracked_foveation` (default on for the Frame) moves the foveation level's full-density
region to where the player looks:
1. At launch the runtime asks for `XR_EXT_eye_gaze_interaction`. If the system reports an eye tracker,
`OpenXRInput` binds the gaze pose (`/user/eyes_ext/input/gaze_ext/pose`) and locates it for each
packet's display time, in the space the eye views are located in.
2. `vr/eye_gaze.h` turns the gaze into tangents of each eye's own view, which may be canted.
`AuroraStereoFrame` carries them as `gaze` and `gazeValid`, appended after its existing fields.
3. Aurora snaps the gaze to a cell of two map texels (64 pixels, about 3 degrees) and builds that
cell's density map with the level's rings centred on the gaze (`gfx/foveation.hpp`).
Each eye keeps up to 32 maps, one per cell looked at, so a glance back reuses its map instead of
uploading a new one. A new map is bound once its upload has completed, and until then the eye keeps
the map it had. A blink, lost tracking or the setting turned off returns to the map centred on the
forward direction, which is byte-identical to the fixed foveation map. No change to the Dawn patch
was needed: maps stay immutable, and the patch already lets a view be rebound to another map.
The session log reports `OpenXR eye gaze: available` (or that the runtime has no eye tracker),
`OpenXR eye gaze: tracking` at the first tracked sample, and `eye foveation medium following the
gaze` for each eye's first gaze map. Foveation pays only when an eye is pixel-bound
(`OPENXR.md`, Foveated rendering), which the Frame's larger eyes make more likely.
If the Frame's driver offers `VK_QCOM_fragment_density_map_offset` or
`VK_EXT_fragment_density_map_offset`, shifting one map per pass would replace switching between
maps. That needs the Dawn patch to create the eye textures with the offset flag, so it waits for
the device's extension list.
## Building and installing
On the Windows build host described in `docs/quest-port.md`:
```powershell
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Headset frame # the steamFrame APK; checks kit.json says cortex-x4+nosve
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Headset frame -Product base -Data <DATA> # a .wcgame for the Frame's kit
```
The APK lands in `android/app/build/outputs/apk/steamFrame/<configuration>`. WheelWizard VR's
"Build for Quest" builds a Frame game unchanged once it is given the Frame APK (Setup takes the
headset from the APK's kit). WheelWizard itself still needs a Steam Frame choice that fetches that
APK from the release, published as `…-SteamFrame.apk`.
Lepton opens an adb port (5555 and up) for each running Android instance, reachable over the
network. With an Android app running on the Frame, `adb connect <frame-ip>:5555` reaches it from the
build PC. How the APK reaches the Steam library (adb into a Lepton instance, frame-control, or
Steam's own sideloading) is to be confirmed on the device.
## Device checklist
Information to collect first, from the build PC over Lepton's adb port, with an Android app running
on the Frame:
```powershell
adb connect <frame-ip>:5555
$f = "<frame-ip>:5555"
adb -s $f shell "getprop ro.build.version.release; getprop ro.build.version.sdk; getprop ro.product.manufacturer; getprop ro.product.model; getprop ro.product.device; getprop ro.hardware.vulkan; getprop ro.board.platform"
adb -s $f shell "uname -a; getconf PAGE_SIZE; grep -m1 Features /proc/cpuinfo; grep -c processor /proc/cpuinfo"
adb -s $f shell cmd gpu vkjson > frame-vkjson.json
adb -s $f shell "pm list packages | grep -i -E 'xr|valve|steam|khronos|openxr'"
```
and on the Frame itself (Desktop Mode, Konsole):
```bash
uname -a; getconf PAGESIZE; grep -m1 Features /proc/cpuinfo; head -5 /etc/os-release
cat ~/.config/openxr/1/active_runtime.json 2>/dev/null; ls /usr/share/openxr/1/ /etc/xdg/openxr/1/ 2>/dev/null
```
What each answers:
- `vkjson`: whether Lepton's Vulkan driver has what the Android bridge needs:
- `VK_ANDROID_external_memory_android_hardware_buffer`;
- `VK_KHR_external_semaphore_fd` and `VK_KHR_external_fence_fd` with sync fd handles.
Without these the APK route cannot present at all, and the native route becomes the way. It also
shows `VK_EXT_fragment_density_map` (foveation), the density map offset extensions, and which
driver Lepton uses.
- The CPU features line: no `sve`, as the CPU target assumes.
- The page size: on a 16 KB-page kernel the runtime finds out at launch and routes translated memory
accesses through the checked path (`guest_flat_memory.h`, `RequiresCheckedAccess`), which works but
is slower. That is worth knowing before measuring.
- The Android version: the app needs API 29 (Android 10) or newer.
Then, on the first launch, the session log (`Logs/<product>_<stamp>_pid<pid>/console.log` next to
`DATA`) and `adb logcat -s SDL WiiCompiledQuest WiiCompiledLauncher` should show, in order:
1. `OpenXR Android loader initialized`;
2. `OpenXR runtime offers N extensions: ...`, the whole list SteamVR's Android runtime has;
3. `OpenXR initialized: runtime '...'` with SteamVR's name;
4. the negotiated Vulkan binding, then `OpenXR Vulkan swapchains ready`;
5. `display refresh rate 120 Hz requested` (or `refused`, with the rates on offer);
6. the session reaching `FOCUSED`;
7. `OpenXR interaction profiles: left /interaction_profiles/valve/frame_controller_valve, right ...`;
8. `OpenXR eye gaze: available`, then `tracking`;
9. `presentation=virtual-screen` for the menus, and `first immersive packet consumed` on race entry.
Then, in the game:
- the D-pad does tricks, the left shoulder opens the settings panel, and View pauses;
- `debug.wiicompiled.fpslog 1` on a race start shows the pacing summary and the GPU time per pass
with foveation off, fixed and following the gaze;
- `render_scale` starts at 0.8, the Quest's value. The runtime's recommended eye size (logged at
startup) and those measurements decide whether the Frame keeps it.
A black headset with a working mirror points at the AHardwareBuffer copy, as on the Quest
(`docs/quest-port.md`, Validation status).
Open questions only the device can answer:
- Whether Lepton's driver supports the AHardwareBuffer and sync fd bridge.
- Whether Lepton shows the 2D setup panel while the app runs in VR mode. If it does not, the game
still starts directly once a `.wcgame` with the game files has been imported, but importing needs
the panel.
- Whether SteamVR grants 120 Hz.
- Whether the gaze needs an Android permission under Lepton.
- Whether "Build on this headset" can run its toolchain through `/system/bin/linker64` inside
Lepton. A game built on the PC does not depend on it.
## A native SteamOS build
Valve recommends native Linux ARM64 builds for the Frame, and one would avoid Lepton and the
two-device copy. The pieces:
- **Backend.** The Windows Vulkan backend (`openxr_vulkan_win32.cpp`), where OpenXR creates Dawn's
own device and eyes are copied on Dawn's queue, ports almost as it is. Only its `_WIN32` guards are
platform-specific.
- **Interop.** Aurora's `vulkan_win32_interop.cpp` finds the patched Dawn's exports with
`GetModuleHandleW`. A static Linux Dawn would reference them directly, and `aurora_core.cmake`
compiles that file on Windows only.
- **Dawn.** A linux-aarch64 Dawn built with `aurora-main/patches/dawn` (the hook ABI and density
maps), as `android/Build-QuestDawn.ps1` already does for Android.
- **`openxr_integration.cpp`.** It needs a Linux branch asking for `XR_KHR_vulkan_enable2` and
`XR_KHR_convert_timespec_time`. Today its `#else` is Android's and requires
`XR_KHR_android_create_instance`.
- **Controller timing.** `openxr_input.cpp` needs a `__linux__` branch for the input clock.
- **CPU target.** An `MKW_LINUX_CPU` knob in place of `-mcpu=native`, for cross-builds.
- **Android-gated fixes.** The Adreno vertex padding, `headset_owns_display` and the
`last_pass_feeding_replay` saving are gated on `__ANDROID__`. They would follow the GPU or the
headset instead.
- **Packaging.** SteamOS packaging, and a way to build the player's game for it.