The Frame build rendered the runtime's 1728 square recommendation, but the panel is 2160 square. The eye image is now the recommended size times vr_resolution_scale (archived, 1.25 by default, which is 2160), held to what the view (maxImageRect) and the swapchain allow; the engine's screen size is set to match before the swapchains are made. The log has the recommended size, the largest the view and swapchain take, and what was used. The HUD shift and the rest work from the real width, so they follow. vr_refresh_rate (archived, default 90, standard section 9) is asked for with xrRequestDisplayRefreshRateFB: the highest the runtime offers that isn't over it. The offered list, the request and the result are logged. The earlier "now: 90 Hz" at a 13.89 ms frame period was a mismatch in what was read, not asked for; vr_refresh follows the real frame period. Frame build only. Builds, haven't run it on the headset yet.
14 KiB
Lambda1VR on the Valve Steam Frame
This is a build of Lambda1VR for Valve's Steam Frame. The Frame runs Android apps in a container called Lepton (Android 11, arm64, GLES 3.2) on top of SteamVR's OpenXR runtime. So it's the same Android app, with a few changes to get it going there.
I haven't had it on my head yet. It builds, it runs on the Frame (unattended, on a desk) and it draws the first level in both eyes. See the end for what's checked and what isn't.
Build
You need JDK 17 and NDK 25.1.8937393, same as the Quest build.
cd Projects/Android
sh ./gradlew assembleFrame
The APK ends up in Projects/Android/build/outputs/apk/frame/lambda1vr-frame.apk.
It's signed with the debug key.
The first build downloads the Khronos OpenXR loader (1.1.63) from Maven
Central, checks its SHA-256 and unpacks it into Projects/Android/build/. Nothing
gets committed. The Quest builds don't touch it.
Install and run
Start "Lepton Development" on the Frame and use adb over the network:
adb connect <frame-ip>:5555
adb install -r lambda1vr-frame.apk
adb shell am start -n com.drbeef.lambda1vr.frame/com.drbeef.lambda1vr.GLES3JNIActivity
Lepton Development deletes the apps you install when it exits. For something that sticks around you can add the APK as a devkit title in Steam on the Frame. The game data lives outside the container either way (next section).
Launched from adb the app doesn't get controllers or focus from SteamVR, so use that for logs. Start it from the Steam library to actually play.
Game data
Install Half-Life from Steam on the Frame, and that's it. The Android container shows the headset's
Steam folders (read only), so the game uses the files where Steam put them, there's nothing to copy. At
start the app looks for steamapps/common/Half-Life/valve/liblist.gam in the Steam library (and in the
other libraries libraryfolders.vdf lists, like one on an SD card) and tells the engine to treat that folder
as read only (-rodir). The expansions (Opposing Force, Blue Shift) are in the same folder on Steam.
~/Documents/Lambda1VR is the writable side, and it wins when the same file is in both. Inside Lepton it's
/sdcard/Documents/Lambda1VR, the headset's own folder, so it survives a Lepton reset. It holds your saves and
settings, and the files this app puts there on first start (the replacement weapon models, the sprites, the
loading screens), like it does on the Quest. The engine also makes empty folders there with the names of the
game folders in Steam's. On the Quest it's /sdcard/xash, that hasn't changed.
If there's no Half-Life in Steam on the headset, it falls back to the old way: copy the valve folder from a
PC's Steam copy to ~/Documents/Lambda1VR/valve. A copy there is used too when Steam has one as well (it's
found first, so a stale copy would be used over the Steam files). The log says which one it picked, with a
[data] line.
Controls
The Frame's controllers are a split gamepad. Left has the stick, trigger, grip, bumper, D-pad and View. Right has the stick, trigger, grip, bumper, A B X Y and Menu.
Half-Life's 25th anniversary update ships its own Steam Input layout for a gamepad
(controller_configs/xbox_controller_config_standard.vdf in the game's valve folder). I used that
one, so it works like the gamepad version people already know, and added the VR parts: your head looks,
and the grips are your hands.
| Frame | Playing |
|---|---|
| Left stick | Move |
| Left stick click | Crouch (toggle). Physically ducking still works |
| Right stick left / right | Turn (Lambda1VR's snap or smooth turn) |
| Right stick click | Flashlight |
| Right stick up | Flat screen view (the scoreboard in multiplayer) |
| Right stick down | Laser sight, and steadies a scope |
| Weapon hand trigger | Fire |
| Other hand trigger | Alt fire |
| A | Jump |
| B | Crouch (while held) |
| X | Use |
| Y | Reload |
| Left / right bumper | Previous / next weapon (fire to pick it, like in Half-Life) |
| D-pad up / down | Previous / next weapon, same as the bumpers |
| D-pad right | Last weapon |
| D-pad left | Crowbar |
| Menu | Pause menu. Held for half a second: quick save |
| View | Flashlight (the scoreboard in multiplayer) |
| View, held 1 s | Recentre, with a buzz |
| View, held 3 s | Recentre and use the head height as standing height |
| Weapon hand grip | Use from that hand: grab, push or pull things, press buttons. Behind your head it pulls the crowbar from the backpack |
| Other hand grip | Near the weapon: the two-handed hold (steadier aim, a scope). Well away from it: use from that hand |
The weapon hand is the right one, or the left one with vr_control_scheme 10. Left-handed swaps the
roles that belong to a hand (the triggers, the grips, the laser, where the gun is) and leaves the buttons
where they are.
The grips use what Lambda1VR already had: the game looks for things you can use near the hand and in
front of it, from where the controller is, not from your head. The other grip decides what it is when you
press it, by how far the hands are from each other (under 35 cm it's the gun hold, over 55 cm it's use, in
between it stays what it was last time), and keeps that until you let go. The log says which it picked.
vr_gesture_triggered_use has to stay on for this, it's what gives the short reach and use from the other
hand. Waving a hand about doesn't use things on the Frame, double clicking jump doesn't crouch.
Kept as it was: aiming from the controller, swinging the crowbar, the flashlight beam from your off hand
(vr_headtorch and vr_reversetorch still work), the two-handed scope, reaching behind your head for the
crowbar, ducking by ducking, walking around the room, haptics, the highlight on things you can use. With
your other hand behind your head, the flashlight and View buttons are quick save, and the right stick up
is quick load, as on the Quest. Walk (the slow key) is gone, the stick is analog.
| Frame | In a menu |
|---|---|
| Weapon hand laser and trigger | Point and click |
| A | Confirm |
| B, Menu, View | Back |
| D-pad, left stick | Move |
| Left / right bumper | Previous / next tab or page |
| Right stick up / down | Scroll |
The map is one table, VrFrameMap.c, and tools/steam_frame/tests in the dev branch checks it for an
input that does two things in one situation and for actions nothing can reach.
If SteamVR hands the app Index or Touch controllers instead, they bind like they do on Quest (Index has no Menu for apps, so pause is the left trackpad pressed hard). The Frame layout only kicks in when the runtime says the Frame profile is active.
What's different from the Quest build, and why
- There's a
framebuild type, not a flavour. A flavour would makeassembleDebugandassembleReleasebuild it as well, and the Quest builds should stay as they are. minSdk is 30 there because Lepton is Android 11 (the Quest build is 32). - The manifest overlay (
Projects/Android/src/frame) drops the headtracking requirement, since the container doesn't have that feature and the install would fail, and the Quest and Pico metadata. - Lepton has no OpenXR loader broker, so the APK carries the Khronos loader.
Java loads it as
openxr_loader. - Only OpenGL ES is required from the runtime. Everything else is enabled if the runtime lists it. There's no Pico or Meta code on that build.
- The eye image is the runtime's recommended size (1728 square) times
vr_resolution_scale(archived, default 1.25, which is the panel's own 2160 square), held to what the view and the swapchain allow. The Quest build uses 1.3x of the recommended size. The log has the recommended, the largest allowed and the size used. vr_refresh_rate(archived, default 90) is the rate to ask for: the highest the runtime offers that isn't over it, with the offered list, the request and the result in the log. SteamVR only offers an app the rate in its per-app settings in the headset, so that's often the only one on the list.vr_refresh, which the game uses to scale walking, follows the real frame period.- When the headset is in standby (
shouldRenderis false) the frame goes in with no layers and the game doesn't simulate. Without that the server kept running. - If SteamVR quits the app the game shuts down the normal way.
- The eye framebuffers use plain GLES 3 on the Frame, so the runtime's GL doesn't
need
EXT_multisampled_render_to_texture. It's one swapchain per eye, there's no multiview. - The HUD, game text and titles (the tram credits) stay level with the world when you tilt your head. They were drawn into the eye image, so they rolled with your head. The HUD pass is turned the other way by the roll the world is drawn with, about the middle of each eye's view, so it follows your head's turn and nod but not its roll. The menus (a panel fixed in the world) and a scope are as they were.
- Each eye is drawn from the eye pose the runtime gives (position, and rotation if the eye has one, in head space), and each projection view goes in with that eye's own pose. The Quest build moves each eye by a fixed 65 mm along the side and submits both views with the head's pose. On my Frame the runtime says the eyes are at +-34.5 mm (68.9 mm apart, that's the headset's IPD setting), with no rotation, so for the world the difference is small. The fixed pose in the layer was the wrong thing to tell the compositor though.
- The HUD sits
vr_hud_distancemetres in front of you (archived cvar, default 1.0). The client DLL (a submodule I can't touch) shifts every HUD item by width / 36 per eye, which came to about 0.4 m on the Frame and was hard to look at. While the HUD draws, the DLL is told the eyes are the mono ones (it adds nothing then), and the HUD is shifted instead by the pixels that put it at that distance, from that eye's real position and focal length. A scope and the flat menu screen are left as they were. - The eye framebuffers are made through gl4es but their attachments are set with the
driver's own GL (
eglGetProcAddress). The plaingl*names inlibxash.soare gl4es's, and it swaps a texture it doesn't know for an empty one of its own. - The engine's messages go to logcat under the tag
Xash. They always did, but the developer level defaulted to 0, so nothing came out. It's 3 on the Frame build;-devincommandline.txtstill wins. vr_refreshis taken from the frame period, not from the refresh rates the runtime lists. SteamVR sets the rate per app, and it offered 90 Hz while the display ran at 72.- The log lines that start with
[openxr]say what was picked: runtime, enabled extensions, swapchain format and size, session state, which profiles got bindings and what the runtime answered. - The all-files permission screen doesn't exist on the Frame. There the app checks that it can write to the data folder and only asks if it can't.
Files I changed that the Quest build uses too
Projects/Android/build.gradle: the frame build type, the loader download,buildConfigon (forBuildConfig.STEAM_FRAME)Projects/Android/gradlewandgradlew.bat: the wrapper jar and properties were there already, the scripts weren'tProjects/AndroidPrebuilt/jni/Android.mk: the Frame build says which loader to linkProjects/Android/jni/src/Xash3D/xash3d/engine/Android.mk:L1VR_STEAM_FRAME, and-Wallfor the VR sourcesjava/com/drbeef/lambda1vr/GLES3JNIActivity.java: loader, data folder, permission checkTBXR_Common.c/.h,OpenXrInput.c,L1VR_SurfaceView.c,VrEyeMath.c/.h(new),VrCvars.h,common/port.h, and in the enginegl_rmain.c,gl_draw.c,cl_game.c,cl_scrn.c: the Frame code, all behindL1VR_STEAM_FRAME#include <string.h>inTBXR_Common.candOpenXrInput.c, which-Wallasked for
The Quest build
I built assembleRelease before and after these changes. The repo doesn't ship
the Meta loader, so both builds used the Khronos one as a stand-in to link.
This was done before the shutdown fix below, and the comparison wasn't redone after it.
- Everything except
libxash.sois byte for byte the same. - The
string.hincludes change how a few functions inlibxash.socompile (strcpy becomes the checked version, memset gets inlined differently). Same behaviour. - With those two includes taken out of the Quest build too, the code,
read-only data, data, GOT, dynamic symbols and relocations of
libxash.soare identical to the old build. Only the debug info and the build id differ, because line numbers moved. - The Quest APK gets a
BuildConfigclass in the dex, since Java checks it now. - One real change for Quest too:
jVMinL1VR_SurfaceView.cwas never set, sojni_shutdowndereferenced NULL. The compiler drops everything after a call like that, which made the quit path inAppThreadFunctionrun off the end of the function (an abort on the Frame).JNI_OnLoadsets it now. I haven't built or run it on a Quest.
Checked and not checked
Checked on the Frame, unattended (headset on a desk, nobody wearing it, started over adb):
- the Khronos loader finds SteamVR 2.17.10, 1728x1728 swapchains, the Frame, Index and Touch
binding suggestions all come back
XR_SUCCESS - the session goes through READY and SYNCHRONIZED, the frame loop holds 72 fps
- the first level (
c1a0) loads from the Steam Half-Life files and both eyes show it, with a sensible difference between the eyes, turned 90 degrees too - the app quits cleanly when told to
- the stereo, measured on the eye images with rays traced into the map: surfaces at 2.4 to
10 m land within 0.6 px of the interpupillary distance times the focal length over
the depth, a wall 58 m away shows 0.1 px where 0.7 is right, and the HUD at
vr_hud_distance1 and 2 measured 41.0 and 21.0 px of disparity (41.4 and 20.7 expected)
Checked on the Mac:
assembleFramebuilds- the APK has the pinned Khronos loader and the game libraries, arm64 only
aapt2 dump badgingsays minSdk 30, no headtracking feature, no Quest or Pico metadata, packagecom.drbeef.lambda1vr.frame- the Quest release build is as described above
Not checked yet:
- anything with a person in the headset: how it looks and feels, scale, the HUD, controllers, performance, quitting from the SteamVR menu
- starting it from the Steam library (so far only over adb)
- the replacement weapon and hand models: the repo's
assetsdon't have the.mdlfiles (they're git-ignored), so the engine logs that it can't loadv_hand.mdland friends - that the Frame controller paths are accepted by the runtime (the log lines say)
- the recentre buttons
- Pico: nothing there changed as far as I can tell, but I can't build or run it