The client DLL pushes every HUD item sideways by width / 36 per eye
(it works that out from vr_stereo_side). On the Frame's lenses that's
about 4.5 degrees an eye, so the HUD sat around 0.4 m away and was
hard on the eyes. The DLL is a submodule, so while it draws the HUD the
eyes are given as the mono ones (which is zero for it), and the whole
HUD pass is shifted here instead, by the pixels that put it
vr_hud_distance metres ahead given that eye's real position and focal
length. That also puts the centre print and the engine's FPS counter at
the same place. Scope mode and the screen layer are left alone.
vr_hud_distance is archived, in metres, and defaults to 1.0. The shift
for each eye is logged once.
Frame build only. Builds, haven't had it in the headset yet.
The engine moves each eye a fixed 65 mm times vr_worldscale along the
side and draws both with the head's rotation, and the layer was
submitted with the head's pose for both views. On the Frame the runtime
says where each eye is and which way it points, so use that:
- the world is drawn from each eye's position in head space, scaled by
vr_worldscale, with the eye's turn relative to the head if it has one
- each projection view is submitted with that eye's pose (the head's
placed in the stage, then the eye's in the head), from the same
predicted time the render used
- the mono eyes of a scope and the flat screen layer keep drawing from
one point, as before
The eye maths is in VrEyeMath.c, which has no engine or OpenXR types
so it can be tested on the host. Frame build only.
Builds. Haven't had it in the headset yet.
Position and rotation of each eye in head space, the fov and the eye
separation, once at the start, and again when the poses first are valid.
Needed to check the stereo against what the engine assumes.
The first run on the Frame had both eyes' framebuffers come back as
GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT, and the eyes stayed black. The
plain gl* names in libxash.so are gl4es's (it's linked first), and gl4es
swaps a texture name it hasn't seen for a new empty texture of its own,
so the swapchain image never made it into the attachment. The EXT path
doesn't have the problem since it gets its functions from eglGetProcAddress.
The plain GLES 3 fallback I added does the same now. The framebuffer is
still made and bound through gl4es, because gl4es refuses to bind a
framebuffer it didn't make and the engine draws through it.
Also on the Frame build:
- the engine's messages show up in logcat (tag Xash). They always went
there, but the default developer level is 0 and the commandline.txt
from the assets has no -dev, so nothing was printed. The default is
3 there now, -dev on the commandline still wins.
- vr_refresh comes from the frame period instead of the list of rates
the runtime offers. SteamVR picks the rate per app, it said 90 on
offer while the display ran at 72, and the positional movement
scaling uses vr_refresh.
Builds. Haven't had it in the headset yet.
jVM is a static that nothing assigns, so jni_shutdown dereferences NULL.
The compiler takes that as can't-happen and drops the code after the
call, so when the game quits (AppThreadFunction after Host_Shutdown and
TBXR_LeaveVR) it runs off the end of the function into whatever comes
next. On the Frame that was a call to a fortified snprintf with garbage
arguments and an abort. On the Quest it probably crashes or does
something random on the way out too, it just doesn't matter there since
the app is closing anyway.
JNI_OnLoad gets the VM, so keep it there.
On the Quest the data is in /sdcard/xash. On the Frame that would be gone
whenever the Android container gets reset, so it uses
/sdcard/Documents/Lambda1VR there, which is the headset's own
~/Documents/Lambda1VR. The Java side, the chdir and the engine's
default path all follow it. The Quest path is the same as before.
The container on the Frame has no settings page for the all files
permission, so on that build we just check we can write to the folder
and only ask if we can't (and don't fall over if there's no page).
On the Frame build the bindings go to the Frame's own profile first
(valve/frame_controller_valve), then Index, then Touch. The Pico profile
isn't tried there. Every profile logs what the runtime said to the
suggestion.
The game's actions stay the same: sticks, triggers, grips, stick clicks
and A/B are where they are on Quest. The Frame has no X/Y on the left
controller, so the torch is the left bumper (or the right X) and the
screen view is View (or the right Y). Menu is the right one and is the
pause. Holding View for a second recentres, three seconds also takes the
head height as the standing height, with a buzz on both controllers.
The left-handed schemes get the same physical buttons.
The layout only kicks in when the runtime says the Frame profile is
the active one, so Index or Touch controllers on SteamVR behave like they
do on Quest. Index has no Menu for apps, so its pause is the left
trackpad pressed hard.
Builds. Haven't tried it in the headset yet.
SteamVR doesn't offer what the Quest and Pico runtimes do, and the frame
loop assumed they were there. All of this is behind L1VR_STEAM_FRAME, so
the Quest and Pico code paths stay as they were.
- Only OpenGL ES is required. Everything else (android create instance,
performance settings, refresh rate, the Valve controller extension) is
enabled when the runtime lists it. The android create instance struct
only goes in the chain if that extension is on.
- No Pico or Meta calls on the Frame, and no assuming OPENXR_HMD is set.
- The graphics binding gets the EGLConfig instead of the surface.
- The eye framebuffers fall back to plain GLES 3 when there's no
multisampling, so the runtime doesn't need the EXT extension.
- Render at the recommended size, not 1.3x of it.
- In standby (shouldRender false) the frame goes in with no layers and
the game waits too. Before, the server kept simulating.
- SteamVR quitting the app (exiting, loss pending) goes through the
normal shutdown instead of spinning on events.
- A recentre that takes the head's yaw and floor position, for the
controller code to call. It keeps the view yaw and the world position,
so nothing jumps.
- Log what's going on with an [openxr] prefix: runtime, extensions,
swapchain format and size, session state, refresh rates.
Builds. Haven't tried it in the headset yet.
The Frame runs Android apps in a container (Lepton) that has no OpenXR
loader broker, so the APK needs its own loader. This adds a "frame" build
type: ./gradlew assembleFrame.
It's release plus:
- applicationId .frame, minSdk 30 (the container is Android 11)
- a manifest overlay that drops the headtracking requirement and the
Quest/Pico metadata, since the install fails with it
- the Khronos loader 1.1.63, fetched from Maven Central at build time,
checked against a SHA-256 and unpacked into build/ (not committed)
- L1VR_STEAM_FRAME for the native code, BuildConfig.STEAM_FRAME for Java
Quest builds don't see any of this. I also added the gradle wrapper script
to Projects/Android, since the wrapper jar and properties were already
there, and -Wall for the VR sources.
Haven't run it on the headset yet.
- Prevent the highest IPD setting on Quest 2 and Quest 3s from making the view move with the controller
- Smooth Turn menu option (so user doesn't have to faff with setting a low value)
- Gesture Trigger Use is now enabled by default
- Fixed positional not working at higher refresh rates
- Improvements to the use of the pointer in the menu screen (also seems to fix scroll bars)
- Improved menu item layout
- change to sv_game.c which seems to stop the menu hanging on Start New Game sometimes
- Adjusted draw location of hands
- Don't draw offhand backwards if "Reverse Torch Direction" is enabled
- Update version to 1.5.0
- Ability to select refresh rate in Video Options
- Changed deadzones for controllers
- Updated to build against latest Mobile SDK 1.50
- Enabled Extra Latency Mode
- Building against NDK 21.0.6113669
- Removed hacky use of LOAD_GLES2 and use gl4es properly
- Added Team Beef to Credits
- Correct some bad C